@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.
@@ -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 12px 0 0;position:relative">
330
- <input type="text" id="globalSearch" class="search-input" placeholder="Search..." style="width:200px;padding:6px 12px;font-size:12px" onkeyup="if(event.key==='Enter') doGlobalSearch(this.value)">
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 { const r = await fetch('/api/'+ep); if(!r.ok) throw new Error(r.statusText); const d = await r.json(); cache[ep]=d; return d; }
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 fetch('/api/action/'+ep, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body||{}) });
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 loadSearch() {
775
+ async function loadSearchTab() {
732
776
  const el = $('#sec-search');
733
- set(el, '<div class="loading"><div class="spinner"></div><div>Searching for "'+esc(currentSearchQuery)+'"...</div></div>');
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
- const res = await api('search?q='+encodeURIComponent(currentSearchQuery));
736
- if(!res || !res.results || res.results.length === 0) {
737
- set(el, emptyState('&#128270;', 'No results found', 'Try a different query', 'Back to Overview', 'loadTab("overview")'));
738
- return;
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 = '<h2>Search Results: "'+esc(currentSearchQuery)+'" ('+res.results.length+')</h2>';
741
- h += '<div class="grid" style="grid-template-columns:1fr">';
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
- h += '<div class="card" style="padding:12px">';
744
- h += '<div style="font-weight:600;margin-bottom:6px;font-size:12px;color:var(--accent)">'+esc(r.file)+' <span style="color:var(--text3)">: '+r.line+'</span></div>';
745
- h += '<div style="font-family:var(--mono);font-size:11px;color:var(--text2);background:var(--bg);padding:8px;border-radius:4px;overflow-x:auto;white-space:pre-wrap">'+esc(r.preview)+'</div>';
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
- h += '</div>';
749
- set(el, h);
838
+ resultsEl.innerHTML = h;
750
839
  } catch(e) {
751
- set(el, '<div class="empty-state">Error: '+e.message+'</div>');
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('&#128274;', '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>&#128100; 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">&#128269;</div><h4>Files Scanned</h4><div class="value">' + pii.filesScanned + '</div></div>';
1085
+ h += '<div class="card"><div class="stat-icon">&#128680;</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">&#128308;</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">&#128993;</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('action/snapshot', { label });
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('action/delete-snapshot', { id });
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuhEC,CAAC;AACX,CAAC"}
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"}
@@ -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;AAqD7B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAspCD,wBAAgB,qBAAqB,IAAI,IAAI,CAAC,MAAM,CAwHnD;AAED,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAqCrF;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"}
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"}
@@ -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
- // Try to extract readable text
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 (preview.length > 200)
846
- preview = preview.slice(0, 200) + '...';
868
+ if (roleFilter && role !== roleFilter)
869
+ continue;
870
+ if (preview.length > 300)
871
+ preview = preview.slice(0, 300) + '...';
847
872
  results.push({
848
- file: path.relative(PROJECTS_DIR, 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
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
1387
- execFile(cmd, [url], () => { });
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
  });