@asifkibria/claude-code-toolkit 1.2.0 → 1.3.0
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/README.md +126 -113
- package/dist/cli.js +365 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/alerts.d.ts +38 -0
- package/dist/lib/alerts.d.ts.map +1 -0
- package/dist/lib/alerts.js +296 -0
- package/dist/lib/alerts.js.map +1 -0
- package/dist/lib/dashboard-ui.d.ts.map +1 -1
- package/dist/lib/dashboard-ui.js +215 -23
- package/dist/lib/dashboard-ui.js.map +1 -1
- package/dist/lib/dashboard.d.ts +2 -1
- package/dist/lib/dashboard.d.ts.map +1 -1
- package/dist/lib/dashboard.js +159 -10
- package/dist/lib/dashboard.js.map +1 -1
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +124 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/mcp-validator.d.ts +23 -0
- package/dist/lib/mcp-validator.d.ts.map +1 -1
- package/dist/lib/mcp-validator.js +138 -0
- package/dist/lib/mcp-validator.js.map +1 -1
- package/dist/lib/scanner.d.ts.map +1 -1
- package/dist/lib/scanner.js +103 -5
- package/dist/lib/scanner.js.map +1 -1
- package/dist/lib/search.d.ts +56 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +284 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/security.d.ts +39 -0
- package/dist/lib/security.d.ts.map +1 -1
- package/dist/lib/security.js +379 -7
- package/dist/lib/security.js.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +5 -2
- package/dist/lib/storage.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/dashboard-ui.js
CHANGED
|
@@ -326,8 +326,10 @@ table tbody tr:hover td { background: var(--accent-glow); }
|
|
|
326
326
|
<div class="hstat" style="text-align:center" title="Issues found"><div style="font-size:16px;font-weight:700" id="hsIssues">-</div><div style="font-size:9px;color:var(--text3);text-transform:uppercase">Issues</div></div>
|
|
327
327
|
</div>
|
|
328
328
|
<div class="header-right">
|
|
329
|
-
<div class="search-bar" style="margin:0
|
|
330
|
-
<
|
|
329
|
+
<div class="search-bar" style="margin:0 16px 0 0;position:relative;display:flex;align-items:center;background:var(--card);border:2px solid var(--accent);border-radius:var(--radius);padding:2px">
|
|
330
|
+
<span style="padding:0 8px;color:var(--accent);font-size:16px">🔍</span>
|
|
331
|
+
<input type="text" id="globalSearch" class="search-input" placeholder="Search all conversations..." style="width:280px;padding:8px 12px;font-size:13px;border:none;background:transparent" onkeyup="if(event.key==='Enter') doGlobalSearch(this.value)">
|
|
332
|
+
<button class="btn" style="margin:2px;padding:6px 12px;font-size:12px" onclick="doGlobalSearch(document.getElementById('globalSearch').value)">Search</button>
|
|
331
333
|
</div>
|
|
332
334
|
<div class="auto-refresh">
|
|
333
335
|
<label>Auto-refresh</label>
|
|
@@ -344,6 +346,7 @@ table tbody tr:hover td { background: var(--accent-glow); }
|
|
|
344
346
|
</div>
|
|
345
347
|
<div class="nav" id="nav">
|
|
346
348
|
<div class="nav-item active" data-tab="overview">Overview</div>
|
|
349
|
+
<div class="nav-item" data-tab="search" style="background:var(--accent);color:white;font-weight:600">🔍 Search</div>
|
|
347
350
|
<div class="nav-item" data-tab="storage">Storage</div>
|
|
348
351
|
<div class="nav-item" data-tab="sessions">Sessions</div>
|
|
349
352
|
<div class="nav-item" data-tab="security">Security</div>
|
|
@@ -465,14 +468,55 @@ function updateNavBadges(data) {
|
|
|
465
468
|
addBadge('traces',data.criticalTraces||0,'nav-badge-red');
|
|
466
469
|
}
|
|
467
470
|
|
|
471
|
+
const TOKEN_STORAGE_KEY = 'cct_dashboard_token';
|
|
472
|
+
|
|
473
|
+
function getStoredToken() {
|
|
474
|
+
try { return localStorage.getItem(TOKEN_STORAGE_KEY) || ''; } catch { return ''; }
|
|
475
|
+
}
|
|
476
|
+
function setStoredToken(value) {
|
|
477
|
+
try {
|
|
478
|
+
if (value) localStorage.setItem(TOKEN_STORAGE_KEY, value);
|
|
479
|
+
else localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
480
|
+
} catch { /* ignore */ }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function promptForToken(message) {
|
|
484
|
+
const token = window.prompt(message || 'Enter dashboard access token');
|
|
485
|
+
if (token && token.trim()) {
|
|
486
|
+
setStoredToken(token.trim());
|
|
487
|
+
return token.trim();
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function fetchWithAuth(path, options, retry = true) {
|
|
493
|
+
const headers = Object.assign({}, options?.headers || {});
|
|
494
|
+
const token = getStoredToken();
|
|
495
|
+
if (token) headers['Authorization'] = 'Bearer ' + token;
|
|
496
|
+
const response = await fetch(path, { ...(options || {}), headers });
|
|
497
|
+
if (response.status === 401 && retry) {
|
|
498
|
+
const provided = await promptForToken('Dashboard token required');
|
|
499
|
+
if (!provided) throw new Error('Unauthorized');
|
|
500
|
+
headers['Authorization'] = 'Bearer ' + provided;
|
|
501
|
+
return fetchWithAuth(path, { ...(options || {}), headers }, false);
|
|
502
|
+
}
|
|
503
|
+
return response;
|
|
504
|
+
}
|
|
505
|
+
|
|
468
506
|
const cache = {};
|
|
469
507
|
async function api(ep) {
|
|
470
|
-
try {
|
|
508
|
+
try {
|
|
509
|
+
const r = await fetchWithAuth('/api/'+ep);
|
|
510
|
+
if(!r.ok) throw new Error(r.statusText);
|
|
511
|
+
const d = await r.json();
|
|
512
|
+
cache[ep]=d;
|
|
513
|
+
return d;
|
|
514
|
+
}
|
|
471
515
|
catch(e) { console.error('API:',ep,e); return cache[ep]||null; }
|
|
472
516
|
}
|
|
473
517
|
async function post(ep, body) {
|
|
474
518
|
try {
|
|
475
|
-
const r = await
|
|
519
|
+
const r = await fetchWithAuth('/api/action/'+ep, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body||{}) });
|
|
476
520
|
if (!r.ok) { console.error('API error:', r.status, r.statusText); return { success: false, error: 'HTTP ' + r.status }; }
|
|
477
521
|
return r.json();
|
|
478
522
|
} catch (e) { console.error('Post error:', e); return { success: false, error: e.message }; }
|
|
@@ -689,7 +733,7 @@ function toggleTheme() {
|
|
|
689
733
|
})();
|
|
690
734
|
|
|
691
735
|
let currentTab = 'overview';
|
|
692
|
-
const tabOrder=['overview','storage','sessions','security','traces','mcp','logs','config','analytics','backups','context','maintenance','snapshots','about'];
|
|
736
|
+
const tabOrder=['overview','search','storage','sessions','security','traces','mcp','logs','config','analytics','backups','context','maintenance','snapshots','about'];
|
|
693
737
|
$('#nav').addEventListener('click', e => {
|
|
694
738
|
const t = e.target.dataset?.tab; if(!t) return;
|
|
695
739
|
$$('.nav-item').forEach(n=>n.classList.remove('active'));
|
|
@@ -728,29 +772,82 @@ async function doGlobalSearch(q) {
|
|
|
728
772
|
loadSearch();
|
|
729
773
|
}
|
|
730
774
|
|
|
731
|
-
async function
|
|
775
|
+
async function loadSearchTab() {
|
|
732
776
|
const el = $('#sec-search');
|
|
733
|
-
|
|
777
|
+
let h = '<h2>🔍 Search All Conversations</h2>';
|
|
778
|
+
h += '<p style="color:var(--text2);margin-bottom:20px">Search across all your Claude Code conversations to find specific discussions, code snippets, errors, or any text.</p>';
|
|
779
|
+
h += '<div style="display:flex;gap:12px;margin-bottom:24px;flex-wrap:wrap;align-items:center">';
|
|
780
|
+
h += '<input type="text" id="searchTabInput" class="search-input" placeholder="Enter search term (e.g., API key, function name, error message...)" style="flex:1;min-width:300px;padding:12px 16px;font-size:14px" value="'+esc(currentSearchQuery)+'" onkeyup="if(event.key===\\'Enter\\') doSearchFromTab()">';
|
|
781
|
+
h += '<select id="searchRole" style="padding:12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)"><option value="">All roles</option><option value="user">User only</option><option value="assistant">Assistant only</option></select>';
|
|
782
|
+
h += '<input type="number" id="searchLimit" placeholder="Limit" value="50" min="10" max="500" style="width:80px;padding:12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)">';
|
|
783
|
+
h += '<button class="btn btn-primary" style="padding:12px 24px;font-size:14px" onclick="doSearchFromTab()">🔍 Search</button>';
|
|
784
|
+
h += '</div>';
|
|
785
|
+
h += '<div id="searchResults">';
|
|
786
|
+
if (currentSearchQuery) {
|
|
787
|
+
h += '<div class="loading"><div class="spinner"></div><div>Searching...</div></div>';
|
|
788
|
+
} else {
|
|
789
|
+
h += '<div style="text-align:center;padding:60px;color:var(--text3)">';
|
|
790
|
+
h += '<div style="font-size:64px;margin-bottom:16px">🔍</div>';
|
|
791
|
+
h += '<div style="font-size:18px;font-weight:600;margin-bottom:8px">Start Searching</div>';
|
|
792
|
+
h += '<div>Enter a search term above to find content across all your conversations</div>';
|
|
793
|
+
h += '</div>';
|
|
794
|
+
}
|
|
795
|
+
h += '</div>';
|
|
796
|
+
set(el, h);
|
|
797
|
+
if (currentSearchQuery) { setTimeout(() => doSearchFromTab(), 100); }
|
|
798
|
+
}
|
|
799
|
+
async function doSearchFromTab() {
|
|
800
|
+
const q = $('#searchTabInput')?.value || '';
|
|
801
|
+
const role = $('#searchRole')?.value || '';
|
|
802
|
+
const limit = $('#searchLimit')?.value || '50';
|
|
803
|
+
if (!q || q.length < 2) { toast('Enter at least 2 characters', 'error'); return; }
|
|
804
|
+
currentSearchQuery = q;
|
|
805
|
+
const resultsEl = $('#searchResults');
|
|
806
|
+
if (resultsEl) resultsEl.innerHTML = '<div class="loading"><div class="spinner"></div><div>Searching for "'+esc(q)+'"...</div></div>';
|
|
734
807
|
try {
|
|
735
|
-
|
|
736
|
-
if(
|
|
737
|
-
|
|
738
|
-
|
|
808
|
+
let url = 'search?q='+encodeURIComponent(q)+'&limit='+limit;
|
|
809
|
+
if (role) url += '&role='+role;
|
|
810
|
+
const res = await api(url);
|
|
811
|
+
if (!res || !res.results || res.results.length === 0) {
|
|
812
|
+
resultsEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text3)"><div style="font-size:48px;margin-bottom:12px">😕</div><div style="font-size:16px">No results found for "'+esc(q)+'"</div><div style="margin-top:8px">Try different keywords or check spelling</div></div>';
|
|
813
|
+
return;
|
|
739
814
|
}
|
|
740
|
-
let h = '<
|
|
741
|
-
h += '<
|
|
815
|
+
let h = '<div style="margin-bottom:16px;padding:12px;background:var(--card);border-radius:var(--radius-sm);border-left:4px solid var(--accent)">';
|
|
816
|
+
h += '<strong>'+res.results.length+' results</strong> found for "<em>'+esc(q)+'</em>"';
|
|
817
|
+
h += '</div>';
|
|
818
|
+
const grouped = {};
|
|
742
819
|
res.results.forEach(r => {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
820
|
+
const projectMatch = r.file.match(/projects\\/([^\\/]+)/);
|
|
821
|
+
const project = projectMatch ? projectMatch[1].replace(/-/g, '/') : 'Unknown';
|
|
822
|
+
if (!grouped[project]) grouped[project] = [];
|
|
823
|
+
grouped[project].push(r);
|
|
824
|
+
});
|
|
825
|
+
Object.entries(grouped).forEach(([project, results]) => {
|
|
826
|
+
h += '<div class="card" style="margin-bottom:16px;padding:16px">';
|
|
827
|
+
h += '<div style="font-weight:700;font-size:14px;color:var(--accent);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)">📁 '+esc(project)+' <span style="font-weight:400;color:var(--text3)">('+results.length+' matches)</span></div>';
|
|
828
|
+
results.forEach(r => {
|
|
829
|
+
const fileName = r.file.split('/').pop();
|
|
830
|
+
const roleIcon = r.role === 'user' ? '👤' : r.role === 'assistant' ? '🤖' : '📝';
|
|
831
|
+
h += '<div style="margin-bottom:12px;padding:10px;background:var(--bg);border-radius:var(--radius-sm);border-left:3px solid '+(r.role==='user'?'var(--yellow)':'var(--green)' )+'">';
|
|
832
|
+
h += '<div style="font-size:11px;color:var(--text3);margin-bottom:6px">'+roleIcon+' <strong>'+esc(r.role||'unknown')+'</strong> • '+esc(fileName)+' • Line '+r.line+'</div>';
|
|
833
|
+
h += '<div style="font-family:var(--mono);font-size:12px;color:var(--text);white-space:pre-wrap;word-break:break-word">'+highlightMatch(esc(r.preview), q)+'</div>';
|
|
834
|
+
h += '</div>';
|
|
835
|
+
});
|
|
746
836
|
h += '</div>';
|
|
747
837
|
});
|
|
748
|
-
|
|
749
|
-
set(el, h);
|
|
838
|
+
resultsEl.innerHTML = h;
|
|
750
839
|
} catch(e) {
|
|
751
|
-
|
|
840
|
+
resultsEl.innerHTML = '<div style="color:var(--red);padding:20px">Error: '+e.message+'</div>';
|
|
752
841
|
}
|
|
753
842
|
}
|
|
843
|
+
function highlightMatch(text, query) {
|
|
844
|
+
if (!query) return text;
|
|
845
|
+
const parts = text.split(new RegExp('(' + query + ')', 'i'));
|
|
846
|
+
return parts.map(p => p.toLowerCase() === query.toLowerCase() ? '<mark style="background:var(--yellow);color:var(--bg);padding:1px 3px;border-radius:2px">' + p + '</mark>' : p).join('');
|
|
847
|
+
}
|
|
848
|
+
async function loadSearch() {
|
|
849
|
+
loadSearchTab();
|
|
850
|
+
}
|
|
754
851
|
|
|
755
852
|
function healthRing(pct, color) {
|
|
756
853
|
const r=47, c=2*Math.PI*r, off=c-(pct/100)*c;
|
|
@@ -920,7 +1017,7 @@ async function loadSessions() {
|
|
|
920
1017
|
}
|
|
921
1018
|
|
|
922
1019
|
async function loadSecurity() {
|
|
923
|
-
const [d, comp] = await Promise.all([api('security'), api('compliance')]);
|
|
1020
|
+
const [d, comp, pii] = await Promise.all([api('security'), api('compliance'), api('pii')]);
|
|
924
1021
|
const el = $('#sec-security');
|
|
925
1022
|
if (!d) { set(el, emptyState('🔒', 'Security scan unavailable', 'Could not scan for secrets.', 'Retry', 'refreshCurrent()')); return; }
|
|
926
1023
|
let h = '<h2>Security Scan</h2>';
|
|
@@ -975,6 +1072,46 @@ async function loadSecurity() {
|
|
|
975
1072
|
}
|
|
976
1073
|
h += '</div>';
|
|
977
1074
|
}
|
|
1075
|
+
if (pii) {
|
|
1076
|
+
h += '<div class="card mt"><h3>👤 PII Scan (Personal Identifiable Information)</h3>';
|
|
1077
|
+
h += '<p style="color:var(--text3);font-size:12px;margin:8px 0">Showing exact values and full file paths for all detected PII.</p>';
|
|
1078
|
+
if (pii.totalFindings > 0) {
|
|
1079
|
+
h += '<div class="action-bar" style="margin-top:12px">';
|
|
1080
|
+
h += '<button class="btn btn-danger" onclick="doRedactAllPII()">Redact All PII (' + pii.totalFindings + ')</button>';
|
|
1081
|
+
h += '</div>';
|
|
1082
|
+
}
|
|
1083
|
+
h += '<div class="grid" style="margin-top:12px">';
|
|
1084
|
+
h += '<div class="card"><div class="stat-icon">🔍</div><h4>Files Scanned</h4><div class="value">' + pii.filesScanned + '</div></div>';
|
|
1085
|
+
h += '<div class="card"><div class="stat-icon">🚨</div><h4>Total Findings</h4><div class="value ' + (pii.totalFindings > 0 ? 'c-orange' : 'c-green') + '">' + pii.totalFindings + '</div></div>';
|
|
1086
|
+
const highCount = pii.bySensitivity?.high || 0;
|
|
1087
|
+
const medCount = pii.bySensitivity?.medium || 0;
|
|
1088
|
+
const lowCount = pii.bySensitivity?.low || 0;
|
|
1089
|
+
h += '<div class="card"><div class="stat-icon">🔴</div><h4>High Severity</h4><div class="value ' + (highCount > 0 ? 'c-red' : 'c-green') + '">' + highCount + '</div></div>';
|
|
1090
|
+
h += '<div class="card"><div class="stat-icon">🟡</div><h4>Medium</h4><div class="value ' + (medCount > 0 ? 'c-orange' : 'c-green') + '">' + medCount + '</div></div>';
|
|
1091
|
+
h += '</div>';
|
|
1092
|
+
if (pii.byCategory && Object.keys(pii.byCategory).length > 0) {
|
|
1093
|
+
h += '<div style="margin-top:16px"><h4 style="font-size:13px;font-weight:600;margin-bottom:10px">By Category</h4>';
|
|
1094
|
+
h += '<table><tr><th>Category</th><th>Count</th></tr>';
|
|
1095
|
+
Object.entries(pii.byCategory).forEach(([cat, cnt]) => { h += '<tr><td>' + esc(cat) + '</td><td>' + cnt + '</td></tr>'; });
|
|
1096
|
+
h += '</table></div>';
|
|
1097
|
+
}
|
|
1098
|
+
if (pii.findings?.length) {
|
|
1099
|
+
const showing = pii.findings.length;
|
|
1100
|
+
const total = pii.totalFindings;
|
|
1101
|
+
h += '<div style="margin-top:16px"><h4 style="font-size:13px;font-weight:600;margin-bottom:10px">Findings (showing ' + showing + ' of ' + total + ')</h4>';
|
|
1102
|
+
h += '<div id="pii-findings-container">';
|
|
1103
|
+
h += renderPIITable(pii.findings, true);
|
|
1104
|
+
h += '</div>';
|
|
1105
|
+
if (pii.hasMore) {
|
|
1106
|
+
h += '<div style="margin-top:12px;display:flex;gap:8px">';
|
|
1107
|
+
h += '<button class="btn" onclick="loadMorePII(100)">Load 100 More</button>';
|
|
1108
|
+
h += '<button class="btn" onclick="loadMorePII(' + total + ')">Load All (' + total + ')</button>';
|
|
1109
|
+
h += '</div>';
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
h += '</div>';
|
|
1113
|
+
window._piiState = { offset: pii.findings?.length || 0, total: pii.totalFindings };
|
|
1114
|
+
}
|
|
978
1115
|
set(el, h); staggerCards(el); refreshTime();
|
|
979
1116
|
}
|
|
980
1117
|
|
|
@@ -1762,6 +1899,61 @@ async function doRedactAll() {
|
|
|
1762
1899
|
else { showResult(false, 'Redaction Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
|
|
1763
1900
|
});
|
|
1764
1901
|
}
|
|
1902
|
+
async function doRedactPII(file, line, piiType) {
|
|
1903
|
+
showModal('Redact PII', 'This will replace the PII on line ' + line + ' with [PII_REDACTED]. A backup of the file will be created first.', 'Redact', async () => {
|
|
1904
|
+
showProgress('Redacting PII...');
|
|
1905
|
+
const r = await post('redact-pii', { file: file, line: line, type: piiType });
|
|
1906
|
+
if (r.success) { showResult(true, 'PII Redacted', '<span>Items redacted: <strong>' + r.redactedCount + '</strong></span><span>Backup: ' + esc(r.backupPath?.split('/').pop() || 'created') + '</span>', ['security', 'overview'], r.backupPath ? [r.backupPath] : null); }
|
|
1907
|
+
else { showResult(false, 'Redaction Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
async function doRedactAllPII() {
|
|
1911
|
+
showModal('Redact ALL PII', 'This will redact ALL detected personal identifiable information (PII) across ALL conversation files. Backups will be created for each modified file. This action cannot be undone (except by restoring backups).', 'Redact All PII', async () => {
|
|
1912
|
+
showProgress('Redacting all PII...');
|
|
1913
|
+
const r = await post('redact-all-pii');
|
|
1914
|
+
if (r.success) { showResult(true, 'All PII Redacted', '<span>Files modified: <strong>' + r.filesModified + '</strong></span><span>PII items redacted: <strong>' + r.piiRedacted + '</strong></span>' + (r.errors?.length ? '<span class="c-orange">Errors: ' + r.errors.length + '</span>' : ''), ['security', 'overview', 'backups'], r.items); }
|
|
1915
|
+
else { showResult(false, 'Redaction Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
function renderPIITable(findings, showDetails) {
|
|
1919
|
+
let h = '<div style="overflow-x:auto"><table style="width:100%;min-width:900px">';
|
|
1920
|
+
h += '<tr><th style="width:80px">Severity</th><th style="width:100px">Type</th><th style="width:200px">Exact Value</th><th>Full Project Path</th><th style="width:320px">File Location</th><th style="width:80px">Actions</th></tr>';
|
|
1921
|
+
findings.forEach(f => {
|
|
1922
|
+
const sevBadge = f.sensitivity === 'high' ? badge('high', 'red') : f.sensitivity === 'medium' ? badge('medium', 'orange') : badge('low', 'green');
|
|
1923
|
+
const fullFilePath = f.file || '';
|
|
1924
|
+
const fileName = fullFilePath.split('/').pop() || '';
|
|
1925
|
+
const projectMatch = fullFilePath.match(/projects\\/([^\\/]+)/);
|
|
1926
|
+
const projectEncoded = projectMatch ? projectMatch[1] : '';
|
|
1927
|
+
const projectPath = projectEncoded.replace(/-/g, '/');
|
|
1928
|
+
const exactValue = f.fullValue || f.maskedValue || '';
|
|
1929
|
+
const fp = esc(f.file);
|
|
1930
|
+
h += '<tr>';
|
|
1931
|
+
h += '<td>' + sevBadge + '</td>';
|
|
1932
|
+
h += '<td style="font-size:12px">' + esc(f.type) + '</td>';
|
|
1933
|
+
h += '<td class="mono" style="color:var(--red);font-weight:600;word-break:break-all">' + esc(exactValue) + '</td>';
|
|
1934
|
+
h += '<td class="mono" style="word-break:break-all;font-size:11px;color:var(--text2)">' + esc(projectPath) + '</td>';
|
|
1935
|
+
h += '<td class="mono" style="font-size:11px"><div style="word-break:break-all;color:var(--accent)">' + esc(fileName) + '</div><div style="color:var(--text3)">Line <strong>' + f.line + '</strong> • <span style="font-size:10px;opacity:0.7">' + esc(fullFilePath) + '</span></div></td>';
|
|
1936
|
+
h += '<td><button class="btn btn-sm btn-danger" onclick="doRedactPII(\\''+fp+'\\',' + f.line + ',\\''+esc(f.type||'')+'\\')">Redact</button></td>';
|
|
1937
|
+
h += '</tr>';
|
|
1938
|
+
});
|
|
1939
|
+
h += '</table></div>';
|
|
1940
|
+
return h;
|
|
1941
|
+
}
|
|
1942
|
+
async function loadMorePII(count) {
|
|
1943
|
+
if (!window._piiState) return;
|
|
1944
|
+
showProgress('Loading more PII findings...');
|
|
1945
|
+
const pii = await api('pii?limit=' + count + '&offset=0');
|
|
1946
|
+
hideProgress();
|
|
1947
|
+
if (pii) {
|
|
1948
|
+
window._piiState.offset = pii.findings.length;
|
|
1949
|
+
const container = $('#pii-findings-container');
|
|
1950
|
+
if (container) {
|
|
1951
|
+
container.innerHTML = renderPIITable(pii.findings, true);
|
|
1952
|
+
const header = container.previousElementSibling;
|
|
1953
|
+
if (header) header.innerHTML = 'Findings (showing ' + pii.findings.length + ' of ' + pii.totalFindings + ')';
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1765
1957
|
async function doTestMcp() {
|
|
1766
1958
|
showProgress('Testing MCP servers...');
|
|
1767
1959
|
const r = await post('test-mcp');
|
|
@@ -1993,7 +2185,7 @@ async function doTakeSnapshot() {
|
|
|
1993
2185
|
showModal('Take Snapshot', 'Label for this snapshot:', 'Create', async () => {
|
|
1994
2186
|
const label = $('#snapshotLabelInput')?.value || 'Manual Snapshot';
|
|
1995
2187
|
showProgress('Taking snapshot...');
|
|
1996
|
-
const r = await post('
|
|
2188
|
+
const r = await post('snapshot', { label });
|
|
1997
2189
|
hideProgress();
|
|
1998
2190
|
if(r.success) { toast('Snapshot created', 'success'); loadSnapshots(); }
|
|
1999
2191
|
else { toast('Failed: '+r.error, 'error'); }
|
|
@@ -2008,7 +2200,7 @@ async function doTakeSnapshot() {
|
|
|
2008
2200
|
async function doDeleteSnapshot(id) {
|
|
2009
2201
|
if(!confirm('Delete this snapshot?')) return;
|
|
2010
2202
|
showProgress('Deleting...');
|
|
2011
|
-
const r = await post('
|
|
2203
|
+
const r = await post('delete-snapshot', { id });
|
|
2012
2204
|
hideProgress();
|
|
2013
2205
|
if(r.success) { toast('Deleted', 'success'); loadSnapshots(); }
|
|
2014
2206
|
else { toast('Failed: '+r.error, 'error'); }
|
|
@@ -2066,7 +2258,7 @@ async function doCompareSnapshots(id1, id2) {
|
|
|
2066
2258
|
showModal('Snapshot Comparison', html, 'Close', null, true);
|
|
2067
2259
|
}
|
|
2068
2260
|
|
|
2069
|
-
const loaders = { overview: loadOverview, storage: loadStorage, sessions: loadSessions, security: loadSecurity, traces: loadTraces, mcp: loadMcp, logs: loadLogs, config: loadConfig, analytics: loadAnalytics, backups: loadBackups, context: loadContext, maintenance: loadMaintenance, snapshots: loadSnapshots, about: loadAbout };
|
|
2261
|
+
const loaders = { overview: loadOverview, search: loadSearchTab, storage: loadStorage, sessions: loadSessions, security: loadSecurity, traces: loadTraces, mcp: loadMcp, logs: loadLogs, config: loadConfig, analytics: loadAnalytics, backups: loadBackups, context: loadContext, maintenance: loadMaintenance, snapshots: loadSnapshots, about: loadAbout };
|
|
2070
2262
|
loadTab('overview');
|
|
2071
2263
|
</script>
|
|
2072
2264
|
</body>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-ui.js","sourceRoot":"","sources":["../../src/lib/dashboard-ui.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB;IACnC,OAAO
|
|
1
|
+
{"version":3,"file":"dashboard-ui.js","sourceRoot":"","sources":["../../src/lib/dashboard-ui.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAutEC,CAAC;AACX,CAAC"}
|
package/dist/lib/dashboard.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ export interface DashboardOptions {
|
|
|
3
3
|
port?: number;
|
|
4
4
|
open?: boolean;
|
|
5
5
|
daemon?: boolean;
|
|
6
|
+
authToken?: string;
|
|
6
7
|
}
|
|
7
|
-
export declare function createDashboardServer(): http.Server;
|
|
8
|
+
export declare function createDashboardServer(authToken?: string): http.Server;
|
|
8
9
|
export declare function startDashboard(options?: DashboardOptions): Promise<http.Server>;
|
|
9
10
|
export declare function stopDashboard(): boolean;
|
|
10
11
|
export declare function isDashboardRunning(): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../../src/lib/dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAyD7B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4uCD,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CA6KrE;AAED,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CA6CrF;AAED,wBAAgB,aAAa,IAAI,OAAO,CAWvC;AAED,wBAAgB,kBAAkB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAStF"}
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -8,7 +8,10 @@ import { analyzeClaudeStorage, cleanClaudeDirectory } from "./storage.js";
|
|
|
8
8
|
import { listSessions, diagnoseSession, repairSession, extractSessionContent } from "./session-recovery.js";
|
|
9
9
|
import { scanForSecrets, auditSession, enforceRetention, generateComplianceReport } from "./security.js";
|
|
10
10
|
import { inventoryTraces, cleanTraces, wipeAllTraces, generateEnhancedPreview } from "./trace.js";
|
|
11
|
-
import { diagnoseMcpServers, probeMcpServer } from "./mcp-validator.js";
|
|
11
|
+
import { diagnoseMcpServers, probeMcpServer, analyzeMcpPerformance } from "./mcp-validator.js";
|
|
12
|
+
import { scanForPII, redactPII, redactAllPII } from "./security.js";
|
|
13
|
+
import { checkAlerts, checkQuotas } from "./alerts.js";
|
|
14
|
+
import { linkSessionsToGit } from "./git.js";
|
|
12
15
|
import { listLogFiles, parseAllLogs, getLogSummary } from "./logs.js";
|
|
13
16
|
import { findAllJsonlFiles, findBackupFiles, scanFile, fixFile, getConversationStats, estimateContextSize, generateUsageAnalytics, findDuplicates, findArchiveCandidates, archiveConversations, runMaintenance, deleteOldBackups, restoreFromBackup, exportConversation, } from "./scanner.js";
|
|
14
17
|
import { saveStorageSnapshot, listStorageSnapshots, loadStorageSnapshot, compareStorageSnapshots, deleteStorageSnapshot, // Missing export in storage.ts? I added it.
|
|
@@ -40,6 +43,24 @@ function parseUrl(url) {
|
|
|
40
43
|
}
|
|
41
44
|
return { pathname, params };
|
|
42
45
|
}
|
|
46
|
+
function extractBearerToken(req, params, authToken) {
|
|
47
|
+
if (!authToken)
|
|
48
|
+
return true;
|
|
49
|
+
const header = req.headers["authorization"];
|
|
50
|
+
if (typeof header === "string" && header.toLowerCase().startsWith("bearer ")) {
|
|
51
|
+
const provided = header.slice(7).trim();
|
|
52
|
+
if (provided === authToken)
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (params?.token === authToken) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function rejectUnauthorized(res) {
|
|
61
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
62
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
63
|
+
}
|
|
43
64
|
function matchRoute(pathname, pattern) {
|
|
44
65
|
const pathParts = pathname.split("/").filter(Boolean);
|
|
45
66
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
@@ -813,10 +834,11 @@ function getSearch(params) {
|
|
|
813
834
|
const query = params.q || params.query;
|
|
814
835
|
if (!query || query.length < 2)
|
|
815
836
|
return { results: [], count: 0, error: "Query too short" };
|
|
837
|
+
const roleFilter = params.role || '';
|
|
838
|
+
const maxResults = Math.min(parseInt(params.limit) || 50, 500);
|
|
816
839
|
const files = findAllJsonlFiles(PROJECTS_DIR);
|
|
817
840
|
const results = [];
|
|
818
841
|
let count = 0;
|
|
819
|
-
const maxResults = 50;
|
|
820
842
|
const lowerQuery = query.toLowerCase();
|
|
821
843
|
for (const file of files) {
|
|
822
844
|
if (count >= maxResults)
|
|
@@ -828,9 +850,10 @@ function getSearch(params) {
|
|
|
828
850
|
const line = lines[i];
|
|
829
851
|
if (line.toLowerCase().includes(lowerQuery)) {
|
|
830
852
|
let preview = line;
|
|
853
|
+
let role = 'unknown';
|
|
831
854
|
try {
|
|
832
855
|
const data = JSON.parse(line);
|
|
833
|
-
|
|
856
|
+
role = data.message?.role || data.role || (data.type === 'human' ? 'user' : data.type === 'assistant' ? 'assistant' : 'unknown');
|
|
834
857
|
if (data.message?.content) {
|
|
835
858
|
if (typeof data.message.content === 'string')
|
|
836
859
|
preview = data.message.content;
|
|
@@ -842,12 +865,15 @@ function getSearch(params) {
|
|
|
842
865
|
}
|
|
843
866
|
}
|
|
844
867
|
catch { }
|
|
845
|
-
if (
|
|
846
|
-
|
|
868
|
+
if (roleFilter && role !== roleFilter)
|
|
869
|
+
continue;
|
|
870
|
+
if (preview.length > 300)
|
|
871
|
+
preview = preview.slice(0, 300) + '...';
|
|
847
872
|
results.push({
|
|
848
|
-
file
|
|
873
|
+
file,
|
|
849
874
|
line: i + 1,
|
|
850
875
|
preview,
|
|
876
|
+
role,
|
|
851
877
|
match: true
|
|
852
878
|
});
|
|
853
879
|
count++;
|
|
@@ -1055,6 +1081,19 @@ function actionRedactAll() {
|
|
|
1055
1081
|
}
|
|
1056
1082
|
return { success: true, filesModified, secretsRedacted, errors, items: Array.from(fileGroups.keys()).slice(0, 50) };
|
|
1057
1083
|
}
|
|
1084
|
+
function actionRedactPII(body) {
|
|
1085
|
+
const file = body.file;
|
|
1086
|
+
const lineNum = body.line;
|
|
1087
|
+
const piiType = body.type;
|
|
1088
|
+
if (!file || !lineNum)
|
|
1089
|
+
return { success: false, error: "file and line required" };
|
|
1090
|
+
const result = redactPII(file, lineNum, piiType);
|
|
1091
|
+
return { ...result };
|
|
1092
|
+
}
|
|
1093
|
+
function actionRedactAllPII() {
|
|
1094
|
+
const result = redactAllPII();
|
|
1095
|
+
return { ...result };
|
|
1096
|
+
}
|
|
1058
1097
|
function actionArchive(body) {
|
|
1059
1098
|
const dryRun = body.dryRun !== false;
|
|
1060
1099
|
const days = body.days || 30;
|
|
@@ -1181,6 +1220,54 @@ const getRoutes = {
|
|
|
1181
1220
|
"/api/maintenance": () => getMaintenanceCheck(),
|
|
1182
1221
|
"/api/scan": () => getScan(),
|
|
1183
1222
|
"/api/search": (params) => getSearch(params),
|
|
1223
|
+
"/api/alerts": () => {
|
|
1224
|
+
const report = checkAlerts();
|
|
1225
|
+
return { alerts: report.alerts, critical: report.critical, warning: report.warning, info: report.info };
|
|
1226
|
+
},
|
|
1227
|
+
"/api/quotas": () => {
|
|
1228
|
+
const quotas = checkQuotas();
|
|
1229
|
+
return { quotas };
|
|
1230
|
+
},
|
|
1231
|
+
"/api/mcp-perf": () => {
|
|
1232
|
+
const report = analyzeMcpPerformance();
|
|
1233
|
+
return { totalCalls: report.totalCalls, totalErrors: report.totalErrors, errorRate: report.errorRate, toolStats: report.toolStats, serverStats: Object.fromEntries(report.serverStats) };
|
|
1234
|
+
},
|
|
1235
|
+
"/api/pii": (params) => {
|
|
1236
|
+
const limit = parseInt(params?.limit) || 50;
|
|
1237
|
+
const offset = parseInt(params?.offset) || 0;
|
|
1238
|
+
const result = scanForPII(undefined, { includeFullValues: true });
|
|
1239
|
+
const paginatedFindings = result.findings.slice(offset, offset + limit);
|
|
1240
|
+
return {
|
|
1241
|
+
filesScanned: result.filesScanned,
|
|
1242
|
+
totalFindings: result.totalFindings,
|
|
1243
|
+
findings: paginatedFindings,
|
|
1244
|
+
byCategory: result.byCategory,
|
|
1245
|
+
bySensitivity: result.bySensitivity,
|
|
1246
|
+
offset,
|
|
1247
|
+
limit,
|
|
1248
|
+
hasMore: offset + limit < result.totalFindings
|
|
1249
|
+
};
|
|
1250
|
+
},
|
|
1251
|
+
"/api/git": () => {
|
|
1252
|
+
const report = linkSessionsToGit();
|
|
1253
|
+
return { sessionsWithGit: report.sessionsWithGit, sessionsWithoutGit: report.sessionsWithoutGit, branches: Object.fromEntries(report.branches), links: report.links.slice(0, 20) };
|
|
1254
|
+
},
|
|
1255
|
+
"/api/cost": () => {
|
|
1256
|
+
const files = findAllJsonlFiles(PROJECTS_DIR);
|
|
1257
|
+
let totalInput = 0, totalOutput = 0;
|
|
1258
|
+
for (const f of files) {
|
|
1259
|
+
try {
|
|
1260
|
+
const est = estimateContextSize(f);
|
|
1261
|
+
const b = est.breakdown;
|
|
1262
|
+
totalInput += b.userTokens + b.systemTokens + b.toolUseTokens;
|
|
1263
|
+
totalOutput += b.assistantTokens + b.toolResultTokens;
|
|
1264
|
+
}
|
|
1265
|
+
catch { /* skip */ }
|
|
1266
|
+
}
|
|
1267
|
+
const inputCost = (totalInput / 1_000_000) * 15;
|
|
1268
|
+
const outputCost = (totalOutput / 1_000_000) * 75;
|
|
1269
|
+
return { inputTokens: totalInput, outputTokens: totalOutput, totalTokens: totalInput + totalOutput, inputCost, outputCost, totalCost: inputCost + outputCost, sessions: files.length };
|
|
1270
|
+
},
|
|
1184
1271
|
"/api/snapshots": () => ({ snapshots: listStorageSnapshots() }),
|
|
1185
1272
|
"/api/snapshot": (params) => {
|
|
1186
1273
|
const id = params?.id;
|
|
@@ -1213,6 +1300,8 @@ const postRoutes = {
|
|
|
1213
1300
|
"/api/action/clean-traces": (b) => actionCleanTraces(b),
|
|
1214
1301
|
"/api/action/redact": (b) => actionRedact(b),
|
|
1215
1302
|
"/api/action/redact-all": () => actionRedactAll(),
|
|
1303
|
+
"/api/action/redact-pii": (b) => actionRedactPII(b),
|
|
1304
|
+
"/api/action/redact-all-pii": () => actionRedactAllPII(),
|
|
1216
1305
|
"/api/action/archive": (b) => actionArchive(b),
|
|
1217
1306
|
"/api/action/maintenance": (b) => actionMaintenanceRun(b),
|
|
1218
1307
|
"/api/action/delete-backups": (b) => actionDeleteBackups(b),
|
|
@@ -1235,7 +1324,7 @@ const postRoutes = {
|
|
|
1235
1324
|
return { success: deleteStorageSnapshot(id) };
|
|
1236
1325
|
}
|
|
1237
1326
|
};
|
|
1238
|
-
export function createDashboardServer() {
|
|
1327
|
+
export function createDashboardServer(authToken) {
|
|
1239
1328
|
const html = generateDashboardHTML();
|
|
1240
1329
|
const server = http.createServer(async (req, res) => {
|
|
1241
1330
|
const { pathname, params } = parseUrl(req.url || "/");
|
|
@@ -1244,7 +1333,53 @@ export function createDashboardServer() {
|
|
|
1244
1333
|
res.end(html);
|
|
1245
1334
|
return;
|
|
1246
1335
|
}
|
|
1336
|
+
if (req.method === "GET" && pathname === "/api/events") {
|
|
1337
|
+
if (!extractBearerToken(req, params, authToken)) {
|
|
1338
|
+
rejectUnauthorized(res);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
res.writeHead(200, {
|
|
1342
|
+
"Content-Type": "text/event-stream",
|
|
1343
|
+
"Cache-Control": "no-cache",
|
|
1344
|
+
"Connection": "keep-alive",
|
|
1345
|
+
"Access-Control-Allow-Origin": "*",
|
|
1346
|
+
});
|
|
1347
|
+
const sendEvent = (event, data) => {
|
|
1348
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
1349
|
+
};
|
|
1350
|
+
sendEvent("connected", { time: new Date().toISOString() });
|
|
1351
|
+
const interval = setInterval(() => {
|
|
1352
|
+
try {
|
|
1353
|
+
const files = findAllJsonlFiles(PROJECTS_DIR);
|
|
1354
|
+
let totalSize = 0;
|
|
1355
|
+
for (const f of files) {
|
|
1356
|
+
try {
|
|
1357
|
+
totalSize += fs.statSync(f).size;
|
|
1358
|
+
}
|
|
1359
|
+
catch { /* skip */ }
|
|
1360
|
+
}
|
|
1361
|
+
sendEvent("stats", {
|
|
1362
|
+
sessions: files.length,
|
|
1363
|
+
totalSize,
|
|
1364
|
+
timestamp: new Date().toISOString(),
|
|
1365
|
+
});
|
|
1366
|
+
const alertReport = checkAlerts();
|
|
1367
|
+
if (alertReport.alerts.length > 0) {
|
|
1368
|
+
sendEvent("alerts", { count: alertReport.alerts.length, critical: alertReport.critical });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
catch { /* skip */ }
|
|
1372
|
+
}, 10000);
|
|
1373
|
+
req.on("close", () => {
|
|
1374
|
+
clearInterval(interval);
|
|
1375
|
+
});
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1247
1378
|
if (req.method === "GET" && pathname.startsWith("/api/")) {
|
|
1379
|
+
if (!extractBearerToken(req, params, authToken)) {
|
|
1380
|
+
rejectUnauthorized(res);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1248
1383
|
const handler = getRoutes[pathname];
|
|
1249
1384
|
if (handler) {
|
|
1250
1385
|
try {
|
|
@@ -1336,6 +1471,10 @@ export function createDashboardServer() {
|
|
|
1336
1471
|
}
|
|
1337
1472
|
}
|
|
1338
1473
|
if (req.method === "POST" && pathname.startsWith("/api/action/")) {
|
|
1474
|
+
if (!extractBearerToken(req, params, authToken)) {
|
|
1475
|
+
rejectUnauthorized(res);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1339
1478
|
const handler = postRoutes[pathname];
|
|
1340
1479
|
if (handler) {
|
|
1341
1480
|
try {
|
|
@@ -1359,7 +1498,7 @@ export function createDashboardServer() {
|
|
|
1359
1498
|
export async function startDashboard(options) {
|
|
1360
1499
|
const port = options?.port || 1405;
|
|
1361
1500
|
const shouldOpen = options?.open !== false;
|
|
1362
|
-
const server = createDashboardServer();
|
|
1501
|
+
const server = createDashboardServer(options?.authToken);
|
|
1363
1502
|
return new Promise((resolve, reject) => {
|
|
1364
1503
|
server.on("error", (err) => {
|
|
1365
1504
|
if (err.code === "EADDRINUSE") {
|
|
@@ -1370,6 +1509,9 @@ export async function startDashboard(options) {
|
|
|
1370
1509
|
server.listen(port, "127.0.0.1", () => {
|
|
1371
1510
|
const url = `http://localhost:${port}`;
|
|
1372
1511
|
console.log(`Dashboard running at ${url}`);
|
|
1512
|
+
if (options?.authToken) {
|
|
1513
|
+
console.log("Authentication required: include the dashboard token in Authorization headers.");
|
|
1514
|
+
}
|
|
1373
1515
|
if (options?.daemon) {
|
|
1374
1516
|
try {
|
|
1375
1517
|
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
@@ -1383,8 +1525,15 @@ export async function startDashboard(options) {
|
|
|
1383
1525
|
}
|
|
1384
1526
|
if (shouldOpen) {
|
|
1385
1527
|
const platform = os.platform();
|
|
1386
|
-
|
|
1387
|
-
|
|
1528
|
+
if (platform === "win32") {
|
|
1529
|
+
execFile("cmd", ["/c", "start", "", url], () => { });
|
|
1530
|
+
}
|
|
1531
|
+
else if (platform === "darwin") {
|
|
1532
|
+
execFile("open", [url], () => { });
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
execFile("xdg-open", [url], () => { });
|
|
1536
|
+
}
|
|
1388
1537
|
}
|
|
1389
1538
|
resolve(server);
|
|
1390
1539
|
});
|