claude-memory-layer 1.0.49 → 1.0.51
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/dist/cli/index.js +442 -8
- package/dist/cli/index.js.map +4 -4
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/server/api/index.js +439 -5
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +441 -7
- package/dist/server/index.js.map +4 -4
- package/dist/ui/assets/js/bootstrap.js +63 -1
- package/dist/ui/assets/js/disclosure.js +158 -10
- package/dist/ui/assets/js/modals.js +39 -18
- package/dist/ui/assets/js/overview.js +99 -9
- package/dist/ui/assets/js/state.js +28 -1
- package/dist/ui/assets/js/views.js +626 -15
- package/dist/ui/index.html +130 -0
- package/dist/ui/style.css +460 -0
- package/package.json +3 -1
|
@@ -30,9 +30,41 @@ async function loadProjects() {
|
|
|
30
30
|
option.textContent = `${p.projectName} (${p.dbSizeHuman})`;
|
|
31
31
|
select.appendChild(option);
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
const savedProject = window.localStorage?.getItem('cml.dashboard.project') || '';
|
|
35
|
+
if (!state.currentProject && savedProject && state.projects.some(p => p.hash === savedProject)) {
|
|
36
|
+
state.currentProject = savedProject;
|
|
37
|
+
select.value = savedProject;
|
|
38
|
+
}
|
|
39
|
+
updateProjectScopeUI();
|
|
33
40
|
} catch (error) {
|
|
34
41
|
console.error('Failed to load projects:', error);
|
|
42
|
+
updateProjectScopeUI();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function updateProjectScopeUI() {
|
|
47
|
+
const label = document.getElementById('scope-context-label');
|
|
48
|
+
const detail = document.getElementById('scope-context-detail');
|
|
49
|
+
const disclosureBadge = document.getElementById('disclosure-scope-badge');
|
|
50
|
+
const empty = document.getElementById('global-empty-state');
|
|
51
|
+
const project = state.currentProject ? state.projects.find(p => p.hash === state.currentProject) : null;
|
|
52
|
+
const eventCount = state.stats?.storage?.eventCount || 0;
|
|
53
|
+
const sessionCount = state.stats?.sessions?.total || 0;
|
|
54
|
+
const vectorCount = state.stats?.storage?.vectorCount || 0;
|
|
55
|
+
|
|
56
|
+
if (project) {
|
|
57
|
+
if (label) label.textContent = `Scope: ${project.projectName}`;
|
|
58
|
+
if (detail) detail.textContent = `Project-local memory · ${project.dbSizeHuman || project.hash}`;
|
|
59
|
+
if (disclosureBadge) disclosureBadge.textContent = `Search → Expand → Source · Project-local: ${project.projectName}`;
|
|
60
|
+
if (empty) empty.hidden = true;
|
|
61
|
+
return;
|
|
35
62
|
}
|
|
63
|
+
|
|
64
|
+
if (label) label.textContent = 'Scope: All projects';
|
|
65
|
+
if (detail) detail.textContent = 'Global aggregate view. Select a project for live project-local sessions and retrieval evidence.';
|
|
66
|
+
if (disclosureBadge) disclosureBadge.textContent = 'Search → Expand → Source · Global scope unless a project is selected';
|
|
67
|
+
if (empty) empty.hidden = !(eventCount === 0 && sessionCount === 0 && vectorCount === 0);
|
|
36
68
|
}
|
|
37
69
|
|
|
38
70
|
function setupEventListeners() {
|
|
@@ -108,6 +140,19 @@ function setupEventListeners() {
|
|
|
108
140
|
await loadUserPromptsView();
|
|
109
141
|
});
|
|
110
142
|
}
|
|
143
|
+
|
|
144
|
+
const sessionRefresh = document.getElementById('session-refresh');
|
|
145
|
+
if (sessionRefresh) {
|
|
146
|
+
sessionRefresh.addEventListener('click', async () => {
|
|
147
|
+
await loadSessionInspectorView();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
document.querySelectorAll('#session-snapshot-tabs .sort-btn').forEach(btn => {
|
|
151
|
+
btn.addEventListener('click', () => {
|
|
152
|
+
setSessionSnapshotTab(btn.dataset.sessionSnapshotTab || 'overview');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
111
156
|
const userPromptPrev = document.getElementById('user-prompt-prev');
|
|
112
157
|
if (userPromptPrev) {
|
|
113
158
|
userPromptPrev.addEventListener('click', async () => {
|
|
@@ -131,6 +176,8 @@ function setupEventListeners() {
|
|
|
131
176
|
if (projectSelect) {
|
|
132
177
|
projectSelect.addEventListener('change', async (e) => {
|
|
133
178
|
state.currentProject = e.target.value;
|
|
179
|
+
if (window.localStorage) window.localStorage.setItem('cml.dashboard.project', state.currentProject || '');
|
|
180
|
+
updateProjectScopeUI();
|
|
134
181
|
await refreshData();
|
|
135
182
|
if (state.chartInstance) {
|
|
136
183
|
state.chartInstance.destroy();
|
|
@@ -143,9 +190,10 @@ function setupEventListeners() {
|
|
|
143
190
|
await initActivityChart();
|
|
144
191
|
// Reload current view if not overview
|
|
145
192
|
if (state.currentView !== 'overview') {
|
|
146
|
-
switchView(state.currentView);
|
|
193
|
+
await switchView(state.currentView, { forceReload: true });
|
|
147
194
|
}
|
|
148
195
|
updateChatProjectScope();
|
|
196
|
+
updateProjectScopeUI();
|
|
149
197
|
});
|
|
150
198
|
}
|
|
151
199
|
|
|
@@ -160,6 +208,20 @@ function setupEventListeners() {
|
|
|
160
208
|
vectorHealthRecoverBtn.addEventListener('click', recoverVectorHealth);
|
|
161
209
|
}
|
|
162
210
|
|
|
211
|
+
const playgroundRunBtn = document.getElementById('playground-run-btn');
|
|
212
|
+
if (playgroundRunBtn) {
|
|
213
|
+
playgroundRunBtn.addEventListener('click', runPlaygroundDryRun);
|
|
214
|
+
}
|
|
215
|
+
const playgroundQuery = document.getElementById('playground-query');
|
|
216
|
+
if (playgroundQuery) {
|
|
217
|
+
playgroundQuery.addEventListener('keydown', (e) => {
|
|
218
|
+
if (e.key === 'Enter') {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
runPlaygroundDryRun();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
163
225
|
// Stat cards
|
|
164
226
|
document.querySelectorAll('.stat-card[data-stat]').forEach(card => {
|
|
165
227
|
card.addEventListener('click', () => {
|
|
@@ -22,6 +22,65 @@ function renderDisclosureStatus(message, isError = false) {
|
|
|
22
22
|
status.style.color = isError ? 'var(--error)' : 'var(--text-muted)';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function disclosureString(value) {
|
|
26
|
+
if (value === undefined || value === null || value === '') return null;
|
|
27
|
+
return String(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeDisclosureEventId(value) {
|
|
31
|
+
const raw = disclosureString(value);
|
|
32
|
+
if (!raw) return null;
|
|
33
|
+
return raw.startsWith('event:') ? raw.slice('event:'.length) : raw;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getDisclosureJumpTarget(...candidates) {
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const target = getDisclosureJumpTargetFromCandidate(candidate);
|
|
39
|
+
if (target) return target;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getDisclosureJumpTargetFromCandidate(candidate) {
|
|
45
|
+
if (!candidate || typeof candidate !== 'object') return null;
|
|
46
|
+
|
|
47
|
+
const nestedCandidates = [];
|
|
48
|
+
if (candidate.primaryEvent) nestedCandidates.push(candidate.primaryEvent);
|
|
49
|
+
if (candidate.rawEvent) nestedCandidates.push(candidate.rawEvent);
|
|
50
|
+
if (candidate.event) nestedCandidates.push(candidate.event);
|
|
51
|
+
if (Array.isArray(candidate.rawEvents)) nestedCandidates.push(...candidate.rawEvents);
|
|
52
|
+
for (const nested of nestedCandidates) {
|
|
53
|
+
const target = getDisclosureJumpTargetFromCandidate(nested);
|
|
54
|
+
if (target) return target;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const metadata = candidate.metadata && typeof candidate.metadata === 'object' ? candidate.metadata : {};
|
|
58
|
+
const sessionId = disclosureString(candidate.sessionId || metadata.sessionId);
|
|
59
|
+
if (!sessionId) return null;
|
|
60
|
+
|
|
61
|
+
const eventId = normalizeDisclosureEventId(
|
|
62
|
+
candidate.eventId ||
|
|
63
|
+
metadata.eventId ||
|
|
64
|
+
candidate.id ||
|
|
65
|
+
candidate.sourceRef ||
|
|
66
|
+
(Array.isArray(candidate.eventIds) ? candidate.eventIds[0] : null)
|
|
67
|
+
);
|
|
68
|
+
return { sessionId, eventId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function disclosureAttrArg(value) {
|
|
72
|
+
if (typeof jsAttrArg === 'function') return jsAttrArg(value);
|
|
73
|
+
return escapeHtml(JSON.stringify(String(value ?? '')));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderDisclosureJumpButton(target, label = 'Open in Sessions') {
|
|
77
|
+
if (!target?.sessionId) return '';
|
|
78
|
+
return `
|
|
79
|
+
<button type="button" class="inline-action-btn disclosure-session-jump" onclick="event.stopPropagation(); jumpToSession(${disclosureAttrArg(target.sessionId)}, ${disclosureAttrArg(target.eventId || '')})">
|
|
80
|
+
<i class="ri-corner-right-up-line"></i> ${escapeHtml(label)}
|
|
81
|
+
</button>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
25
84
|
async function handleDisclosureSearch() {
|
|
26
85
|
const input = document.getElementById('disclosure-search-input');
|
|
27
86
|
const query = (input?.value || '').trim();
|
|
@@ -35,6 +94,7 @@ async function handleDisclosureSearch() {
|
|
|
35
94
|
state.isDisclosureLoading = true;
|
|
36
95
|
state.disclosureResults = [];
|
|
37
96
|
state.disclosureMeta = null;
|
|
97
|
+
state.disclosureQuery = query;
|
|
38
98
|
state.disclosureSelectedId = null;
|
|
39
99
|
state.disclosureExpansion = null;
|
|
40
100
|
state.disclosureSource = null;
|
|
@@ -61,10 +121,14 @@ async function handleDisclosureSearch() {
|
|
|
61
121
|
|
|
62
122
|
state.disclosureResults = data.results || [];
|
|
63
123
|
state.disclosureMeta = data.meta || null;
|
|
124
|
+
state.isDisclosureLoading = false;
|
|
125
|
+
if (button) button.disabled = false;
|
|
64
126
|
renderDisclosureStatus(`Search layer returned ${state.disclosureResults.length} result(s). Click a result to expand/source.`);
|
|
65
127
|
renderDisclosureResults();
|
|
66
128
|
} catch (error) {
|
|
67
129
|
state.disclosureResults = [];
|
|
130
|
+
state.isDisclosureLoading = false;
|
|
131
|
+
if (button) button.disabled = false;
|
|
68
132
|
renderDisclosureStatus(error.message || 'Disclosure search failed', true);
|
|
69
133
|
renderDisclosureResults();
|
|
70
134
|
} finally {
|
|
@@ -96,27 +160,39 @@ function renderDisclosureResults() {
|
|
|
96
160
|
· fallback=${meta.fallbackApplied ? 'yes' : 'no'}
|
|
97
161
|
</div>`;
|
|
98
162
|
|
|
163
|
+
const query = state.disclosureQuery || document.getElementById('disclosure-search-input')?.value || '';
|
|
99
164
|
const resultHtml = state.disclosureResults.map((r, idx) => {
|
|
100
165
|
const active = r.id === state.disclosureSelectedId ? ' active' : '';
|
|
101
166
|
const score = Number(r.score || 0).toFixed(3);
|
|
102
167
|
const reasons = (r.reasons || []).map(reason => `<span class="disclosure-chip">${escapeHtml(reason)}</span>`).join('');
|
|
103
168
|
const provenance = renderDisclosureProvenance(r.metadata, ['sourceProjectHash', 'sourceEntryId', 'topics']);
|
|
169
|
+
const scope = getDisclosureScopeLabel(r);
|
|
170
|
+
const jumpTarget = getDisclosureJumpTarget(r);
|
|
171
|
+
const jumpButton = renderDisclosureJumpButton(jumpTarget);
|
|
104
172
|
return `
|
|
105
|
-
<
|
|
173
|
+
<div class="disclosure-result${active}" data-result-id="${escapeHtml(r.id)}" role="button" tabindex="0">
|
|
106
174
|
<div class="disclosure-result-head">
|
|
107
175
|
<span class="event-type-badge">#${idx + 1} ${escapeHtml(r.resultType || 'result')}</span>
|
|
176
|
+
<span class="disclosure-scope-pill">${escapeHtml(scope)}</span>
|
|
108
177
|
<span class="disclosure-score">score ${score}</span>
|
|
109
178
|
</div>
|
|
110
179
|
<div class="disclosure-title">${escapeHtml(r.title || r.sourceRef || r.id)}</div>
|
|
111
|
-
<div class="disclosure-snippet"
|
|
180
|
+
<div class="disclosure-snippet" title="${escapeHtml(r.snippet || '(no snippet)')}">${highlightDisclosureText(r.snippet || '(no snippet)', query)}</div>
|
|
112
181
|
${provenance}
|
|
113
|
-
<div class="disclosure-
|
|
114
|
-
|
|
182
|
+
<div class="disclosure-rank-explain"><strong>Why this ranked</strong>${reasons || '<span class="disclosure-chip">no_reason</span>'}</div>
|
|
183
|
+
<div class="disclosure-result-cta"><span>Inspect evidence <i class="ri-arrow-right-line"></i></span>${jumpButton}</div>
|
|
184
|
+
</div>`;
|
|
115
185
|
}).join('');
|
|
116
186
|
|
|
117
187
|
container.innerHTML = metaHtml + resultHtml;
|
|
118
188
|
container.querySelectorAll('.disclosure-result').forEach(btn => {
|
|
119
|
-
|
|
189
|
+
const openResult = () => loadDisclosureDrilldown(btn.dataset.resultId);
|
|
190
|
+
btn.addEventListener('click', openResult);
|
|
191
|
+
btn.addEventListener('keydown', (event) => {
|
|
192
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
openResult();
|
|
195
|
+
});
|
|
120
196
|
});
|
|
121
197
|
}
|
|
122
198
|
|
|
@@ -171,7 +247,7 @@ function renderDisclosureDrilldown(isLoading = false, message = null, isError =
|
|
|
171
247
|
const surrounding = (expansion.surroundingFacts || []).map(f => `
|
|
172
248
|
<div class="disclosure-context-item">
|
|
173
249
|
<span class="event-type-badge">${escapeHtml(f.resultType || 'fact')}</span>
|
|
174
|
-
<span>${escapeHtml(f.snippet || f.title || f.id || '')}</span>
|
|
250
|
+
<span>${escapeHtml(buildSafeDisclosurePreview(f.snippet || f.title || f.id || ''))}</span>
|
|
175
251
|
</div>`).join('') || '<div class="disclosure-empty compact">No surrounding facts.</div>';
|
|
176
252
|
|
|
177
253
|
const related = (expansion.relatedSources || []).map(s => `
|
|
@@ -183,33 +259,105 @@ function renderDisclosureDrilldown(isLoading = false, message = null, isError =
|
|
|
183
259
|
const rawEvent = source?.primaryEvent || source?.rawEvents?.[0] || source?.rawEvent || source?.event || null;
|
|
184
260
|
const isSharedSource = source?.sourceType === 'shared_troubleshooting';
|
|
185
261
|
const sourcePreview = rawEvent
|
|
186
|
-
?
|
|
262
|
+
? buildSafeSourcePreview(rawEvent)
|
|
187
263
|
: isSharedSource
|
|
188
264
|
? 'No local raw events for this shared source.'
|
|
189
265
|
: (source ? JSON.stringify(source, null, 2) : (message || 'Source layer returned no raw event.'));
|
|
266
|
+
const sourceSafetyNote = rawEvent && hasCompactionBoilerplate(rawEvent.content || rawEvent.preview || '')
|
|
267
|
+
? '<div class="snapshot-note">Context compaction boilerplate hidden from the default evidence preview.</div>'
|
|
268
|
+
: '';
|
|
190
269
|
const sourceProvenance = source?.metadata
|
|
191
270
|
? renderDisclosureProvenance(source.metadata, ['sourceProjectHash', 'sourceEntryId', 'topics', 'rootCause', 'solution', 'symptoms', 'confidence', 'usageCount'])
|
|
192
271
|
: '';
|
|
193
272
|
const sourceProvenanceBlock = sourceProvenance
|
|
194
273
|
? `<div class="modal-section-title">${isSharedSource ? 'Shared source provenance' : 'Source metadata'}</div>${sourceProvenance}`
|
|
195
274
|
: '';
|
|
275
|
+
const sourceJumpButton = renderDisclosureJumpButton(
|
|
276
|
+
getDisclosureJumpTarget(source, rawEvent, expansion.target),
|
|
277
|
+
'Open in Sessions'
|
|
278
|
+
);
|
|
196
279
|
|
|
197
280
|
container.innerHTML = `
|
|
281
|
+
<div class="disclosure-stepper" aria-label="Search → Expand → Source">
|
|
282
|
+
<span class="active">1 Search result</span>
|
|
283
|
+
<span class="active">2 Expanded context</span>
|
|
284
|
+
<span class="active">3 Source evidence</span>
|
|
285
|
+
</div>
|
|
198
286
|
<div class="disclosure-layer">
|
|
199
|
-
<div class="modal-section-title">
|
|
200
|
-
<div class="modal-content-block">${escapeHtml(expansion.
|
|
287
|
+
<div class="modal-section-title">Search result</div>
|
|
288
|
+
<div class="modal-content-block">${escapeHtml(expansion.target?.title || expansion.target?.snippet || state.disclosureSelectedId || '')}</div>
|
|
289
|
+
<div class="modal-section-title">Expanded context</div>
|
|
290
|
+
<div class="modal-content-block">${escapeHtml(buildSafeDisclosurePreview(expansion.expandedContext || expansion.target?.snippet || ''))}</div>
|
|
201
291
|
<div class="modal-section-title">Surrounding context</div>
|
|
202
292
|
<div class="disclosure-context-list">${surrounding}</div>
|
|
203
293
|
<div class="modal-section-title">Related sources</div>
|
|
204
294
|
<div class="disclosure-reasons">${related}</div>
|
|
205
295
|
</div>
|
|
206
296
|
<div class="disclosure-layer">
|
|
207
|
-
<div class="modal-section-title">Source
|
|
297
|
+
<div class="modal-section-title">Source evidence</div>
|
|
298
|
+
<div class="disclosure-safe-label">Safe preview</div>
|
|
208
299
|
<div class="modal-content-block">${escapeHtml(sourcePreview)}</div>
|
|
300
|
+
${sourceJumpButton}
|
|
301
|
+
${sourceSafetyNote}
|
|
302
|
+
${rawEvent ? '<button class="sort-btn" type="button" disabled title="Raw transcript and metadata are intentionally hidden in this dashboard panel.">Show raw/meta text</button>' : ''}
|
|
209
303
|
${sourceProvenanceBlock}
|
|
210
304
|
</div>`;
|
|
211
305
|
}
|
|
212
306
|
|
|
307
|
+
function getDisclosureScopeLabel(result) {
|
|
308
|
+
const id = String(result?.id || result?.sourceRef || '');
|
|
309
|
+
if (id.startsWith('shared:') || String(result?.sourceRef || '').startsWith('shared:')) return 'Shared';
|
|
310
|
+
if (result?.metadata?.sourceProjectHash) return 'Project-local';
|
|
311
|
+
return state.currentProject ? 'Project-local' : 'Global';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function escapeRegExp(value) {
|
|
315
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function highlightDisclosureText(value, query) {
|
|
319
|
+
let html = escapeHtml(String(value || ''));
|
|
320
|
+
const terms = String(query || '')
|
|
321
|
+
.split(/\s+/)
|
|
322
|
+
.map(term => term.trim())
|
|
323
|
+
.filter(term => term.length >= 2)
|
|
324
|
+
.slice(0, 5);
|
|
325
|
+
for (const term of terms) {
|
|
326
|
+
const escapedTerm = escapeRegExp(escapeHtml(term));
|
|
327
|
+
html = html.replace(new RegExp(`(${escapedTerm})`, 'ig'), '<mark>$1</mark>');
|
|
328
|
+
}
|
|
329
|
+
return html;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hasCompactionBoilerplate(value) {
|
|
333
|
+
return /CONTEXT COMPACTION|REFERENCE ONLY/i.test(String(value || ''));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildSafeDisclosurePreview(value) {
|
|
337
|
+
const text = String(value || '');
|
|
338
|
+
if (!hasCompactionBoilerplate(text)) return text;
|
|
339
|
+
const lines = text.split(/\r?\n/);
|
|
340
|
+
const safeLines = [];
|
|
341
|
+
for (const line of lines) {
|
|
342
|
+
if (hasCompactionBoilerplate(line)) break;
|
|
343
|
+
safeLines.push(line);
|
|
344
|
+
}
|
|
345
|
+
const safeText = safeLines.join('\n').trim();
|
|
346
|
+
const note = '[context compaction handoff hidden from preview]';
|
|
347
|
+
return safeText ? `${safeText}\n${note}` : note;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildSafeSourcePreview(rawEvent) {
|
|
351
|
+
const rawContent = String(rawEvent.content || rawEvent.preview || '');
|
|
352
|
+
const safeLines = rawContent
|
|
353
|
+
.split(/\r?\n/)
|
|
354
|
+
.filter(line => !hasCompactionBoilerplate(line))
|
|
355
|
+
.slice(0, 8);
|
|
356
|
+
const header = `${rawEvent.eventType || rawEvent.type || 'event'} · ${rawEvent.timestamp || ''}`;
|
|
357
|
+
const body = safeLines.join('\n').slice(0, 1200);
|
|
358
|
+
return `${header}\n${body || '(safe preview empty)'}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
213
361
|
function renderDisclosureProvenance(metadata, allowedKeys = null) {
|
|
214
362
|
if (!metadata) return '';
|
|
215
363
|
const entries = Object.entries(metadata)
|
|
@@ -36,7 +36,7 @@ async function openDetailModal(eventId) {
|
|
|
36
36
|
const ctx = data.context || [];
|
|
37
37
|
|
|
38
38
|
const eventType = evt.eventType || 'unknown';
|
|
39
|
-
const typeClass =
|
|
39
|
+
const typeClass = eventTypeBadgeClass(eventType);
|
|
40
40
|
const time = new Date(evt.timestamp).toLocaleString();
|
|
41
41
|
const adherenceBadge = renderAdherenceBadge(evt);
|
|
42
42
|
|
|
@@ -46,8 +46,8 @@ async function openDetailModal(eventId) {
|
|
|
46
46
|
<div class="modal-section-title">Context (Surrounding Events)</div>
|
|
47
47
|
<div class="modal-context-list">
|
|
48
48
|
${ctx.map(c => `
|
|
49
|
-
<div class="modal-context-item" onclick="openDetailModal(
|
|
50
|
-
<span class="event-type-badge ${
|
|
49
|
+
<div class="modal-context-item" ${c.id ? `onclick="openDetailModal(${jsAttrArg(c.id)})"` : ''}>
|
|
50
|
+
<span class="event-type-badge ${eventTypeBadgeClass(c.eventType)}" style="flex-shrink:0;">${escapeHtml(c.eventType || 'event')}</span>
|
|
51
51
|
<div style="flex:1; min-width:0;">
|
|
52
52
|
<div style="font-size:12px; color:var(--text-muted); margin-bottom:4px;">${new Date(c.timestamp).toLocaleString()}</div>
|
|
53
53
|
<div style="font-size:13px; color:var(--text-secondary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${escapeHtml(c.preview || '')}</div>
|
|
@@ -62,7 +62,7 @@ async function openDetailModal(eventId) {
|
|
|
62
62
|
<div class="modal-meta">
|
|
63
63
|
<div class="modal-meta-item">
|
|
64
64
|
<i class="ri-price-tag-3-line"></i>
|
|
65
|
-
<span class="event-type-badge ${typeClass}">${eventType}</span>
|
|
65
|
+
<span class="event-type-badge ${typeClass}">${escapeHtml(eventType)}</span>
|
|
66
66
|
</div>
|
|
67
67
|
${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
|
|
68
68
|
<div class="modal-meta-item">
|
|
@@ -71,8 +71,13 @@ async function openDetailModal(eventId) {
|
|
|
71
71
|
</div>
|
|
72
72
|
<div class="modal-meta-item">
|
|
73
73
|
<i class="ri-chat-1-line"></i>
|
|
74
|
-
Session: ${evt.sessionId ? evt.sessionId.slice(0, 12) + '...' : 'N/A'}
|
|
74
|
+
Session: ${evt.sessionId ? escapeHtml(evt.sessionId.slice(0, 12)) + '...' : 'N/A'}
|
|
75
75
|
</div>
|
|
76
|
+
${evt.sessionId ? `
|
|
77
|
+
<button type="button" class="inline-action-btn" onclick="jumpToSession(${jsAttrArg(evt.sessionId)}, ${jsAttrArg(evt.id || eventId)})">
|
|
78
|
+
<i class="ri-corner-right-up-line"></i> Open in Sessions
|
|
79
|
+
</button>
|
|
80
|
+
` : ''}
|
|
76
81
|
</div>
|
|
77
82
|
<div class="modal-section-title">Content</div>
|
|
78
83
|
<div class="modal-content-block">${escapeHtml(evt.content || '(empty)')}</div>
|
|
@@ -111,19 +116,28 @@ async function showEventsListModal() {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
body.innerHTML = events.map(e => {
|
|
114
|
-
const typeClass =
|
|
119
|
+
const typeClass = eventTypeBadgeClass(e.eventType);
|
|
115
120
|
const adherenceBadge = renderAdherenceBadge(e);
|
|
121
|
+
const eventArg = jsAttrArg(e.id || '');
|
|
122
|
+
const sessionArg = jsAttrArg(e.sessionId || '');
|
|
116
123
|
return `
|
|
117
|
-
<div class="modal-list-item" onclick="openDetailModal(
|
|
124
|
+
<div class="modal-list-item" ${e.id ? `onclick="openDetailModal(${eventArg})"` : ''}>
|
|
118
125
|
<div class="modal-list-info">
|
|
119
126
|
<div class="title">
|
|
120
|
-
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
|
|
127
|
+
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${escapeHtml(e.eventType || 'event')}</span>
|
|
121
128
|
${adherenceBadge}
|
|
122
129
|
${escapeHtml((e.preview || '').slice(0, 80))}
|
|
123
130
|
</div>
|
|
124
|
-
<div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
|
|
131
|
+
<div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${escapeHtml((e.sessionId || '').slice(0, 12))}...</div>
|
|
132
|
+
${e.sessionId ? `
|
|
133
|
+
<div class="event-actions">
|
|
134
|
+
<button type="button" class="inline-action-btn" onclick="event.stopPropagation(); jumpToSession(${sessionArg}, ${eventArg})">
|
|
135
|
+
<i class="ri-corner-right-up-line"></i> Open in Sessions
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
` : ''}
|
|
125
139
|
</div>
|
|
126
|
-
${e.accessCount > 0 ? `<div class="modal-list-badge"><i class="ri-eye-line"></i> ${e.accessCount}</div>` : ''}
|
|
140
|
+
${e.accessCount > 0 ? `<div class="modal-list-badge"><i class="ri-eye-line"></i> ${formatNumber(e.accessCount)}</div>` : ''}
|
|
127
141
|
</div>
|
|
128
142
|
`;
|
|
129
143
|
}).join('');
|
|
@@ -152,12 +166,12 @@ async function showSessionsModal() {
|
|
|
152
166
|
const started = new Date(s.startedAt).toLocaleString();
|
|
153
167
|
const lastEvent = new Date(s.lastEventAt).toLocaleString();
|
|
154
168
|
return `
|
|
155
|
-
<div class="modal-list-item" onclick="showSessionDetailInModal(
|
|
169
|
+
<div class="modal-list-item" onclick="showSessionDetailInModal(${jsAttrArg(s.id)})">
|
|
156
170
|
<div class="modal-list-info">
|
|
157
|
-
<div class="title"><i class="ri-chat-1-line" style="color:var(--accent-primary); margin-right:6px;"></i>${s.id.slice(0, 20)}...</div>
|
|
171
|
+
<div class="title"><i class="ri-chat-1-line" style="color:var(--accent-primary); margin-right:6px;"></i>${escapeHtml(s.id.slice(0, 20))}...</div>
|
|
158
172
|
<div class="subtitle">Started: ${started} | Last: ${lastEvent}</div>
|
|
159
173
|
</div>
|
|
160
|
-
<div class="modal-list-badge">${s.eventCount} events</div>
|
|
174
|
+
<div class="modal-list-badge">${formatNumber(s.eventCount)} events</div>
|
|
161
175
|
</div>
|
|
162
176
|
`;
|
|
163
177
|
}).join('');
|
|
@@ -172,7 +186,7 @@ async function showSessionDetailInModal(sessionId) {
|
|
|
172
186
|
body.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted);">Loading session...</div>';
|
|
173
187
|
|
|
174
188
|
try {
|
|
175
|
-
const res = await fetch(apiUrl(`${API_BASE}/sessions/${sessionId}`));
|
|
189
|
+
const res = await fetch(apiUrl(`${API_BASE}/sessions/${encodeURIComponent(sessionId)}`));
|
|
176
190
|
const data = await res.json();
|
|
177
191
|
const session = data.session;
|
|
178
192
|
const events = data.events || [];
|
|
@@ -180,7 +194,7 @@ async function showSessionDetailInModal(sessionId) {
|
|
|
180
194
|
|
|
181
195
|
body.innerHTML = `
|
|
182
196
|
<div class="modal-meta">
|
|
183
|
-
<div class="modal-meta-item"><i class="ri-fingerprint-line"></i>${sessionId.slice(0, 20)}...</div>
|
|
197
|
+
<div class="modal-meta-item"><i class="ri-fingerprint-line"></i>${escapeHtml(sessionId.slice(0, 20))}...</div>
|
|
184
198
|
<div class="modal-meta-item"><i class="ri-time-line"></i>${new Date(session.startedAt).toLocaleString()}</div>
|
|
185
199
|
<div class="modal-meta-item"><i class="ri-file-list-3-line"></i>${session.eventCount} events</div>
|
|
186
200
|
</div>
|
|
@@ -197,17 +211,24 @@ async function showSessionDetailInModal(sessionId) {
|
|
|
197
211
|
</div>
|
|
198
212
|
<div class="modal-section-title">Events</div>
|
|
199
213
|
${events.map(e => {
|
|
200
|
-
const typeClass =
|
|
214
|
+
const typeClass = eventTypeBadgeClass(e.eventType);
|
|
201
215
|
const adherenceBadge = renderAdherenceBadge(e);
|
|
216
|
+
const eventArg = jsAttrArg(e.id || '');
|
|
217
|
+
const sessionArg = jsAttrArg(e.sessionId || sessionId);
|
|
202
218
|
return `
|
|
203
|
-
<div class="modal-list-item" onclick="closeAllModals(); openDetailModal(
|
|
219
|
+
<div class="modal-list-item" ${e.id ? `onclick="closeAllModals(); openDetailModal(${eventArg})"` : ''}>
|
|
204
220
|
<div class="modal-list-info">
|
|
205
221
|
<div class="title">
|
|
206
|
-
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
|
|
222
|
+
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${escapeHtml(e.eventType || 'event')}</span>
|
|
207
223
|
${adherenceBadge}
|
|
208
224
|
${escapeHtml((e.preview || '').slice(0, 80))}
|
|
209
225
|
</div>
|
|
210
226
|
<div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
|
|
227
|
+
<div class="event-actions">
|
|
228
|
+
<button type="button" class="inline-action-btn" onclick="event.stopPropagation(); jumpToSession(${sessionArg}, ${eventArg})">
|
|
229
|
+
<i class="ri-corner-right-up-line"></i> Open in Sessions
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
211
232
|
</div>
|
|
212
233
|
</div>
|
|
213
234
|
`;
|