@jagilber-org/index-server 1.22.0 → 1.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/CODE_OF_CONDUCT.md +2 -0
  3. package/CONTRIBUTING.md +32 -2
  4. package/README.md +83 -20
  5. package/SECURITY.md +17 -5
  6. package/dist/config/dashboardConfig.d.ts +3 -0
  7. package/dist/config/dashboardConfig.js +3 -0
  8. package/dist/config/defaultValues.d.ts +1 -1
  9. package/dist/config/defaultValues.js +1 -1
  10. package/dist/config/featureConfig.d.ts +2 -0
  11. package/dist/config/featureConfig.js +6 -1
  12. package/dist/config/runtimeConfig.d.ts +1 -1
  13. package/dist/config/runtimeConfig.js +8 -9
  14. package/dist/dashboard/client/admin.html +173 -54
  15. package/dist/dashboard/client/css/admin.css +151 -0
  16. package/dist/dashboard/client/js/admin.auth.js +25 -11
  17. package/dist/dashboard/client/js/admin.config.js +1 -1
  18. package/dist/dashboard/client/js/admin.feedback.js +328 -0
  19. package/dist/dashboard/client/js/admin.graph.js +120 -18
  20. package/dist/dashboard/client/js/admin.instructions.js +27 -13
  21. package/dist/dashboard/client/js/admin.logs.js +1 -5
  22. package/dist/dashboard/client/js/admin.maintenance.js +53 -8
  23. package/dist/dashboard/client/js/admin.messaging.js +1 -4
  24. package/dist/dashboard/client/js/admin.overview.js +5 -1
  25. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  26. package/dist/dashboard/client/js/admin.utils.js +43 -1
  27. package/dist/dashboard/client/js/mermaid.min.js +813 -537
  28. package/dist/dashboard/export/DataExporter.js +2 -1
  29. package/dist/dashboard/server/AdminPanel.d.ts +3 -0
  30. package/dist/dashboard/server/AdminPanel.js +132 -35
  31. package/dist/dashboard/server/ApiRoutes.js +40 -9
  32. package/dist/dashboard/server/DashboardServer.js +1 -1
  33. package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
  34. package/dist/dashboard/server/FileMetricsStorage.js +52 -5
  35. package/dist/dashboard/server/HttpTransport.js +6 -0
  36. package/dist/dashboard/server/InstanceManager.js +7 -2
  37. package/dist/dashboard/server/KnowledgeStore.js +7 -2
  38. package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
  39. package/dist/dashboard/server/MetricsCollector.js +113 -17
  40. package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
  41. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  42. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
  43. package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
  44. package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
  45. package/dist/dashboard/server/routes/admin.routes.js +35 -27
  46. package/dist/dashboard/server/routes/alerts.routes.js +4 -3
  47. package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
  48. package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
  49. package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
  50. package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
  51. package/dist/dashboard/server/routes/graph.routes.js +10 -13
  52. package/dist/dashboard/server/routes/index.d.ts +1 -0
  53. package/dist/dashboard/server/routes/index.js +74 -39
  54. package/dist/dashboard/server/routes/instances.routes.js +2 -1
  55. package/dist/dashboard/server/routes/instructions.routes.js +46 -27
  56. package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
  57. package/dist/dashboard/server/routes/logs.routes.js +5 -4
  58. package/dist/dashboard/server/routes/messaging.routes.js +15 -14
  59. package/dist/dashboard/server/routes/metrics.routes.js +14 -13
  60. package/dist/dashboard/server/routes/scripts.routes.js +6 -3
  61. package/dist/dashboard/server/routes/status.routes.js +25 -6
  62. package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
  63. package/dist/dashboard/server/routes/usage.routes.js +2 -1
  64. package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
  65. package/dist/dashboard/server/utils/escapeHtml.js +11 -0
  66. package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
  67. package/dist/dashboard/server/utils/pathContainment.js +15 -0
  68. package/dist/dashboard/server/wsInit.js +2 -2
  69. package/dist/lib/mcpStdioLogging.d.ts +165 -0
  70. package/dist/lib/mcpStdioLogging.js +287 -0
  71. package/dist/schemas/index.d.ts +37 -2
  72. package/dist/schemas/index.js +27 -3
  73. package/dist/server/backgroundServicesStartup.d.ts +7 -1
  74. package/dist/server/backgroundServicesStartup.js +25 -8
  75. package/dist/server/certInit.d.ts +97 -0
  76. package/dist/server/certInit.js +359 -0
  77. package/dist/server/certInit.types.d.ts +92 -0
  78. package/dist/server/certInit.types.js +34 -0
  79. package/dist/server/handshake/fallbackFrames.d.ts +31 -0
  80. package/dist/server/handshake/fallbackFrames.js +38 -0
  81. package/dist/server/handshake/initializeDetector.d.ts +31 -0
  82. package/dist/server/handshake/initializeDetector.js +88 -0
  83. package/dist/server/handshake/protocol.d.ts +15 -0
  84. package/dist/server/handshake/protocol.js +37 -0
  85. package/dist/server/handshake/readyEmitter.d.ts +6 -0
  86. package/dist/server/handshake/readyEmitter.js +88 -0
  87. package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
  88. package/dist/server/handshake/safetyFallbacks.js +134 -0
  89. package/dist/server/handshake/stdinSniffer.d.ts +1 -0
  90. package/dist/server/handshake/stdinSniffer.js +260 -0
  91. package/dist/server/handshake/tracing.d.ts +16 -0
  92. package/dist/server/handshake/tracing.js +95 -0
  93. package/dist/server/handshakeManager.d.ts +23 -23
  94. package/dist/server/handshakeManager.js +36 -466
  95. package/dist/server/index-server.d.ts +23 -0
  96. package/dist/server/index-server.js +194 -9
  97. package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
  98. package/dist/server/mcpReadOnlySurfaces.js +297 -0
  99. package/dist/server/sdkServer.js +69 -7
  100. package/dist/server/transport.d.ts +5 -6
  101. package/dist/server/transport.js +46 -64
  102. package/dist/server/transportFactory.d.ts +3 -9
  103. package/dist/server/transportFactory.js +18 -380
  104. package/dist/services/atomicFs.d.ts +3 -0
  105. package/dist/services/atomicFs.js +171 -13
  106. package/dist/services/auditLog.d.ts +17 -2
  107. package/dist/services/auditLog.js +75 -14
  108. package/dist/services/bootstrapGating.js +1 -1
  109. package/dist/services/categoryRules.d.ts +10 -0
  110. package/dist/services/categoryRules.js +17 -0
  111. package/dist/services/classificationService.js +7 -5
  112. package/dist/services/embeddingService.d.ts +27 -11
  113. package/dist/services/embeddingService.js +51 -14
  114. package/dist/services/feedbackStorage.d.ts +39 -0
  115. package/dist/services/feedbackStorage.js +88 -0
  116. package/dist/services/handlers/instructions.add.js +429 -317
  117. package/dist/services/handlers/instructions.groom.js +128 -31
  118. package/dist/services/handlers/instructions.import.js +56 -23
  119. package/dist/services/handlers/instructions.patch.js +43 -32
  120. package/dist/services/handlers/instructions.query.js +20 -29
  121. package/dist/services/handlers/instructions.shared.d.ts +54 -0
  122. package/dist/services/handlers/instructions.shared.js +126 -1
  123. package/dist/services/handlers.activation.js +83 -81
  124. package/dist/services/handlers.dashboardConfig.d.ts +2 -2
  125. package/dist/services/handlers.dashboardConfig.js +1 -2
  126. package/dist/services/handlers.diagnostics.js +75 -54
  127. package/dist/services/handlers.feedback.d.ts +4 -11
  128. package/dist/services/handlers.feedback.js +11 -333
  129. package/dist/services/handlers.gates.js +69 -37
  130. package/dist/services/handlers.graph.js +2 -2
  131. package/dist/services/handlers.help.js +2 -2
  132. package/dist/services/handlers.instructionSchema.js +4 -2
  133. package/dist/services/handlers.integrity.js +42 -22
  134. package/dist/services/handlers.messaging.js +1 -1
  135. package/dist/services/handlers.metrics.js +51 -6
  136. package/dist/services/handlers.prompt.js +10 -2
  137. package/dist/services/handlers.search.js +94 -44
  138. package/dist/services/handlers.trace.js +1 -1
  139. package/dist/services/handlers.usage.js +38 -7
  140. package/dist/services/indexContext.d.ts +21 -1
  141. package/dist/services/indexContext.js +267 -82
  142. package/dist/services/indexLoader.d.ts +1 -0
  143. package/dist/services/indexLoader.js +28 -8
  144. package/dist/services/instructionRecordValidation.d.ts +39 -0
  145. package/dist/services/instructionRecordValidation.js +388 -0
  146. package/dist/services/instructions.dispatcher.js +4 -4
  147. package/dist/services/loaderSchemaValidator.d.ts +15 -0
  148. package/dist/services/loaderSchemaValidator.js +69 -0
  149. package/dist/services/logger.js +11 -2
  150. package/dist/services/mcpLogBridge.d.ts +49 -0
  151. package/dist/services/mcpLogBridge.js +83 -0
  152. package/dist/services/ownershipService.js +18 -8
  153. package/dist/services/performanceBaseline.js +23 -22
  154. package/dist/services/promptReviewService.d.ts +3 -1
  155. package/dist/services/promptReviewService.js +41 -13
  156. package/dist/services/regexSafety.d.ts +6 -0
  157. package/dist/services/regexSafety.js +46 -0
  158. package/dist/services/seedBootstrap.js +4 -4
  159. package/dist/services/storage/factory.d.ts +14 -1
  160. package/dist/services/storage/factory.js +61 -1
  161. package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
  162. package/dist/services/storage/jsonEmbeddingStore.js +83 -0
  163. package/dist/services/storage/jsonFileStore.d.ts +3 -1
  164. package/dist/services/storage/jsonFileStore.js +8 -6
  165. package/dist/services/storage/migrationEngine.d.ts +13 -0
  166. package/dist/services/storage/migrationEngine.js +31 -0
  167. package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
  168. package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
  169. package/dist/services/storage/sqliteStore.d.ts +3 -1
  170. package/dist/services/storage/sqliteStore.js +2 -2
  171. package/dist/services/storage/types.d.ts +48 -1
  172. package/dist/services/toolRegistry.js +77 -67
  173. package/dist/services/toolRegistry.zod.js +89 -86
  174. package/dist/services/tracing.js +5 -4
  175. package/dist/utils/envUtils.d.ts +4 -0
  176. package/dist/utils/envUtils.js +7 -0
  177. package/dist/utils/memoryMonitor.js +11 -10
  178. package/package.json +11 -4
  179. package/schemas/instruction.schema.json +38 -1
  180. package/scripts/copy-dashboard-assets.mjs +1 -1
  181. package/scripts/dist/README.md +1 -1
  182. package/scripts/setup-wizard.mjs +781 -0
  183. package/server.json +1 -0
  184. package/dist/externalClientLib.d.ts +0 -1
  185. package/dist/externalClientLib.js +0 -2
  186. package/dist/portableClientWrapper.d.ts +0 -1
  187. package/dist/portableClientWrapper.js +0 -2
  188. package/dist/services/indexingService.d.ts +0 -1
  189. package/dist/services/indexingService.js +0 -2
@@ -4,7 +4,7 @@
4
4
  var _configRefreshTimer = null;
5
5
  var _collapsedCategories = {};
6
6
 
7
- function escapeHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
7
+ var escapeHtml = window.adminUtils.escapeHtml;
8
8
 
9
9
  function buildFlagRow(f, featureFlags) {
10
10
  var isBool = f.type === 'boolean';
@@ -0,0 +1,328 @@
1
+ /**
2
+ * admin.feedback.js — Dashboard Feedback CRUD panel.
3
+ *
4
+ * Human-operator surface for browsing, creating, editing, and deleting
5
+ * persisted feedback entries, plus a client-side GitHub issue handoff.
6
+ *
7
+ * Design constraints (Morpheus architecture review / decisions.md G-1..G-8):
8
+ * - CRUD against /api/admin/feedback (operator-tier, NOT the MCP surface)
9
+ * - GitHub handoff is client-side ONLY — window.open with pre-filled URL
10
+ * - No server-side GitHub API calls, no token handling, no OAuth
11
+ * - GitHub target: https://github.com/jagilber-org/index-server
12
+ */
13
+
14
+ (function () {
15
+ 'use strict';
16
+
17
+ // ── Constants ───────────────────────────────────────────────────────────────
18
+
19
+ const GITHUB_ISSUES_URL = 'https://github.com/jagilber-org/index-server/issues/new';
20
+ const API_BASE = '/api/admin/feedback';
21
+
22
+ const STATUS_COLORS = {
23
+ 'new': '#3b82f6',
24
+ 'acknowledged': '#8b5cf6',
25
+ 'in-progress': '#ff9830',
26
+ 'resolved': '#73bf69',
27
+ 'closed': '#8e959e',
28
+ };
29
+
30
+ const SEV_COLORS = {
31
+ 'low': '#73bf69',
32
+ 'medium': '#ff9830',
33
+ 'high': '#f2495c',
34
+ 'critical': '#dc2626',
35
+ };
36
+
37
+ // ── Module state ────────────────────────────────────────────────────────────
38
+
39
+ let _entries = [];
40
+ let _selectedId = null;
41
+ let _filterText = '';
42
+ let _statusFilter = '';
43
+ let _editMode = null; // 'create' | 'edit' | null
44
+
45
+ // ── Public API ──────────────────────────────────────────────────────────────
46
+
47
+ window.initFeedback = async function () {
48
+ setupDelegation();
49
+ await loadEntries();
50
+ renderTable();
51
+ };
52
+
53
+ // ── GitHub URL builder (tested by Tank B-2..B-5) ────────────────────────────
54
+
55
+ function buildGitHubIssueUrl(entry) {
56
+ const params = new URLSearchParams();
57
+ const titleParam = entry && entry.title
58
+ ? `[Feedback] ${entry.title}`
59
+ : '[Feedback] New Issue';
60
+ params.set('title', titleParam);
61
+
62
+ const lines = [];
63
+ if (entry) {
64
+ if (entry.type) lines.push(`**Type:** ${entry.type}`);
65
+ if (entry.severity) lines.push(`**Severity:** ${entry.severity}`);
66
+ if (entry.status) lines.push(`**Status:** ${entry.status}`);
67
+ if (entry.description) lines.push(`\n${entry.description}`);
68
+ }
69
+ params.set('body', lines.join('\n'));
70
+ return `${GITHUB_ISSUES_URL}?${params.toString()}`;
71
+ }
72
+
73
+ // ── Event delegation (CSP-safe) ─────────────────────────────────────────────
74
+
75
+ function setupDelegation() {
76
+ const section = document.getElementById('feedback-section');
77
+ if (!section || section._fbDelegated) return;
78
+ section._fbDelegated = true;
79
+
80
+ section.addEventListener('click', function (e) {
81
+ const el = e.target.closest('[data-fb-action]');
82
+ if (!el) return;
83
+ const action = el.dataset.fbAction;
84
+
85
+ if (action === 'refresh') { loadEntries().then(renderTable); return; }
86
+ if (action === 'create') { openCreate(); return; }
87
+ if (action === 'edit') { openEdit(el.dataset.id); return; }
88
+ if (action === 'delete') { confirmDelete(el.dataset.id); return; }
89
+ if (action === 'save') { saveEntry(); return; }
90
+ if (action === 'cancel') { closeDetail(); return; }
91
+ if (action === 'github') { openGitHubHandoff(); return; }
92
+ if (action === 'row-select') { openEdit(el.dataset.id); return; }
93
+ });
94
+
95
+ const filterInput = document.getElementById('feedback-filter');
96
+ if (filterInput) {
97
+ filterInput.addEventListener('input', function () {
98
+ _filterText = this.value.toLowerCase();
99
+ renderTable();
100
+ });
101
+ }
102
+
103
+ const statusSel = document.getElementById('feedback-status-filter');
104
+ if (statusSel) {
105
+ statusSel.addEventListener('change', function () {
106
+ _statusFilter = this.value;
107
+ renderTable();
108
+ });
109
+ }
110
+ }
111
+
112
+ // ── API helpers ─────────────────────────────────────────────────────────────
113
+
114
+ async function apiFetch(method, path, body) {
115
+ const opts = { method, headers: {} };
116
+ if (body !== undefined) {
117
+ opts.headers['Content-Type'] = 'application/json';
118
+ opts.body = JSON.stringify(body);
119
+ }
120
+ const res = await fetch(path, opts);
121
+ const text = await res.text();
122
+ let data;
123
+ try { data = JSON.parse(text); } catch { data = {}; }
124
+ if (!res.ok) throw Object.assign(new Error(data.error || `HTTP ${res.status}`), { status: res.status });
125
+ return data;
126
+ }
127
+
128
+ async function loadEntries() {
129
+ try {
130
+ const data = await apiFetch('GET', API_BASE);
131
+ _entries = Array.isArray(data.entries) ? data.entries : [];
132
+ } catch (err) {
133
+ console.warn('[feedback] loadEntries failed:', err.message);
134
+ _entries = [];
135
+ showTableMessage(`⚠️ Failed to load entries: ${err.message}`);
136
+ }
137
+ }
138
+
139
+ // ── Table rendering ─────────────────────────────────────────────────────────
140
+
141
+ function renderTable() {
142
+ const container = document.getElementById('feedback-table');
143
+ if (!container) return;
144
+
145
+ const filtered = _entries.filter(e => {
146
+ const matchText = !_filterText ||
147
+ (e.title || '').toLowerCase().includes(_filterText) ||
148
+ (e.type || '').toLowerCase().includes(_filterText) ||
149
+ (e.description || '').toLowerCase().includes(_filterText);
150
+ const matchStatus = !_statusFilter || e.status === _statusFilter;
151
+ return matchText && matchStatus;
152
+ });
153
+
154
+ if (filtered.length === 0) {
155
+ container.innerHTML = `<div class="feedback-empty">${
156
+ _entries.length === 0
157
+ ? 'No feedback entries. Click <strong>New Entry</strong> to create one.'
158
+ : 'No entries match the current filter.'
159
+ }</div>`;
160
+ return;
161
+ }
162
+
163
+ const rows = filtered.map(e => {
164
+ const sColor = STATUS_COLORS[e.status] || '#8e959e';
165
+ const sevColor = SEV_COLORS[e.severity] || '#8e959e';
166
+ const ts = e.timestamp ? new Date(e.timestamp).toLocaleString() : '—';
167
+ return `<tr class="feedback-row${e.id === _selectedId ? ' selected' : ''}"
168
+ data-fb-action="row-select" data-id="${esc(e.id)}">
169
+ <td class="fb-cell fb-id">${esc(e.id.slice(0, 8))}…</td>
170
+ <td class="fb-cell fb-title">${esc(e.title)}</td>
171
+ <td class="fb-cell fb-type">${esc(e.type)}</td>
172
+ <td class="fb-cell fb-sev" style="color:${sevColor}">${esc(e.severity)}</td>
173
+ <td class="fb-cell fb-status">
174
+ <span class="fb-badge" style="background:${sColor}22;color:${sColor};border:1px solid ${sColor}44">${esc(e.status)}</span>
175
+ </td>
176
+ <td class="fb-cell fb-ts text-dim">${esc(ts)}</td>
177
+ <td class="fb-cell fb-actions" onclick="event.stopPropagation()">
178
+ <button class="action-btn sm" data-fb-action="edit" data-id="${esc(e.id)}" title="Edit">✏️</button>
179
+ <button class="action-btn sm danger" data-fb-action="delete" data-id="${esc(e.id)}" title="Delete">🗑️</button>
180
+ </td>
181
+ </tr>`;
182
+ });
183
+
184
+ container.innerHTML = `
185
+ <table class="fb-table">
186
+ <thead>
187
+ <tr>
188
+ <th class="fb-th">ID</th>
189
+ <th class="fb-th">Title</th>
190
+ <th class="fb-th">Type</th>
191
+ <th class="fb-th">Severity</th>
192
+ <th class="fb-th">Status</th>
193
+ <th class="fb-th">Created</th>
194
+ <th class="fb-th">Actions</th>
195
+ </tr>
196
+ </thead>
197
+ <tbody>${rows.join('')}</tbody>
198
+ </table>`;
199
+ }
200
+
201
+ function showTableMessage(msg) {
202
+ const container = document.getElementById('feedback-table');
203
+ if (!container) return;
204
+ const message = document.createElement('div');
205
+ message.className = 'feedback-empty';
206
+ message.textContent = String(msg || '');
207
+ container.replaceChildren(message);
208
+ }
209
+
210
+ // ── Detail / edit panel ──────────────────────────────────────────────────────
211
+
212
+ function openCreate() {
213
+ _selectedId = null;
214
+ _editMode = 'create';
215
+ populateForm(null);
216
+ showDetail('New Feedback Entry');
217
+ document.getElementById('feedback-delete-btn').style.display = 'none';
218
+ document.getElementById('feedback-github-btn').style.display = 'none';
219
+ }
220
+
221
+ function openEdit(id) {
222
+ const entry = _entries.find(e => e.id === id);
223
+ if (!entry) return;
224
+ _selectedId = id;
225
+ _editMode = 'edit';
226
+ populateForm(entry);
227
+ showDetail('Edit Feedback Entry');
228
+ document.getElementById('feedback-delete-btn').style.display = '';
229
+ document.getElementById('feedback-github-btn').style.display = '';
230
+ renderTable(); // update row highlight
231
+ }
232
+
233
+ function showDetail(titleText) {
234
+ const detail = document.getElementById('feedback-detail');
235
+ const titleEl = document.getElementById('feedback-detail-title');
236
+ if (detail) detail.classList.remove('hidden');
237
+ if (titleEl) titleEl.textContent = titleText;
238
+ if (detail) detail.scrollIntoView({ behavior: 'smooth', block: 'start' });
239
+ }
240
+
241
+ function closeDetail() {
242
+ _selectedId = null;
243
+ _editMode = null;
244
+ const detail = document.getElementById('feedback-detail');
245
+ if (detail) detail.classList.add('hidden');
246
+ renderTable();
247
+ }
248
+
249
+ function populateForm(entry) {
250
+ setValue('feedback-entry-title', entry ? entry.title : '');
251
+ setValue('feedback-entry-type', entry ? entry.type : 'bug-report');
252
+ setValue('feedback-entry-severity', entry ? entry.severity : 'medium');
253
+ setValue('feedback-entry-status', entry ? entry.status : 'new');
254
+ setValue('feedback-entry-description', entry ? (entry.description || '') : '');
255
+ }
256
+
257
+ function readForm() {
258
+ return {
259
+ title: (getValue('feedback-entry-title') || '').trim(),
260
+ type: getValue('feedback-entry-type') || 'other',
261
+ severity: getValue('feedback-entry-severity') || 'medium',
262
+ status: getValue('feedback-entry-status') || 'new',
263
+ description: (getValue('feedback-entry-description') || '').trim(),
264
+ };
265
+ }
266
+
267
+ // ── CRUD operations ──────────────────────────────────────────────────────────
268
+
269
+ async function saveEntry() {
270
+ const data = readForm();
271
+ if (!data.title) {
272
+ alert('Title is required.');
273
+ return;
274
+ }
275
+ try {
276
+ if (_editMode === 'create') {
277
+ await apiFetch('POST', API_BASE, { type: data.type, severity: data.severity, title: data.title, description: data.description });
278
+ } else if (_editMode === 'edit' && _selectedId) {
279
+ await apiFetch('PATCH', `${API_BASE}/${_selectedId}`, { status: data.status, title: data.title, description: data.description, severity: data.severity });
280
+ }
281
+ await loadEntries();
282
+ closeDetail();
283
+ } catch (err) {
284
+ alert(`Save failed: ${err.message}`);
285
+ }
286
+ }
287
+
288
+ async function confirmDelete(id) {
289
+ if (!id) return;
290
+ const entry = _entries.find(e => e.id === id);
291
+ const label = entry ? entry.title : id;
292
+ if (!confirm(`Delete entry: "${label}"?`)) return;
293
+ try {
294
+ await apiFetch('DELETE', `${API_BASE}/${id}`);
295
+ if (_selectedId === id) closeDetail();
296
+ await loadEntries();
297
+ renderTable();
298
+ } catch (err) {
299
+ alert(`Delete failed: ${err.message}`);
300
+ }
301
+ }
302
+
303
+ // ── GitHub handoff (client-side only — no server calls, no token) ────────────
304
+
305
+ function openGitHubHandoff() {
306
+ const data = readForm();
307
+ const entry = _selectedId
308
+ ? { ..._entries.find(e => e.id === _selectedId), ...data }
309
+ : data;
310
+ const url = buildGitHubIssueUrl(entry);
311
+ window.open(url, '_blank', 'noopener,noreferrer');
312
+ }
313
+
314
+ // ── Utilities ────────────────────────────────────────────────────────────────
315
+
316
+ const esc = window.adminUtils.escapeHtml;
317
+
318
+ function setValue(id, val) {
319
+ const el = document.getElementById(id);
320
+ if (el) el.value = val;
321
+ }
322
+
323
+ function getValue(id) {
324
+ const el = document.getElementById(id);
325
+ return el ? el.value : '';
326
+ }
327
+
328
+ })();
@@ -30,28 +30,130 @@
30
30
  host.replaceChildren(box);
31
31
  }
32
32
 
33
- function renderGraphSvg(host, svgMarkup){
34
- if(!host) return;
33
+ const SVG_ALLOWED_TAGS = new Map([
34
+ ['svg', 'svg'], ['g', 'g'], ['defs', 'defs'], ['style', 'style'], ['title', 'title'], ['desc', 'desc'],
35
+ ['path', 'path'], ['rect', 'rect'], ['circle', 'circle'], ['ellipse', 'ellipse'], ['polygon', 'polygon'], ['polyline', 'polyline'], ['line', 'line'],
36
+ ['text', 'text'], ['tspan', 'tspan'], ['textpath', 'textPath'],
37
+ ['marker', 'marker'], ['pattern', 'pattern'], ['clippath', 'clipPath'], ['mask', 'mask'], ['symbol', 'symbol'], ['use', 'use'],
38
+ ['lineargradient', 'linearGradient'], ['radialgradient', 'radialGradient'], ['stop', 'stop']
39
+ ]);
40
+ const SVG_ALLOWED_ATTRS = new Set([
41
+ 'id', 'class', 'role',
42
+ 'xmlns', 'xmlns:xlink', 'xml:space',
43
+ 'viewbox', 'preserveaspectratio', 'version',
44
+ 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'dx', 'dy', 'cx', 'cy', 'r', 'rx', 'ry',
45
+ 'width', 'height', 'd', 'points', 'pathlength',
46
+ 'transform', 'transform-origin',
47
+ 'fill', 'fill-opacity', 'fill-rule',
48
+ 'stroke', 'stroke-opacity', 'stroke-width', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit',
49
+ 'opacity', 'color',
50
+ 'font-family', 'font-size', 'font-style', 'font-weight',
51
+ 'text-anchor', 'dominant-baseline', 'alignment-baseline', 'baseline-shift', 'letter-spacing', 'word-spacing',
52
+ 'marker-start', 'marker-mid', 'marker-end',
53
+ 'markerunits', 'markerwidth', 'markerheight', 'orient', 'refx', 'refy',
54
+ 'patternunits', 'patterncontentunits', 'patterntransform',
55
+ 'gradientunits', 'gradienttransform', 'spreadmethod',
56
+ 'offset', 'stop-color', 'stop-opacity',
57
+ 'clip-path', 'clippathunits', 'mask', 'maskunits', 'maskcontentunits', 'filter',
58
+ 'href', 'xlink:href', 'style',
59
+ 'aria-hidden', 'aria-label', 'aria-labelledby', 'aria-describedby'
60
+ ]);
61
+
62
+ function hasUnsafeUrlValue(value){
63
+ const text = String(value || '');
64
+ if(!text) return false;
65
+ if(/\b(?:javascript|vbscript|data)\s*:/i.test(text)) return true;
66
+ const urlMatches = text.matchAll(/url\(([^)]*)\)/gi);
67
+ for(const match of urlMatches){
68
+ const inner = String(match[1] || '').trim().replace(/^['"]|['"]$/g, '');
69
+ if(!/^#[-\w:.]+$/i.test(inner)) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ function sanitizeSvgStyleText(cssText){
75
+ const css = String(cssText || '');
76
+ if(!css.trim()) return '';
77
+ if(/@import|expression\s*\(|-moz-binding|behavior\s*:|<\/style/i.test(css)) return '';
78
+ if(hasUnsafeUrlValue(css)) return '';
79
+ return css;
80
+ }
81
+
82
+ function sanitizeSvgNode(node){
83
+ if(!node) return null;
84
+ if(node.nodeType === Node.TEXT_NODE){
85
+ return document.createTextNode(node.textContent || '');
86
+ }
87
+ if(node.nodeType !== Node.ELEMENT_NODE) return null;
88
+ const tag = String(node.localName || node.nodeName || '').toLowerCase();
89
+ const canonicalTag = SVG_ALLOWED_TAGS.get(tag);
90
+ if(!canonicalTag) return null;
91
+ const clean = document.createElementNS('http://www.w3.org/2000/svg', canonicalTag);
92
+ Array.from(node.attributes || []).forEach((attr) => {
93
+ const name = String(attr.name || '').toLowerCase();
94
+ if(!name || name.startsWith('on') || !SVG_ALLOWED_ATTRS.has(name)) return;
95
+ const value = attr.value || '';
96
+ if((name === 'href' || name === 'xlink:href') && !/^\s*#[-\w:.]+\s*$/i.test(value)) return;
97
+ if(hasUnsafeUrlValue(value)) return;
98
+ if(name === 'style'){
99
+ const safeStyle = sanitizeSvgStyleText(value);
100
+ if(!safeStyle) return;
101
+ clean.setAttribute(attr.name, safeStyle);
102
+ return;
103
+ }
104
+ if(name === 'xlink:href'){
105
+ clean.setAttributeNS('http://www.w3.org/1999/xlink', attr.name, value);
106
+ return;
107
+ }
108
+ clean.setAttribute(attr.name, value);
109
+ });
110
+ if(tag === 'style'){
111
+ const safeCss = sanitizeSvgStyleText(node.textContent || '');
112
+ if(!safeCss) return null;
113
+ clean.textContent = safeCss;
114
+ return clean;
115
+ }
116
+ Array.from(node.childNodes || []).forEach((child) => {
117
+ const cleanChild = sanitizeSvgNode(child);
118
+ if(cleanChild) clean.appendChild(cleanChild);
119
+ });
120
+ return clean;
121
+ }
122
+
123
+ function sanitizeGraphSvg(svgMarkup){
124
+ // Step 1: parse user-supplied (mermaid-rendered) markup. This is the only
125
+ // call to DOMParser.parseFromString in this module; everything below works
126
+ // off allowlist-reconstructed nodes.
35
127
  const parsed = new DOMParser().parseFromString(String(svgMarkup || ''), 'image/svg+xml');
36
128
  if(parsed.querySelector('parsererror') || parsed.documentElement.tagName.toLowerCase() !== 'svg'){
129
+ return null;
130
+ }
131
+ // Step 2: walk the parsed tree and rebuild it as fresh nodes via
132
+ // createElementNS using the SVG_ALLOWED_TAGS / SVG_ALLOWED_ATTRS
133
+ // allowlists. The returned subtree shares no node identity with `parsed`.
134
+ const reconstructed = sanitizeSvgNode(parsed.documentElement);
135
+ if(!reconstructed || reconstructed.tagName.toLowerCase() !== 'svg') return null;
136
+ if(!reconstructed.getAttribute('xmlns')) reconstructed.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
137
+ // Step 3: serialize the reconstructed (safe) tree to a string and re-parse
138
+ // it. This conclusively severs any data-flow link from the original
139
+ // untrusted markup so static analyzers see only allowlisted text reach
140
+ // DOM insertion sites.
141
+ const safeMarkup = new XMLSerializer().serializeToString(reconstructed);
142
+ const reparsed = new DOMParser().parseFromString(safeMarkup, 'image/svg+xml');
143
+ if(reparsed.querySelector('parsererror') || reparsed.documentElement.tagName.toLowerCase() !== 'svg'){
144
+ return null;
145
+ }
146
+ return reparsed.documentElement;
147
+ }
148
+
149
+ function renderGraphSvg(host, svgMarkup){
150
+ if(!host) return;
151
+ const safeSvg = sanitizeGraphSvg(svgMarkup);
152
+ if(!safeSvg){
37
153
  renderGraphTextMessage(host, 'Mermaid render failed:: invalid SVG output');
38
154
  return;
39
155
  }
40
- parsed.querySelectorAll('script, foreignObject, iframe, object, embed, style, base, meta').forEach(node => node.remove());
41
- parsed.querySelectorAll('*').forEach((node) => {
42
- Array.from(node.attributes).forEach((attr) => {
43
- const name = attr.name.toLowerCase();
44
- const value = attr.value || '';
45
- if(name.startsWith('on')){
46
- node.removeAttribute(attr.name);
47
- return;
48
- }
49
- if((name === 'href' || name === 'xlink:href') && /^\s*(javascript:|data:)/i.test(value)){
50
- node.removeAttribute(attr.name);
51
- }
52
- });
53
- });
54
- host.replaceChildren(document.importNode(parsed.documentElement, true));
156
+ host.replaceChildren(safeSvg);
55
157
  }
56
158
 
57
159
  async function reloadGraphMermaid(){
@@ -266,7 +368,7 @@
266
368
  } catch(procErr){ setGraphMetaProgress('process-error','a='+attemptId); }
267
369
  } else {
268
370
  if(target) target.textContent = `(graph unavailable${lastErr?': '+(lastErr.message||lastErr):''})`;
269
- setGraphMetaProgress('unavailable','err='+(lastErr && (lastErr.message||String(lastErr))||'none'));
371
+ setGraphMetaProgress('unavailable','err='+((lastErr && (lastErr.message||String(lastErr))) || 'none'));
270
372
  }
271
373
  clearTimeout(__graphReloadWatchdog);
272
374
  __graphReloadInFlight = false;
@@ -5,10 +5,7 @@
5
5
 
6
6
  // Helper: safe global references (these live on page scope)
7
7
  const globals = window;
8
-
9
- function escapeHtml(s) {
10
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
11
- }
8
+ const escapeHtml = window.adminUtils.escapeHtml;
12
9
 
13
10
  function sanitizeHtmlFragment(html) {
14
11
  const template = document.createElement('template');
@@ -54,7 +51,21 @@
54
51
  if (!signal) return '';
55
52
  const colors = { 'outdated': '#f2495c', 'not-relevant': '#ff9830', 'helpful': '#73bf69', 'applied': '#5794f2' };
56
53
  const color = colors[signal] || '#888';
57
- return '<span class="signal-badge" style="background:' + color + '22;color:' + color + ';border:1px solid ' + color + '44;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;">' + signal + '</span>';
54
+ return '<span class="signal-badge" style="background:' + color + '22;color:' + color + ';border:1px solid ' + color + '44;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;">' + escapeHtml(signal) + '</span>';
55
+ }
56
+
57
+ function wireInstructionListActions(listEl) {
58
+ if (!listEl) return;
59
+ listEl.querySelectorAll('[data-instruction-action]').forEach((button) => {
60
+ if (button.__instructionActionBound) return;
61
+ button.addEventListener('click', () => {
62
+ const action = button.getAttribute('data-instruction-action');
63
+ const instructionName = button.getAttribute('data-instruction-name') || '';
64
+ if (action === 'edit') editInstruction(instructionName);
65
+ if (action === 'delete') deleteInstruction(instructionName);
66
+ });
67
+ button.__instructionActionBound = true;
68
+ });
58
69
  }
59
70
 
60
71
  async function loadInstructionCategories() {
@@ -227,26 +238,29 @@
227
238
  const comment = usage.lastComment || '';
228
239
  const signalHtml = signal ? getSignalBadge(signal) : '<span style="opacity:.4;font-size:10px;">none</span>';
229
240
  const commentTip = comment ? ' title="Last comment: ' + escapeHtml(comment.slice(0, 200)) + '"' : '';
241
+ const safeSize = escapeHtml(String(instr.size ?? '0'));
242
+ const safeSizeCategory = escapeHtml(String(instr.sizeCategory || 'unknown'));
243
+ const safeModified = escapeHtml(new Date(instr.mtime).toLocaleString());
230
244
  return `
231
245
  <div class="instruction-item" data-instruction="${escapedName}">
232
246
  <div class="instruction-item-header">
233
247
  <div class="instruction-name">${highlightedName}</div>
234
248
  <div class="instruction-actions">
235
- <button class="action-btn" onclick="editInstruction('${escapedName}')">✏ Edit</button>
236
- <button class="action-btn danger" onclick="deleteInstruction('${escapedName}')">🗑 Delete</button>
249
+ <button class="action-btn" data-instruction-action="edit" data-instruction-name="${escapedName}">✏ Edit</button>
250
+ <button class="action-btn danger" data-instruction-action="delete" data-instruction-name="${escapedName}">🗑 Delete</button>
237
251
  </div>
238
252
  </div>
239
253
  <div class="instruction-meta">
240
254
  <div class="meta-chip" title="Category"><span class="chip-label">CAT</span><span class="chip-value">${safeCat}</span></div>
241
- <div class="meta-chip" title="Size"><span class="chip-label">SIZE</span><span class="chip-value">${instr.size}</span><span class="chip-sub">(${escapeHtml(instr.sizeCategory)})</span></div>
242
- <div class="meta-chip" title="Last Modified"><span class="chip-label">MTIME</span><span class="chip-value">${new Date(instr.mtime).toLocaleString()}</span></div>
255
+ <div class="meta-chip" title="Size"><span class="chip-label">SIZE</span><span class="chip-value">${safeSize}</span><span class="chip-sub">(${safeSizeCategory})</span></div>
256
+ <div class="meta-chip" title="Last Modified"><span class="chip-label">MTIME</span><span class="chip-value">${safeModified}</span></div>
243
257
  <div class="meta-chip" title="Usage Count"><span class="chip-label">USES</span><span class="chip-value">${usageCount}</span></div>
244
258
  <div class="meta-chip"${commentTip}><span class="chip-label">SIGNAL</span><span class="chip-value">${signalHtml}</span></div>
245
259
  </div>
246
260
  <div class="instruction-summary">${highlightedSummary || '<span class="summary-empty">No summary</span>'}</div>
247
261
  </div>`;
248
262
  }).join('');
249
- const listEl = document.getElementById('instructions-list'); if(listEl) listEl.innerHTML = rows;
263
+ const listEl = document.getElementById('instructions-list'); if(listEl) { listEl.innerHTML = rows; wireInstructionListActions(listEl); }
250
264
  buildInstructionPaginationControls(totalFiltered);
251
265
  try { console.debug('[admin.instructions] renderInstructionList: rendered rows=', pageItems.length); } catch(e){}
252
266
  try { const dbg = document.getElementById('admin-debug'); if(dbg) dbg.textContent = JSON.stringify({ stage:'renderInstructionList', filtered: totalFiltered, rendered: pageItems.length, page: globals.instructionPage }, null, 2); } catch(e){}
@@ -487,11 +501,11 @@
487
501
  return;
488
502
  }
489
503
  const rows = results.results.map(r=>{
490
- let safeName = (r.name||'').replace(/[&<>]/g, c=> ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
491
- let safeSnippet = (r.snippet||'').replace(/[&<>]/g, c=> ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])).replace(/\*\*(.+?)\*\*/g,'<mark>$1</mark>');
504
+ let safeName = escapeHtml(r.name || '');
505
+ let safeSnippet = escapeHtml(r.snippet || '').replace(/\*\*(.+?)\*\*/g,'<mark>$1</mark>');
492
506
  safeName = highlightMatch(safeName, trimmed, isRegex);
493
507
  safeSnippet = highlightMatch(safeSnippet, trimmed, isRegex);
494
- const cats = Array.isArray(r.categories) && r.categories.length? r.categories.slice(0,6).map(c => String(c).replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]))).join(', ') : '—';
508
+ const cats = Array.isArray(r.categories) && r.categories.length ? r.categories.slice(0,6).map(c => escapeHtml(c)).join(', ') : '—';
495
509
  return `<div class="instruction-global-result" style="background:#1f2228; border:1px solid #2c3038; border-radius:4px; padding:6px 8px; margin-bottom:6px;">
496
510
  <div style="font-weight:600; font-size:12px;">${safeName} <span style="opacity:.55; font-weight:400;">(${cats})</span></div>
497
511
  <div style="font-size:11px; white-space:normal;">${safeSnippet}</div>
@@ -99,11 +99,7 @@
99
99
  if (el) el.innerHTML = '<div class="info">Log viewer cleared</div>';
100
100
  }
101
101
 
102
- function escapeHtml(text) {
103
- const div = document.createElement('div');
104
- div.textContent = text;
105
- return div.innerHTML;
106
- }
102
+ const escapeHtml = window.adminUtils.escapeHtml;
107
103
 
108
104
  // Expose functions for staged migration
109
105
  window.loadLogs = loadLogs;