claude-memory-layer 1.0.18 → 1.0.19
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/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +372 -74
- package/dist/cli/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +6 -0
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +6 -0
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +6 -0
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +6 -0
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +245 -31
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +329 -31
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +336 -38
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +6 -0
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +236 -4
- package/dist/ui/index.html +51 -0
- package/dist/ui/style.css +34 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +1 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +7 -0
- package/src/ui/app.js +236 -4
- package/src/ui/index.html +51 -0
- package/src/ui/style.css +34 -0
package/src/ui/app.js
CHANGED
|
@@ -12,6 +12,8 @@ const state = {
|
|
|
12
12
|
mostAccessed: null,
|
|
13
13
|
helpfulness: null,
|
|
14
14
|
retrievalTraces: null,
|
|
15
|
+
adherenceSummary: null,
|
|
16
|
+
adherenceWindow: '24h',
|
|
15
17
|
currentLevel: 'L0',
|
|
16
18
|
currentSort: 'recent',
|
|
17
19
|
currentView: 'overview',
|
|
@@ -20,6 +22,9 @@ const state = {
|
|
|
20
22
|
events: [],
|
|
21
23
|
isLoading: false,
|
|
22
24
|
chartInstance: null,
|
|
25
|
+
kpiChartInstance: null,
|
|
26
|
+
kpiWindow: '7d',
|
|
27
|
+
kpi: null,
|
|
23
28
|
chatMessages: [],
|
|
24
29
|
isChatOpen: false,
|
|
25
30
|
isChatStreaming: false,
|
|
@@ -102,13 +107,42 @@ function setupEventListeners() {
|
|
|
102
107
|
});
|
|
103
108
|
|
|
104
109
|
// Sort buttons
|
|
105
|
-
document.querySelectorAll('.sort-btn').forEach(btn => {
|
|
110
|
+
document.querySelectorAll('.sort-btn[data-sort]').forEach(btn => {
|
|
106
111
|
btn.addEventListener('click', (e) => {
|
|
107
112
|
const sort = e.currentTarget.dataset.sort;
|
|
108
113
|
if (sort) selectSort(sort);
|
|
109
114
|
});
|
|
110
115
|
});
|
|
111
116
|
|
|
117
|
+
// Adherence window controls
|
|
118
|
+
document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(btn => {
|
|
119
|
+
btn.addEventListener('click', async (e) => {
|
|
120
|
+
const window = e.currentTarget.dataset.adhWindow;
|
|
121
|
+
if (!window || state.adherenceWindow === window) return;
|
|
122
|
+
state.adherenceWindow = window;
|
|
123
|
+
document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(b => {
|
|
124
|
+
b.classList.toggle('active', b.dataset.adhWindow === window);
|
|
125
|
+
});
|
|
126
|
+
state.adherenceSummary = await fetchAdherenceSummary().catch(() => null);
|
|
127
|
+
updateAdherenceSummaryUI();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// KPI window controls
|
|
132
|
+
document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(btn => {
|
|
133
|
+
btn.addEventListener('click', async (e) => {
|
|
134
|
+
const window = e.currentTarget.dataset.kpiWindow;
|
|
135
|
+
if (!window || state.kpiWindow === window) return;
|
|
136
|
+
state.kpiWindow = window;
|
|
137
|
+
document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(b => {
|
|
138
|
+
b.classList.toggle('active', b.dataset.kpiWindow === window);
|
|
139
|
+
});
|
|
140
|
+
await loadKpiData();
|
|
141
|
+
updateKpiCardsUI();
|
|
142
|
+
renderKpiTrendChart();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
112
146
|
// Search
|
|
113
147
|
const searchInput = document.getElementById('search-input');
|
|
114
148
|
if (searchInput) {
|
|
@@ -125,6 +159,10 @@ function setupEventListeners() {
|
|
|
125
159
|
state.chartInstance.destroy();
|
|
126
160
|
state.chartInstance = null;
|
|
127
161
|
}
|
|
162
|
+
if (state.kpiChartInstance) {
|
|
163
|
+
state.kpiChartInstance.destroy();
|
|
164
|
+
state.kpiChartInstance = null;
|
|
165
|
+
}
|
|
128
166
|
await initActivityChart();
|
|
129
167
|
// Reload current view if not overview
|
|
130
168
|
if (state.currentView !== 'overview') {
|
|
@@ -231,17 +269,24 @@ function setupEventListeners() {
|
|
|
231
269
|
|
|
232
270
|
// --- Data Fetching ---
|
|
233
271
|
|
|
272
|
+
async function loadKpiData() {
|
|
273
|
+
state.kpi = await fetch(apiUrl(`${API_BASE}/stats/kpi`, { window: state.kpiWindow }))
|
|
274
|
+
.then(r => r.json())
|
|
275
|
+
.catch(() => null);
|
|
276
|
+
}
|
|
277
|
+
|
|
234
278
|
async function refreshData() {
|
|
235
279
|
const btn = document.getElementById('refresh-btn');
|
|
236
280
|
if(btn) btn.classList.add('loading');
|
|
237
281
|
|
|
238
282
|
try {
|
|
239
|
-
const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
|
|
283
|
+
const [stats, shared, mostAccessed, helpfulness, retrievalTraces, adherenceSummary] = await Promise.all([
|
|
240
284
|
fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
|
|
241
285
|
fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
|
|
242
286
|
fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
|
|
243
287
|
fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
|
|
244
|
-
fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
|
|
288
|
+
fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null),
|
|
289
|
+
fetchAdherenceSummary().catch(() => null)
|
|
245
290
|
]);
|
|
246
291
|
|
|
247
292
|
state.stats = stats;
|
|
@@ -249,10 +294,15 @@ async function refreshData() {
|
|
|
249
294
|
state.mostAccessed = mostAccessed;
|
|
250
295
|
state.helpfulness = helpfulness;
|
|
251
296
|
state.retrievalTraces = retrievalTraces;
|
|
297
|
+
state.adherenceSummary = adherenceSummary;
|
|
298
|
+
|
|
299
|
+
await loadKpiData();
|
|
252
300
|
|
|
253
301
|
updateStatsUI();
|
|
254
302
|
updateSharedUI();
|
|
255
303
|
updateMemoryUsageUI();
|
|
304
|
+
updateKpiCardsUI();
|
|
305
|
+
renderKpiTrendChart();
|
|
256
306
|
await loadLevelEvents(state.currentLevel);
|
|
257
307
|
|
|
258
308
|
checkEndlessStatus();
|
|
@@ -327,6 +377,98 @@ function updateSharedUI() {
|
|
|
327
377
|
document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
|
|
328
378
|
}
|
|
329
379
|
|
|
380
|
+
function percentText(v) {
|
|
381
|
+
return `${((v || 0) * 100).toFixed(1)}%`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function renderDelta(id, value, lowerIsBetter = false, asPercent = true) {
|
|
385
|
+
const el = document.getElementById(id);
|
|
386
|
+
if (!el) return;
|
|
387
|
+
const v = Number(value || 0);
|
|
388
|
+
const sign = v > 0 ? '+' : '';
|
|
389
|
+
const text = asPercent ? `${sign}${(v * 100).toFixed(1)}%` : `${sign}${v.toFixed(2)}`;
|
|
390
|
+
|
|
391
|
+
let positive = v > 0;
|
|
392
|
+
if (lowerIsBetter) positive = v < 0;
|
|
393
|
+
const cls = v === 0 ? 'neutral' : (positive ? 'good' : 'bad');
|
|
394
|
+
const arrow = v === 0 ? '→' : (positive ? '▲' : '▼');
|
|
395
|
+
|
|
396
|
+
el.className = `kpi-delta ${cls}`;
|
|
397
|
+
el.textContent = `${arrow} ${text} vs prev`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function updateKpiCardsUI() {
|
|
401
|
+
const m = state.kpi?.metrics;
|
|
402
|
+
const d = state.kpi?.deltas;
|
|
403
|
+
if (!m) return;
|
|
404
|
+
const set = (id, value) => {
|
|
405
|
+
const el = document.getElementById(id);
|
|
406
|
+
if (el) el.textContent = value;
|
|
407
|
+
};
|
|
408
|
+
set('kpi-useful-recall', percentText(m.usefulRecallRate));
|
|
409
|
+
set('kpi-completion-turns', Number(m.avgCompletionTurns || 0).toFixed(2));
|
|
410
|
+
set('kpi-rework-rate', percentText(m.reworkRate));
|
|
411
|
+
set('kpi-failure-rate', percentText(m.postChangeFailureRate));
|
|
412
|
+
|
|
413
|
+
if (d) {
|
|
414
|
+
renderDelta('kpi-useful-recall-delta', d.usefulRecallRate, false, true);
|
|
415
|
+
renderDelta('kpi-completion-turns-delta', d.avgCompletionTurns, true, false);
|
|
416
|
+
renderDelta('kpi-rework-rate-delta', d.reworkRate, true, true);
|
|
417
|
+
renderDelta('kpi-failure-rate-delta', d.postChangeFailureRate, true, true);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const alertsEl = document.getElementById('kpi-alerts');
|
|
421
|
+
if (alertsEl) {
|
|
422
|
+
const alerts = state.kpi?.alerts || [];
|
|
423
|
+
if (alerts.length === 0) {
|
|
424
|
+
alertsEl.innerHTML = '<span style="color:var(--success);">No KPI alerts in current window.</span>';
|
|
425
|
+
} else {
|
|
426
|
+
alertsEl.innerHTML = alerts.slice(0, 3).map(a => `⚠️ ${escapeHtml(a.message)} (${a.metric})`).join(' · ');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderKpiTrendChart() {
|
|
432
|
+
const chartEl = document.querySelector('#kpi-trend-chart');
|
|
433
|
+
if (!chartEl) return;
|
|
434
|
+
|
|
435
|
+
const daily = state.kpi?.trend?.daily || [];
|
|
436
|
+
const categories = daily.map(d => d.date);
|
|
437
|
+
const useful = daily.map(d => Number(d.usefulRecallRate || 0) * 100);
|
|
438
|
+
const rework = daily.map(d => Number(d.reworkRate || 0) * 100);
|
|
439
|
+
const fail = daily.map(d => Number(d.postChangeFailureRate || 0) * 100);
|
|
440
|
+
|
|
441
|
+
if (state.kpiChartInstance) {
|
|
442
|
+
state.kpiChartInstance.destroy();
|
|
443
|
+
state.kpiChartInstance = null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const options = {
|
|
447
|
+
series: [
|
|
448
|
+
{ name: 'Useful Recall %', data: useful },
|
|
449
|
+
{ name: 'Rework %', data: rework },
|
|
450
|
+
{ name: 'Failure %', data: fail }
|
|
451
|
+
],
|
|
452
|
+
chart: {
|
|
453
|
+
type: 'line',
|
|
454
|
+
height: 240,
|
|
455
|
+
background: 'transparent',
|
|
456
|
+
toolbar: { show: false },
|
|
457
|
+
fontFamily: 'Outfit, sans-serif'
|
|
458
|
+
},
|
|
459
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
460
|
+
dataLabels: { enabled: false },
|
|
461
|
+
xaxis: { categories, labels: { style: { colors: '#8B9BB4' } } },
|
|
462
|
+
yaxis: { labels: { formatter: (v) => `${v.toFixed(0)}%`, style: { colors: '#8B9BB4' } } },
|
|
463
|
+
theme: { mode: 'dark' },
|
|
464
|
+
grid: { borderColor: 'rgba(255,255,255,0.05)', strokeDashArray: 4 },
|
|
465
|
+
colors: ['#34D399', '#FEB019', '#FF4560']
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
state.kpiChartInstance = new ApexCharts(chartEl, options);
|
|
469
|
+
state.kpiChartInstance.render();
|
|
470
|
+
}
|
|
471
|
+
|
|
330
472
|
function selectLevel(level) {
|
|
331
473
|
state.currentLevel = level;
|
|
332
474
|
|
|
@@ -347,6 +489,23 @@ function selectSort(sort) {
|
|
|
347
489
|
loadLevelEvents(state.currentLevel, sort);
|
|
348
490
|
}
|
|
349
491
|
|
|
492
|
+
function getAdherenceInfo(event) {
|
|
493
|
+
const adherence = event?.metadata?.adherence || event?.meta?.adherence || null;
|
|
494
|
+
if (!adherence || typeof adherence !== 'object') return null;
|
|
495
|
+
const reason = adherence.reason || 'unknown';
|
|
496
|
+
const checked = Boolean(adherence.checked);
|
|
497
|
+
const turn = adherence.turn;
|
|
498
|
+
return { reason, checked, turn };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderAdherenceBadge(event) {
|
|
502
|
+
const info = getAdherenceInfo(event);
|
|
503
|
+
if (!info) return '';
|
|
504
|
+
const modeClass = info.checked ? 'adherence-checked' : 'adherence-skipped';
|
|
505
|
+
const turnText = Number.isFinite(info.turn) ? ` · T${info.turn}` : '';
|
|
506
|
+
return `<span class="adherence-badge ${modeClass}" title="adherence ${info.checked ? 'checked' : 'skipped'}${turnText}">adh:${escapeHtml(info.reason)}</span>`;
|
|
507
|
+
}
|
|
508
|
+
|
|
350
509
|
function updateEventsListUI() {
|
|
351
510
|
const container = document.getElementById('event-list-container');
|
|
352
511
|
container.innerHTML = '';
|
|
@@ -374,13 +533,17 @@ function updateEventsListUI() {
|
|
|
374
533
|
const accessBadge = event.accessCount > 0
|
|
375
534
|
? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
|
|
376
535
|
: '';
|
|
536
|
+
const adherenceBadge = renderAdherenceBadge(event);
|
|
377
537
|
const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
|
|
378
538
|
? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
|
|
379
539
|
: '';
|
|
380
540
|
|
|
381
541
|
el.innerHTML = `
|
|
382
542
|
<div class="event-header">
|
|
383
|
-
<
|
|
543
|
+
<div style="display:flex; gap:8px; align-items:center;">
|
|
544
|
+
<span class="event-type-badge ${typeClass}">${eventType}</span>
|
|
545
|
+
${adherenceBadge}
|
|
546
|
+
</div>
|
|
384
547
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
385
548
|
${accessBadge}
|
|
386
549
|
${lastUsed}
|
|
@@ -400,9 +563,72 @@ function updateMemoryUsageUI() {
|
|
|
400
563
|
updateGraduationBars();
|
|
401
564
|
updateHelpfulnessUI();
|
|
402
565
|
updateMostHelpfulList();
|
|
566
|
+
updateAdherenceSummaryUI();
|
|
403
567
|
updateRetrievalTraceUI();
|
|
404
568
|
}
|
|
405
569
|
|
|
570
|
+
function adherenceWindowToMs(window) {
|
|
571
|
+
if (window === '24h') return 24 * 60 * 60 * 1000;
|
|
572
|
+
if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
|
|
573
|
+
if (window === '30d') return 30 * 24 * 60 * 60 * 1000;
|
|
574
|
+
return 24 * 60 * 60 * 1000;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function fetchAdherenceSummary() {
|
|
578
|
+
const res = await fetch(apiUrl(`${API_BASE}/events`, { level: 'L0', limit: 500, sort: 'recent' }));
|
|
579
|
+
if (!res.ok) return null;
|
|
580
|
+
const data = await res.json();
|
|
581
|
+
const events = data.events || [];
|
|
582
|
+
|
|
583
|
+
const counts = {};
|
|
584
|
+
let checked = 0;
|
|
585
|
+
let skipped = 0;
|
|
586
|
+
let total = 0;
|
|
587
|
+
|
|
588
|
+
const now = Date.now();
|
|
589
|
+
const windowMs = adherenceWindowToMs(state.adherenceWindow);
|
|
590
|
+
|
|
591
|
+
for (const e of events) {
|
|
592
|
+
const ts = e?.timestamp ? new Date(e.timestamp).getTime() : 0;
|
|
593
|
+
if (!ts || now - ts > windowMs) continue;
|
|
594
|
+
|
|
595
|
+
const adherence = e?.metadata?.adherence || e?.meta?.adherence;
|
|
596
|
+
if (!adherence) continue;
|
|
597
|
+
total++;
|
|
598
|
+
const reason = adherence.reason || 'unknown';
|
|
599
|
+
counts[reason] = (counts[reason] || 0) + 1;
|
|
600
|
+
if (adherence.checked) checked++; else skipped++;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return { total, checked, skipped, counts, window: state.adherenceWindow };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function updateAdherenceSummaryUI() {
|
|
607
|
+
const el = document.getElementById('adherence-summary');
|
|
608
|
+
if (!el) return;
|
|
609
|
+
|
|
610
|
+
const s = state.adherenceSummary;
|
|
611
|
+
if (!s || !s.total) {
|
|
612
|
+
el.innerHTML = '<span style="color:var(--text-muted);">No adherence metadata yet.</span>';
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const top = Object.entries(s.counts || {})
|
|
617
|
+
.sort((a, b) => b[1] - a[1])
|
|
618
|
+
.slice(0, 5)
|
|
619
|
+
.map(([reason, count]) => `<span class="adherence-badge adherence-checked" style="margin-right:6px;">${escapeHtml(reason)}: ${count}</span>`)
|
|
620
|
+
.join('');
|
|
621
|
+
|
|
622
|
+
el.innerHTML = `
|
|
623
|
+
<div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:8px;">
|
|
624
|
+
<span><strong>${s.total}</strong> tagged prompts (${escapeHtml(s.window || state.adherenceWindow)})</span>
|
|
625
|
+
<span style="color:var(--success);"><strong>${s.checked}</strong> checked</span>
|
|
626
|
+
<span style="color:var(--text-muted);"><strong>${s.skipped}</strong> skipped</span>
|
|
627
|
+
</div>
|
|
628
|
+
<div>${top}</div>
|
|
629
|
+
`;
|
|
630
|
+
}
|
|
631
|
+
|
|
406
632
|
function updateGraduationBars() {
|
|
407
633
|
const container = document.getElementById('graduation-bars');
|
|
408
634
|
if (!container || !state.stats?.levelStats) return;
|
|
@@ -691,6 +917,7 @@ async function openDetailModal(eventId) {
|
|
|
691
917
|
const eventType = evt.eventType || 'unknown';
|
|
692
918
|
const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
|
|
693
919
|
const time = new Date(evt.timestamp).toLocaleString();
|
|
920
|
+
const adherenceBadge = renderAdherenceBadge(evt);
|
|
694
921
|
|
|
695
922
|
let contextHtml = '';
|
|
696
923
|
if (ctx.length > 0) {
|
|
@@ -716,6 +943,7 @@ async function openDetailModal(eventId) {
|
|
|
716
943
|
<i class="ri-price-tag-3-line"></i>
|
|
717
944
|
<span class="event-type-badge ${typeClass}">${eventType}</span>
|
|
718
945
|
</div>
|
|
946
|
+
${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
|
|
719
947
|
<div class="modal-meta-item">
|
|
720
948
|
<i class="ri-time-line"></i>
|
|
721
949
|
${time}
|
|
@@ -763,11 +991,13 @@ async function showEventsListModal() {
|
|
|
763
991
|
|
|
764
992
|
body.innerHTML = events.map(e => {
|
|
765
993
|
const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
|
|
994
|
+
const adherenceBadge = renderAdherenceBadge(e);
|
|
766
995
|
return `
|
|
767
996
|
<div class="modal-list-item" onclick="openDetailModal('${e.id}')">
|
|
768
997
|
<div class="modal-list-info">
|
|
769
998
|
<div class="title">
|
|
770
999
|
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
|
|
1000
|
+
${adherenceBadge}
|
|
771
1001
|
${escapeHtml((e.preview || '').slice(0, 80))}
|
|
772
1002
|
</div>
|
|
773
1003
|
<div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
|
|
@@ -847,11 +1077,13 @@ async function showSessionDetailInModal(sessionId) {
|
|
|
847
1077
|
<div class="modal-section-title">Events</div>
|
|
848
1078
|
${events.map(e => {
|
|
849
1079
|
const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
|
|
1080
|
+
const adherenceBadge = renderAdherenceBadge(e);
|
|
850
1081
|
return `
|
|
851
1082
|
<div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
|
|
852
1083
|
<div class="modal-list-info">
|
|
853
1084
|
<div class="title">
|
|
854
1085
|
<span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
|
|
1086
|
+
${adherenceBadge}
|
|
855
1087
|
${escapeHtml((e.preview || '').slice(0, 80))}
|
|
856
1088
|
</div>
|
|
857
1089
|
<div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
|
package/src/ui/index.html
CHANGED
|
@@ -120,6 +120,45 @@
|
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
122
122
|
|
|
123
|
+
<!-- KPI Cards -->
|
|
124
|
+
<div class="stats-grid kpi-grid" style="margin-top:-4px;">
|
|
125
|
+
<div class="stat-card kpi-card">
|
|
126
|
+
<div class="stat-value" id="kpi-useful-recall">-</div>
|
|
127
|
+
<div class="kpi-delta" id="kpi-useful-recall-delta">-</div>
|
|
128
|
+
<div class="stat-label"><i class="ri-thumb-up-line"></i> Useful Recall Rate</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="stat-card kpi-card">
|
|
131
|
+
<div class="stat-value" id="kpi-completion-turns">-</div>
|
|
132
|
+
<div class="kpi-delta" id="kpi-completion-turns-delta">-</div>
|
|
133
|
+
<div class="stat-label"><i class="ri-repeat-line"></i> Avg Completion Turns</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="stat-card kpi-card">
|
|
136
|
+
<div class="stat-value" id="kpi-rework-rate">-</div>
|
|
137
|
+
<div class="kpi-delta" id="kpi-rework-rate-delta">-</div>
|
|
138
|
+
<div class="stat-label"><i class="ri-hammer-line"></i> Rework Rate</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="stat-card kpi-card">
|
|
141
|
+
<div class="stat-value" id="kpi-failure-rate">-</div>
|
|
142
|
+
<div class="kpi-delta" id="kpi-failure-rate-delta">-</div>
|
|
143
|
+
<div class="stat-label"><i class="ri-error-warning-line"></i> Post-change Failure</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="card" style="margin-bottom:24px;">
|
|
148
|
+
<div class="card-header">
|
|
149
|
+
<div class="card-title">
|
|
150
|
+
<i class="ri-line-chart-line"></i>
|
|
151
|
+
<span>KPI Trend</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div style="display:flex; gap:8px;">
|
|
154
|
+
<button class="sort-btn active" data-kpi-window="7d">7d</button>
|
|
155
|
+
<button class="sort-btn" data-kpi-window="30d">30d</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div id="kpi-alerts" style="font-size:12px; color:var(--text-muted); margin-bottom:10px;">Loading...</div>
|
|
159
|
+
<div id="kpi-trend-chart" style="height:240px;"></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
123
162
|
<!-- Main Grid -->
|
|
124
163
|
<div class="dashboard-grid">
|
|
125
164
|
|
|
@@ -272,6 +311,18 @@
|
|
|
272
311
|
</div>
|
|
273
312
|
</div>
|
|
274
313
|
|
|
314
|
+
<div style="margin-top:20px;">
|
|
315
|
+
<div class="section-label" style="display:flex; justify-content:space-between; align-items:center;">
|
|
316
|
+
<span>Adherence Reasons</span>
|
|
317
|
+
<span id="adherence-window-controls" style="display:flex; gap:6px;">
|
|
318
|
+
<button class="sort-btn active" data-adh-window="24h">24h</button>
|
|
319
|
+
<button class="sort-btn" data-adh-window="7d">7d</button>
|
|
320
|
+
<button class="sort-btn" data-adh-window="30d">30d</button>
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
<div id="adherence-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
275
326
|
<div style="margin-top:20px;">
|
|
276
327
|
<div class="section-label">Retrieval Trace (1:1)</div>
|
|
277
328
|
<div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
|
package/src/ui/style.css
CHANGED
|
@@ -329,6 +329,23 @@ body {
|
|
|
329
329
|
opacity: 0.5;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
.kpi-grid .stat-card .stat-value {
|
|
333
|
+
font-size: 28px;
|
|
334
|
+
}
|
|
335
|
+
.kpi-grid .stat-card .stat-label {
|
|
336
|
+
font-size: 12px;
|
|
337
|
+
}
|
|
338
|
+
.kpi-delta {
|
|
339
|
+
font-size: 11px;
|
|
340
|
+
margin-top: -4px;
|
|
341
|
+
margin-bottom: 8px;
|
|
342
|
+
color: var(--text-muted);
|
|
343
|
+
font-weight: 600;
|
|
344
|
+
}
|
|
345
|
+
.kpi-delta.good { color: var(--success); }
|
|
346
|
+
.kpi-delta.bad { color: var(--error); }
|
|
347
|
+
.kpi-delta.neutral { color: var(--text-muted); }
|
|
348
|
+
|
|
332
349
|
.stat-value {
|
|
333
350
|
font-size: 36px;
|
|
334
351
|
font-weight: 700;
|
|
@@ -594,6 +611,23 @@ body {
|
|
|
594
611
|
font-weight: 600;
|
|
595
612
|
}
|
|
596
613
|
|
|
614
|
+
.adherence-badge {
|
|
615
|
+
font-size: 10px;
|
|
616
|
+
padding: 3px 7px;
|
|
617
|
+
border-radius: 6px;
|
|
618
|
+
font-weight: 700;
|
|
619
|
+
text-transform: uppercase;
|
|
620
|
+
letter-spacing: 0.2px;
|
|
621
|
+
}
|
|
622
|
+
.adherence-checked {
|
|
623
|
+
background: rgba(52, 211, 153, 0.14);
|
|
624
|
+
color: #34D399;
|
|
625
|
+
}
|
|
626
|
+
.adherence-skipped {
|
|
627
|
+
background: rgba(148, 163, 184, 0.14);
|
|
628
|
+
color: #94A3B8;
|
|
629
|
+
}
|
|
630
|
+
|
|
597
631
|
/* Section Label */
|
|
598
632
|
.section-label {
|
|
599
633
|
font-size: 12px;
|