@asifkibria/claude-code-toolkit 1.0.2 → 1.2.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.
Files changed (69) hide show
  1. package/README.md +165 -214
  2. package/dist/CLAUDE.md +7 -0
  3. package/dist/__tests__/dashboard.test.d.ts +2 -0
  4. package/dist/__tests__/dashboard.test.d.ts.map +1 -0
  5. package/dist/__tests__/dashboard.test.js +606 -0
  6. package/dist/__tests__/dashboard.test.js.map +1 -0
  7. package/dist/__tests__/mcp-validator.test.d.ts +2 -0
  8. package/dist/__tests__/mcp-validator.test.d.ts.map +1 -0
  9. package/dist/__tests__/mcp-validator.test.js +217 -0
  10. package/dist/__tests__/mcp-validator.test.js.map +1 -0
  11. package/dist/__tests__/scanner.test.js +350 -1
  12. package/dist/__tests__/scanner.test.js.map +1 -1
  13. package/dist/__tests__/security.test.d.ts +2 -0
  14. package/dist/__tests__/security.test.d.ts.map +1 -0
  15. package/dist/__tests__/security.test.js +375 -0
  16. package/dist/__tests__/security.test.js.map +1 -0
  17. package/dist/__tests__/session-recovery.test.d.ts +2 -0
  18. package/dist/__tests__/session-recovery.test.d.ts.map +1 -0
  19. package/dist/__tests__/session-recovery.test.js +230 -0
  20. package/dist/__tests__/session-recovery.test.js.map +1 -0
  21. package/dist/__tests__/storage.test.d.ts +2 -0
  22. package/dist/__tests__/storage.test.d.ts.map +1 -0
  23. package/dist/__tests__/storage.test.js +241 -0
  24. package/dist/__tests__/storage.test.js.map +1 -0
  25. package/dist/__tests__/trace.test.d.ts +2 -0
  26. package/dist/__tests__/trace.test.d.ts.map +1 -0
  27. package/dist/__tests__/trace.test.js +376 -0
  28. package/dist/__tests__/trace.test.js.map +1 -0
  29. package/dist/cli.js +501 -20
  30. package/dist/cli.js.map +1 -1
  31. package/dist/index.js +950 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/dashboard-ui.d.ts +2 -0
  34. package/dist/lib/dashboard-ui.d.ts.map +1 -0
  35. package/dist/lib/dashboard-ui.js +2075 -0
  36. package/dist/lib/dashboard-ui.js.map +1 -0
  37. package/dist/lib/dashboard.d.ts +15 -0
  38. package/dist/lib/dashboard.d.ts.map +1 -0
  39. package/dist/lib/dashboard.js +1422 -0
  40. package/dist/lib/dashboard.js.map +1 -0
  41. package/dist/lib/logs.d.ts +42 -0
  42. package/dist/lib/logs.d.ts.map +1 -0
  43. package/dist/lib/logs.js +166 -0
  44. package/dist/lib/logs.js.map +1 -0
  45. package/dist/lib/mcp-validator.d.ts +86 -0
  46. package/dist/lib/mcp-validator.d.ts.map +1 -0
  47. package/dist/lib/mcp-validator.js +463 -0
  48. package/dist/lib/mcp-validator.js.map +1 -0
  49. package/dist/lib/scanner.d.ts +187 -2
  50. package/dist/lib/scanner.d.ts.map +1 -1
  51. package/dist/lib/scanner.js +1224 -14
  52. package/dist/lib/scanner.js.map +1 -1
  53. package/dist/lib/security.d.ts +57 -0
  54. package/dist/lib/security.d.ts.map +1 -0
  55. package/dist/lib/security.js +423 -0
  56. package/dist/lib/security.js.map +1 -0
  57. package/dist/lib/session-recovery.d.ts +60 -0
  58. package/dist/lib/session-recovery.d.ts.map +1 -0
  59. package/dist/lib/session-recovery.js +433 -0
  60. package/dist/lib/session-recovery.js.map +1 -0
  61. package/dist/lib/storage.d.ts +68 -0
  62. package/dist/lib/storage.d.ts.map +1 -0
  63. package/dist/lib/storage.js +500 -0
  64. package/dist/lib/storage.js.map +1 -0
  65. package/dist/lib/trace.d.ts +119 -0
  66. package/dist/lib/trace.d.ts.map +1 -0
  67. package/dist/lib/trace.js +649 -0
  68. package/dist/lib/trace.js.map +1 -0
  69. package/package.json +11 -3
@@ -0,0 +1,2075 @@
1
+ export function generateDashboardHTML() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Claude Code Toolkit</title>
8
+ <style>
9
+ :root {
10
+ --bg: #0a0e14; --bg2: #12171f; --bg3: #1a2030; --bg4: #222d3d;
11
+ --border: #2a3545; --border-light: #354560;
12
+ --text: #e8edf5; --text2: #a0aec0; --text3: #6b7a90;
13
+ --accent: #60a5fa; --accent2: #3b82f6; --accent-glow: rgba(96,165,250,0.15);
14
+ --green: #34d399; --green-bg: rgba(52,211,153,0.1);
15
+ --yellow: #fbbf24; --yellow-bg: rgba(251,191,36,0.1);
16
+ --red: #f87171; --red-bg: rgba(248,113,113,0.1);
17
+ --orange: #fb923c; --orange-bg: rgba(251,146,60,0.1);
18
+ --purple: #a78bfa; --purple-bg: rgba(167,139,250,0.1);
19
+ --radius: 12px; --radius-sm: 8px;
20
+ --shadow: 0 4px 24px rgba(0,0,0,0.3);
21
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
22
+ --mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
23
+ --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
24
+ }
25
+ @media (prefers-color-scheme: light) {
26
+ html:not(.dark) {
27
+ --bg: #f8fafc; --bg2: #ffffff; --bg3: #f1f5f9; --bg4: #e2e8f0;
28
+ --border: #e2e8f0; --border-light: #cbd5e1;
29
+ --text: #0f172a; --text2: #475569; --text3: #6b7280;
30
+ --accent: #3b82f6; --accent2: #2563eb; --accent-glow: rgba(59,130,246,0.08);
31
+ --green: #059669; --green-bg: rgba(5,150,105,0.06);
32
+ --yellow: #d97706; --yellow-bg: rgba(217,119,6,0.06);
33
+ --red: #dc2626; --red-bg: rgba(220,38,38,0.06);
34
+ --orange: #ea580c; --orange-bg: rgba(234,88,12,0.06);
35
+ --purple: #7c3aed; --purple-bg: rgba(124,58,237,0.06);
36
+ --shadow: 0 4px 24px rgba(0,0,0,0.06);
37
+ }
38
+ }
39
+ html.light {
40
+ --bg: #f8fafc; --bg2: #ffffff; --bg3: #f1f5f9; --bg4: #e2e8f0;
41
+ --border: #e2e8f0; --border-light: #cbd5e1;
42
+ --text: #0f172a; --text2: #475569; --text3: #6b7280;
43
+ --accent: #3b82f6; --accent2: #2563eb; --accent-glow: rgba(59,130,246,0.08);
44
+ --green: #059669; --green-bg: rgba(5,150,105,0.06);
45
+ --yellow: #d97706; --yellow-bg: rgba(217,119,6,0.06);
46
+ --red: #dc2626; --red-bg: rgba(220,38,38,0.06);
47
+ --orange: #ea580c; --orange-bg: rgba(234,88,12,0.06);
48
+ --purple: #7c3aed; --purple-bg: rgba(124,58,237,0.06);
49
+ --shadow: 0 4px 24px rgba(0,0,0,0.06);
50
+ }
51
+ .theme-btn { display:flex; align-items:center; justify-content:center; width:32px; height:32px; background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text2); cursor:pointer; transition:var(--transition); }
52
+ .theme-btn:hover { color:var(--accent); border-color:var(--accent); background:var(--accent-glow); }
53
+ .theme-btn svg { width:16px; height:16px; }
54
+ .theme-btn .sun { display:none; }
55
+ .theme-btn .moon { display:block; }
56
+ html.light .theme-btn .sun { display:block; }
57
+ html.light .theme-btn .moon { display:none; }
58
+ * { margin: 0; padding: 0; box-sizing: border-box; }
59
+ body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; }
60
+ .header { display: flex; align-items: center; justify-content: space-between; padding: 16px 28px; background: linear-gradient(135deg, var(--bg2) 0%, var(--bg3) 100%); border-bottom: 1px solid var(--border); }
61
+ .logo { display: flex; align-items: center; gap: 12px; }
62
+ .logo-icon { width: 32px; height: 32px; background: linear-gradient(135deg, var(--accent), var(--purple)); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; color: #fff; box-shadow: 0 2px 10px rgba(96,165,250,0.25); }
63
+ .header-dot { width: 8px; height: 8px; border-radius: 50%; display: none; flex-shrink: 0; }
64
+ .header-dot.visible { display: inline-block; }
65
+ .logo h1 { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; }
66
+ .logo h1 span { color: var(--text3); font-weight: 400; margin-left: 6px; font-size: 12px; }
67
+ .header-right { display: flex; align-items: center; gap: 12px; }
68
+ .auto-refresh { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text3); }
69
+ .auto-refresh label { cursor: pointer; }
70
+ .toggle { width: 36px; height: 20px; background: var(--bg4); border-radius: 10px; position: relative; cursor: pointer; transition: var(--transition); border: none; }
71
+ .toggle.on { background: var(--accent); }
72
+ .toggle::after { content: ''; width: 16px; height: 16px; background: #fff; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: var(--transition); }
73
+ .toggle.on::after { left: 18px; }
74
+ .refresh-time { font-size: 11px; color: var(--text3); min-width: 100px; text-align: right; }
75
+ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--bg3); color: var(--text); cursor: pointer; font-size: 13px; font-weight: 500; transition: var(--transition); font-family: var(--font); }
76
+ .btn:hover { background: var(--bg4); border-color: var(--border-light); }
77
+ .btn:active { transform: scale(0.97); }
78
+ .btn-primary { background: var(--accent2); color: #fff; border-color: transparent; }
79
+ .btn-primary:hover { background: var(--accent); }
80
+ .btn-danger { color: var(--red); border-color: rgba(248,113,113,0.3); }
81
+ .btn-danger:hover { background: var(--red-bg); }
82
+ .btn-success { color: var(--green); border-color: rgba(52,211,153,0.3); }
83
+ .btn-success:hover { background: var(--green-bg); }
84
+ .btn-warn { color: var(--yellow); border-color: rgba(251,191,36,0.3); }
85
+ .btn-warn:hover { background: var(--yellow-bg); }
86
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
87
+ .btn-icon { padding: 6px 8px; }
88
+ .nav { display: flex; gap: 0; background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 28px; overflow-x: auto; scrollbar-width: none; }
89
+ .nav::-webkit-scrollbar { display: none; }
90
+ .nav-item { padding: 12px 18px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--text3); border-bottom: 2px solid transparent; transition: var(--transition); white-space: nowrap; user-select: none; }
91
+ .nav-item:hover { color: var(--text2); background: var(--accent-glow); }
92
+ .nav-item.active { color: var(--accent); border-bottom-color: var(--accent); }
93
+ .nav-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 5px; border-radius: 8px; font-size: 10px; font-weight: 700; margin-left: 6px; line-height: 1; }
94
+ .nav-badge-red { background: var(--red); color: #fff; }
95
+ .nav-badge-yellow { background: var(--yellow); color: #000; }
96
+ .nav-badge-green { background: var(--green); color: #fff; }
97
+ .nav-badge-blue { background: var(--accent2); color: #fff; }
98
+ .content { padding: 24px 28px; max-width: 1280px; margin: 0 auto; width: 100%; flex: 1; }
99
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
100
+ .card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition); }
101
+ .card:hover { border-color: var(--border-light); transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.1); }
102
+ .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
103
+ .card h3 { font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; }
104
+ .card .value { font-size: 32px; font-weight: 700; letter-spacing: -1px; line-height: 1.1; }
105
+ .card .sub { font-size: 12px; color: var(--text3); margin-top: 6px; }
106
+ .card-glow { border-color: rgba(96,165,250,0.2); box-shadow: 0 0 20px var(--accent-glow); }
107
+ .stat-icon { font-size: 20px; line-height: 1; opacity: 0.85; float: right; margin-top: -2px; }
108
+ .mt { margin-top: 16px; }
109
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
110
+ .dot-green { background: var(--green); box-shadow: 0 0 6px var(--green); }
111
+ .dot-yellow { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
112
+ .dot-red { background: var(--red); box-shadow: 0 0 6px var(--red); }
113
+ @keyframes dotPulseGreen { 0%,100%{opacity:1} 50%{opacity:0.6} }
114
+ @keyframes dotPulseYellow { 0%,100%{opacity:1} 50%{opacity:0.5} }
115
+ @keyframes dotPulseRed { 0%,100%{opacity:1} 50%{opacity:0.4} }
116
+ .dot-green { animation: dotPulseGreen 2.5s ease-in-out infinite; }
117
+ .dot-yellow { animation: dotPulseYellow 1.8s ease-in-out infinite; }
118
+ .dot-red { animation: dotPulseRed 1s ease-in-out infinite; }
119
+ .c-green { color: var(--green); } .c-yellow { color: var(--yellow); } .c-red { color: var(--red); } .c-orange { color: var(--orange); } .c-accent { color: var(--accent); } .c-purple { color: var(--purple); }
120
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
121
+ th { text-align: left; padding: 10px 14px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text3); border-bottom: 1px solid var(--border); }
122
+ td { padding: 10px 14px; border-bottom: 1px solid var(--border); }
123
+ tr:hover td { background: var(--accent-glow); }
124
+ th.sort-header { cursor: pointer; user-select: none; transition: var(--transition); }
125
+ th.sort-header:hover { color: var(--accent); }
126
+ th.sort-header::after { content: ''; display: inline-block; margin-left: 4px; opacity: 0.3; }
127
+ th.sort-header.asc::after { content: '\\2191'; opacity: 1; color: var(--accent); }
128
+ th.sort-header.desc::after { content: '\\2193'; opacity: 1; color: var(--accent); }
129
+ .mono { font-family: var(--mono); font-size: 12px; }
130
+ .badge { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; letter-spacing: 0.3px; }
131
+ .b-green { background: var(--green-bg); color: var(--green); }
132
+ .b-yellow { background: var(--yellow-bg); color: var(--yellow); }
133
+ .b-red { background: var(--red-bg); color: var(--red); }
134
+ .b-blue { background: var(--accent-glow); color: var(--accent); }
135
+ .b-orange { background: var(--orange-bg); color: var(--orange); }
136
+ .bars { display: flex; flex-direction: column; gap: 12px; }
137
+ .bar-row { display: flex; align-items: center; gap: 12px; }
138
+ .bar-label { width: 130px; font-size: 12px; flex-shrink: 0; color: var(--text2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
139
+ .bar-track { flex: 1; height: 24px; background: var(--bg); border-radius: 6px; overflow: hidden; }
140
+ .bar-fill { height: 100%; border-radius: 6px; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); min-width: 3px; }
141
+ .bar-val { width: 90px; font-size: 12px; text-align: right; color: var(--text3); flex-shrink: 0; font-family: var(--mono); }
142
+ .spark { display: flex; align-items: flex-end; gap: 2px; height: 80px; padding: 4px 0; }
143
+ .spark-bar { flex: 1; min-width: 4px; border-radius: 3px 3px 0 0; background: var(--accent); opacity: 0.7; transition: opacity 0.15s, height 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
144
+ .spark-bar:hover { opacity: 1; }
145
+ .spark-tooltip { position: fixed; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 10px; font-size: 12px; pointer-events: none; z-index: 50; box-shadow: var(--shadow); white-space: nowrap; transform: translate(-50%, -100%); margin-top: -8px; }
146
+ .spark-tooltip .st-date { color: var(--text3); font-size: 11px; }
147
+ .spark-tooltip .st-val { color: var(--accent); font-weight: 600; margin-left: 6px; }
148
+ .section { display: none; }
149
+ .section.active { display: block; animation: fadeIn 0.25s ease; }
150
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
151
+ @keyframes cardIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
152
+ .card.stagger { opacity: 0; animation: cardIn 0.35s ease forwards; }
153
+ .section h2 { font-size: 20px; font-weight: 700; margin-bottom: 20px; letter-spacing: -0.3px; }
154
+ .loading { text-align: center; padding: 80px 20px; color: var(--text3); }
155
+ .spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; margin-bottom: 12px; }
156
+ @keyframes spin { to { transform: rotate(360deg); } }
157
+ .empty { text-align: center; padding: 60px; color: var(--text3); }
158
+ .empty-state { text-align: center; padding: 60px 24px; }
159
+ .empty-state .es-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
160
+ .empty-state .es-title { font-size: 16px; font-weight: 600; color: var(--text2); margin-bottom: 6px; }
161
+ .empty-state .es-sub { font-size: 13px; color: var(--text3); max-width: 400px; margin: 0 auto 16px; line-height: 1.5; }
162
+ .clickable-card { cursor: pointer; }
163
+ .clickable-card:hover { border-color: var(--accent); box-shadow: 0 8px 25px rgba(96,165,250,0.12); }
164
+ .modal-overlay { display: none; position: fixed; inset: 0; background: linear-gradient(135deg, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.45) 100%); backdrop-filter: blur(8px) saturate(180%); z-index: 100; align-items: center; justify-content: center; animation: fadeIn 0.15s; }
165
+ .modal-overlay.active { display: flex; }
166
+ .modal { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; max-width: 680px; width: 90%; box-shadow: 0 8px 32px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.05); max-height: 80vh; overflow-y: auto; position: relative; }
167
+ .modal h3 { font-size: 16px; font-weight: 600; margin-bottom: 8px; padding-right: 32px; }
168
+ .modal p { font-size: 14px; color: var(--text2); margin-bottom: 20px; line-height: 1.5; }
169
+ .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
170
+ .modal-close { position: absolute; top: 16px; right: 16px; background: none; border: none; color: var(--text3); font-size: 20px; cursor: pointer; line-height: 1; padding: 4px 8px; border-radius: var(--radius-sm); transition: var(--transition); }
171
+ .modal-close:hover { color: var(--text); background: var(--bg4); }
172
+ .modal pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; font-family: var(--mono); font-size: 12px; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; margin-bottom: 16px; color: var(--text2); }
173
+ .toast-container { position: fixed; top: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
174
+ .toast { padding: 12px 20px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; animation: slideIn 0.3s ease; box-shadow: var(--shadow); }
175
+ .toast-success { background: var(--green); color: #fff; }
176
+ .toast-error { background: var(--red); color: #fff; }
177
+ .toast-info { background: var(--accent2); color: #fff; }
178
+ @keyframes slideIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
179
+ .action-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
180
+ .detail-panel { background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-top: 16px; animation: fadeIn 0.2s; }
181
+ .detail-panel h4 { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
182
+ .detail-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; border-bottom: 1px solid var(--border); }
183
+ .detail-row:last-child { border-bottom: none; }
184
+ .detail-key { color: var(--text3); }
185
+ .detail-val { font-weight: 500; }
186
+ .tag-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
187
+ .tag { padding: 2px 8px; border-radius: 4px; font-size: 11px; background: var(--bg4); color: var(--text2); }
188
+ .progress-bar { position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 300; pointer-events: none; opacity: 0; transition: opacity 0.2s; }
189
+ .progress-bar.active { opacity: 1; }
190
+ .progress-bar .track { height: 100%; background: linear-gradient(90deg, var(--accent), var(--purple)); border-radius: 0 2px 2px 0; animation: progressPulse 2s ease-in-out infinite; width: 30%; }
191
+ @keyframes progressPulse { 0%{width:5%;margin-left:0} 50%{width:45%;margin-left:30%} 100%{width:5%;margin-left:95%} }
192
+ .action-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.3); backdrop-filter: blur(2px); z-index: 250; align-items: center; justify-content: center; }
193
+ .action-overlay.active { display: flex; }
194
+ .action-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 32px 40px; text-align: center; box-shadow: var(--shadow); min-width: 300px; animation: fadeIn 0.2s; }
195
+ .action-card .spinner { width: 32px; height: 32px; }
196
+ .action-card .action-label { font-size: 14px; font-weight: 500; margin-top: 4px; color: var(--text2); }
197
+ .result-banner { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 20px; margin-bottom: 20px; animation: fadeIn 0.25s; display: flex; align-items: flex-start; gap: 14px; }
198
+ .result-banner.success { border-color: rgba(52,211,153,0.3); background: var(--green-bg); }
199
+ .result-banner.error { border-color: rgba(248,113,113,0.3); background: var(--red-bg); }
200
+ .result-icon { font-size: 20px; flex-shrink: 0; line-height: 1; margin-top: 2px; }
201
+ .result-body { flex: 1; }
202
+ .result-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
203
+ .result-details { font-size: 12px; color: var(--text2); line-height: 1.6; }
204
+ .result-details span { display: inline-block; margin-right: 16px; }
205
+ .result-close { background: none; border: none; color: var(--text3); cursor: pointer; font-size: 16px; padding: 4px; line-height: 1; }
206
+ .health-ring { position: relative; width: 110px; height: 110px; margin: 0 auto 8px; }
207
+ .health-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
208
+ .health-ring circle { fill: none; stroke-width: 8; stroke-linecap: round; }
209
+ .health-ring .ring-bg { stroke: var(--bg4); }
210
+ .health-ring .ring-fg { transition: stroke-dashoffset 1s cubic-bezier(0.22, 1, 0.36, 1); }
211
+ .health-ring .score { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; }
212
+ .health-ring .score-val { font-size: 26px; font-weight: 800; letter-spacing: -1px; }
213
+ .health-ring .score-lbl { font-size: 9px; color: var(--text3); text-transform: uppercase; font-weight: 600; letter-spacing: 0.5px; }
214
+ .audit-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin: 16px 0; overflow-x: auto; }
215
+ .audit-tab { padding: 8px 16px; cursor: pointer; font-size: 12px; font-weight: 500; color: var(--text3); border-bottom: 2px solid transparent; transition: var(--transition); white-space: nowrap; }
216
+ .audit-tab:hover { color: var(--text2); }
217
+ .audit-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
218
+ .audit-pane { display: none; animation: fadeIn 0.2s; }
219
+ .audit-pane.active { display: block; }
220
+ .search-bar { margin-bottom: 16px; }
221
+ .search-input { width: 100%; max-width: 400px; padding: 8px 14px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--bg); color: var(--text); font-family: var(--font); font-size: 13px; outline: none; transition: var(--transition); }
222
+ .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
223
+ .search-input::placeholder { color: var(--text3); }
224
+ .session-row-active td { background: var(--accent-glow) !important; }
225
+ .project-cell { display: flex; flex-direction: column; gap: 2px; }
226
+ .project-name { font-weight: 500; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
227
+ .project-path { font-size: 11px; color: var(--text3); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
228
+ .code-editor { width: 100%; min-height: 500px; padding: 16px; font-family: var(--mono); font-size: 13px; line-height: 1.5; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg3); color: var(--text); resize: vertical; tab-size: 2; outline: none; transition: border-color var(--transition); }
229
+ .code-editor:focus { border-color: var(--accent); }
230
+ .about-hero { text-align: center; padding: 48px 24px 40px; background: linear-gradient(135deg, rgba(96,165,250,0.06) 0%, rgba(167,139,250,0.06) 50%, rgba(52,211,153,0.04) 100%); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 24px; position: relative; overflow: hidden; }
231
+ .about-hero::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle at 30% 40%, rgba(96,165,250,0.04) 0%, transparent 50%), radial-gradient(circle at 70% 60%, rgba(167,139,250,0.04) 0%, transparent 50%); pointer-events: none; }
232
+ .about-logo { width: 56px; height: 56px; background: linear-gradient(135deg, var(--accent), var(--purple)); border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 800; color: #fff; margin: 0 auto 16px; box-shadow: 0 4px 20px rgba(96,165,250,0.3); position: relative; }
233
+ .about-hero h2 { font-size: 26px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 8px; background: linear-gradient(135deg, var(--accent), var(--purple)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; position: relative; }
234
+ .about-desc { color: var(--text2); font-size: 14px; max-width: 500px; margin: 0 auto; line-height: 1.6; position: relative; }
235
+ .about-meta { display: flex; gap: 20px; justify-content: center; margin-top: 16px; font-size: 12px; color: var(--text3); position: relative; }
236
+ .about-meta span { display: flex; align-items: center; gap: 4px; }
237
+ .feature-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; margin-bottom: 24px; }
238
+ .feature-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; transition: transform var(--transition), border-color var(--transition), box-shadow var(--transition); }
239
+ .feature-card:hover { transform: translateY(-2px); border-color: var(--border-light); box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
240
+ .feature-card .fc-icon { font-size: 22px; margin-bottom: 8px; }
241
+ .feature-card h4 { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
242
+ .feature-card p { font-size: 12px; color: var(--text3); line-height: 1.5; }
243
+ .link-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
244
+ .link-card { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-sm); transition: var(--transition); cursor: pointer; text-decoration: none; color: var(--text); }
245
+ .link-card:hover { border-color: var(--accent); background: var(--accent-glow); transform: translateY(-1px); }
246
+ .link-card .lc-icon { font-size: 20px; flex-shrink: 0; }
247
+ .link-card .lc-title { font-size: 13px; font-weight: 600; }
248
+ .link-card .lc-sub { font-size: 11px; color: var(--text3); }
249
+ .footer { padding: 20px 28px; text-align: center; border-top: 1px solid var(--border); }
250
+ .footer-inner { max-width: 1280px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: var(--text3); flex-wrap: wrap; gap: 8px; }
251
+ .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
252
+ .btn { position: relative; overflow: hidden; }
253
+ .btn::after { content: ''; position: absolute; inset: 0; background: radial-gradient(circle at var(--x,50%) var(--y,50%), rgba(255,255,255,0.15), transparent 60%); opacity: 0; transition: opacity 0.3s; pointer-events: none; }
254
+ .btn:active::after { opacity: 1; }
255
+ table tbody tr { transition: all var(--transition); border-left: 3px solid transparent; }
256
+ table tbody tr:hover { border-left-color: var(--accent); }
257
+ table tbody tr:hover td { background: var(--accent-glow); }
258
+ .kbd-help { position: fixed; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(6px); z-index: 150; display: none; align-items: center; justify-content: center; }
259
+ .kbd-help.active { display: flex; }
260
+ .kbd-help-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px 32px; min-width: 320px; box-shadow: var(--shadow); animation: fadeIn 0.2s; }
261
+ .kbd-help-card h3 { font-size: 15px; font-weight: 600; margin-bottom: 16px; }
262
+ .kbd-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 13px; }
263
+ .kbd-key { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 24px; padding: 0 8px; background: var(--bg4); border: 1px solid var(--border); border-radius: 5px; font-size: 12px; font-weight: 600; font-family: var(--mono); color: var(--text); }
264
+ .kbd-desc { color: var(--text2); }
265
+ @media (max-width: 768px) {
266
+ .header { padding: 12px 16px; flex-wrap: wrap; gap: 12px; }
267
+ .nav { padding: 0 8px; }
268
+ .nav-item { padding: 10px 12px; font-size: 12px; }
269
+ .content { padding: 16px; }
270
+ .grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
271
+ .card { padding: 16px; }
272
+ .card .value { font-size: 26px; }
273
+ .modal { width: 95%; padding: 20px; max-height: 85vh; }
274
+ .btn { min-height: 40px; padding: 8px 14px; }
275
+ table { font-size: 12px; }
276
+ td, th { padding: 8px 10px; }
277
+ .section h2 { font-size: 18px; }
278
+ }
279
+ @media (max-width: 480px) {
280
+ .grid { grid-template-columns: 1fr; }
281
+ .header-right { width: 100%; justify-content: flex-end; }
282
+ .refresh-time { display: none; }
283
+ .bar-label { width: 90px; }
284
+ .bar-val { width: 70px; }
285
+ }
286
+ .footer-links { display: flex; gap: 16px; }
287
+ .footer-links a { color: var(--text3); text-decoration: none; transition: var(--transition); }
288
+ .footer-links a:hover { color: var(--accent); }
289
+ </style>
290
+ </head>
291
+ <body>
292
+ <div class="progress-bar" id="progressBar"><div class="track"></div></div>
293
+ <div class="action-overlay" id="actionOverlay"><div class="action-card"><div class="spinner"></div><div class="action-label" id="actionLabel">Processing...</div></div></div>
294
+ <div class="toast-container" id="toasts"></div>
295
+ <div class="modal-overlay" id="settingsModal"><div class="modal">
296
+ <div class="card-header"><h3>Scanner Settings</h3><button class="modal-close" onclick="$('#settingsModal').classList.remove('active')">&#10005;</button></div>
297
+ <p>Configure thresholds for problematic content detection.</p>
298
+ <div style="display:grid;gap:16px;margin-bottom:24px">
299
+ <div>
300
+ <label style="display:block;margin-bottom:6px;font-size:13px;font-weight:500">Min Text Size (chars)</label>
301
+ <input type="number" id="setConfigMinText" class="search-input" placeholder="500000">
302
+ <div style="font-size:11px;color:var(--text3);margin-top:4px">Content larger than this will be flagged/removed. Default: 500000</div>
303
+ </div>
304
+ <div>
305
+ <label style="display:block;margin-bottom:6px;font-size:13px;font-weight:500">Min Base64 Size (chars)</label>
306
+ <input type="number" id="setConfigMinBase64" class="search-input" placeholder="100000">
307
+ <div style="font-size:11px;color:var(--text3);margin-top:4px">Base64 data larger than this will be flagged/removed. Default: 100000</div>
308
+ </div>
309
+ </div>
310
+ <div class="modal-actions">
311
+ <button class="btn" onclick="$('#settingsModal').classList.remove('active')">Cancel</button>
312
+ <button class="btn btn-primary" onclick="saveSettings()">Save Changes</button>
313
+ </div>
314
+ </div></div>
315
+ <div class="spark-tooltip" id="sparkTip" style="display:none"><span class="st-date" id="stDate"></span><span class="st-val" id="stVal"></span></div>
316
+ <div class="header">
317
+ <div class="logo">
318
+ <div class="logo-icon">C</div>
319
+ <span class="header-dot" id="headerDot"></span>
320
+ <h1>Claude Code Toolkit <span>v1.2.0</span></h1>
321
+ </div>
322
+ <div class="header-stats" id="headerStats" style="display:flex;gap:16px;align-items:center;margin-left:24px;padding-left:24px;border-left:1px solid var(--border)">
323
+ <div class="hstat" style="text-align:center" title="Total sessions"><div style="font-size:16px;font-weight:700;color:var(--accent)" id="hsSessions">-</div><div style="font-size:9px;color:var(--text3);text-transform:uppercase">Sessions</div></div>
324
+ <div class="hstat" style="text-align:center" title="Unique projects"><div style="font-size:16px;font-weight:700;color:var(--green)" id="hsProjects">-</div><div style="font-size:9px;color:var(--text3);text-transform:uppercase">Projects</div></div>
325
+ <div class="hstat" style="text-align:center" title="Total storage used"><div style="font-size:16px;font-weight:700;color:var(--yellow)" id="hsStorage">-</div><div style="font-size:9px;color:var(--text3);text-transform:uppercase">Storage</div></div>
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
+ </div>
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)">
331
+ </div>
332
+ <div class="auto-refresh">
333
+ <label>Auto-refresh</label>
334
+ <button class="toggle" id="autoToggle" onclick="toggleAutoRefresh()"></button>
335
+ </div>
336
+ <button class="theme-btn" id="themeBtn" onclick="toggleTheme()" title="Toggle light/dark theme">
337
+ <svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
338
+ <svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
339
+ </button>
340
+ <button class="btn" style="margin-right:8px" onclick="showSettings()">Settings</button>
341
+ <span class="refresh-time" id="lastRefresh"></span>
342
+ <button class="btn" onclick="refreshCurrent()">Refresh</button>
343
+ </div>
344
+ </div>
345
+ <div class="nav" id="nav">
346
+ <div class="nav-item active" data-tab="overview">Overview</div>
347
+ <div class="nav-item" data-tab="storage">Storage</div>
348
+ <div class="nav-item" data-tab="sessions">Sessions</div>
349
+ <div class="nav-item" data-tab="security">Security</div>
350
+ <div class="nav-item" data-tab="traces">Traces</div>
351
+ <div class="nav-item" data-tab="mcp">MCP</div>
352
+ <div class="nav-item" data-tab="logs">Logs</div>
353
+ <div class="nav-item" data-tab="config">Config</div>
354
+ <div class="nav-item" data-tab="analytics">Analytics</div>
355
+ <div class="nav-item" data-tab="backups">Backups</div>
356
+ <div class="nav-item" data-tab="context">Context</div>
357
+ <div class="nav-item" data-tab="maintenance">Maintenance</div>
358
+ <div class="nav-item" data-tab="snapshots">Snapshots</div>
359
+ <div class="nav-item" data-tab="about">About</div>
360
+ </div>
361
+ <div class="content">
362
+ <div class="section active" id="sec-overview"><div class="loading"><div class="spinner"></div><div>Loading overview...</div></div></div>
363
+ <div class="section" id="sec-storage"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
364
+ <div class="section" id="sec-sessions"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
365
+ <div class="section" id="sec-security"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
366
+ <div class="section" id="sec-traces"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
367
+ <div class="section" id="sec-mcp"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
368
+ <div class="section" id="sec-logs"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
369
+ <div class="section" id="sec-config"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
370
+ <div class="section" id="sec-analytics"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
371
+ <div class="section" id="sec-backups"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
372
+ <div class="section" id="sec-context"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
373
+ <div class="section" id="sec-maintenance"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
374
+ <div class="section" id="sec-snapshots">
375
+ <div class="card-header">
376
+ <h3>Storage Snapshots</h3>
377
+ <button class="btn" onclick="doTakeSnapshot()">+ Take Snapshot</button>
378
+ </div>
379
+ <p>Track storage usage over time. Compare snapshots to see what changed.</p>
380
+ <div class="table-container">
381
+ <table class="data-table">
382
+ <thead><tr><th>Date</th><th>Label</th><th>Total Size</th><th>ID</th><th>Actions</th></tr></thead>
383
+ <tbody id="snapshotTableBody"></tbody>
384
+ </table>
385
+ </div>
386
+ </div>
387
+ <div class="section" id="sec-about"><div class="loading"><div class="spinner"></div><div>Loading...</div></div></div>
388
+ <div class="section" id="sec-search"><div class="loading"><div class="spinner"></div><div>Searching...</div></div></div>
389
+ </div>
390
+ <div class="footer">
391
+ <div class="footer-inner">
392
+ <span>Claude Code Toolkit v1.2.0 &mdash; MIT License</span>
393
+ <div class="footer-links">
394
+ <a href="https://github.com/asifkibria/claude-code-toolkit" target="_blank">GitHub</a>
395
+ <a href="https://github.com/asifkibria/claude-code-toolkit/issues" target="_blank">Issues</a>
396
+ <a href="https://www.npmjs.com/package/@asifkibria/claude-code-toolkit" target="_blank">npm</a>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ <div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
401
+ <div class="modal">
402
+ <button class="modal-close" onclick="closeModal()" aria-label="Close">&times;</button>
403
+ <h3 id="mTitle"></h3>
404
+ <div id="mBodyWrap"><p id="mBody"></p></div>
405
+ <div class="modal-actions">
406
+ <button class="btn" id="mCancel" onclick="closeModal()">Cancel</button>
407
+ <button class="btn btn-primary" id="mConfirm">Confirm</button>
408
+ </div>
409
+ </div>
410
+ </div>
411
+ <div class="kbd-help" id="kbdHelp" onclick="if(event.target===this)this.classList.remove('active')">
412
+ <div class="kbd-help-card">
413
+ <h3>Keyboard Shortcuts</h3>
414
+ <div class="kbd-row"><span class="kbd-desc">Switch tabs</span><span><span class="kbd-key">1</span> &ndash; <span class="kbd-key">0</span></span></div>
415
+ <div class="kbd-row"><span class="kbd-desc">Refresh current tab</span><span class="kbd-key">R</span></div>
416
+ <div class="kbd-row"><span class="kbd-desc">Close modal / overlay</span><span class="kbd-key">Esc</span></div>
417
+ <div class="kbd-row"><span class="kbd-desc">Show this help</span><span class="kbd-key">?</span></div>
418
+ <div style="margin-top:14px;text-align:center;font-size:12px;color:var(--text3)">Press any key to dismiss</div>
419
+ </div>
420
+ </div>
421
+ <script>
422
+ const $ = s => document.querySelector(s);
423
+ const $$ = s => document.querySelectorAll(s);
424
+
425
+ function esc(s) { if (s == null) return ''; return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
426
+ function fmtB(b) { if(b<1024)return b+' B'; if(b<1048576)return(b/1024).toFixed(1)+' KB'; if(b<1073741824)return(b/1048576).toFixed(1)+' MB'; return(b/1073741824).toFixed(1)+' GB'; }
427
+ function fmtK(n) { if(n>1e6)return(n/1e6).toFixed(1)+'M'; if(n>1e3)return(n/1e3).toFixed(1)+'K'; return String(n); }
428
+ function badge(t,c) { return '<span class="badge b-'+c+'">'+esc(t)+'</span>'; }
429
+ function set(el,h) { if(typeof el==='string') el=$(el); el.innerHTML=h; }
430
+ function ago(d) { const ms=Date.now()-new Date(d).getTime(); const m=Math.floor(ms/60000); if(m<60)return m+'m ago'; const hr=Math.floor(m/60); if(hr<24)return hr+'h ago'; return Math.floor(hr/24)+'d ago'; }
431
+
432
+ function toast(msg, type) {
433
+ const d = document.createElement('div');
434
+ d.className = 'toast toast-' + (type||'info');
435
+ d.textContent = msg;
436
+ $('#toasts').appendChild(d);
437
+ setTimeout(() => d.remove(), 4000);
438
+ }
439
+
440
+ function emptyState(icon, title, sub, btnText, btnAction) {
441
+ let h='<div class="empty-state"><div class="es-icon">'+icon+'</div><div class="es-title">'+esc(title)+'</div><div class="es-sub">'+esc(sub)+'</div>';
442
+ if(btnText&&btnAction) h+='<button class="btn btn-primary" onclick="'+btnAction+'">'+esc(btnText)+'</button>';
443
+ return h+'</div>';
444
+ }
445
+ function staggerCards(el) {
446
+ if(typeof el==='string') el=$(el);
447
+ if(!el) return;
448
+ el.querySelectorAll('.card').forEach((c,i)=>{c.classList.add('stagger');c.style.animationDelay=(i*50)+'ms';});
449
+ }
450
+ function updateNavBadges(data) {
451
+ $$('.nav-item .nav-badge').forEach(b=>b.remove());
452
+ if(!data) return;
453
+ const addBadge=(tab,count,cls)=>{
454
+ if(count<=0) return;
455
+ const el=document.querySelector('.nav-item[data-tab="'+tab+'"]');
456
+ if(!el) return;
457
+ const b=document.createElement('span');
458
+ b.className='nav-badge '+cls;
459
+ b.textContent=count>99?'99+':String(count);
460
+ el.appendChild(b);
461
+ };
462
+ addBadge('security',data.secFindings||0,'nav-badge-red');
463
+ addBadge('sessions',data.corrupted||0,'nav-badge-yellow');
464
+ addBadge('maintenance',data.maintenanceActions||0,'nav-badge-blue');
465
+ addBadge('traces',data.criticalTraces||0,'nav-badge-red');
466
+ }
467
+
468
+ const cache = {};
469
+ 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; }
471
+ catch(e) { console.error('API:',ep,e); return cache[ep]||null; }
472
+ }
473
+ async function post(ep, body) {
474
+ try {
475
+ const r = await fetch('/api/action/'+ep, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body||{}) });
476
+ if (!r.ok) { console.error('API error:', r.status, r.statusText); return { success: false, error: 'HTTP ' + r.status }; }
477
+ return r.json();
478
+ } catch (e) { console.error('Post error:', e); return { success: false, error: e.message }; }
479
+ }
480
+
481
+ let modalCb = null;
482
+ function showModal(title, body, btnText, cb, isHtml) {
483
+ $('#mTitle').textContent = title;
484
+ if(isHtml) { set('#mBodyWrap', body); }
485
+ else { set('#mBodyWrap', '<p id="mBody"></p>'); $('#mBody').textContent = body; }
486
+ const btn = $('#mConfirm');
487
+ btn.textContent = btnText || 'Confirm';
488
+ btn.className = 'btn ' + (/Delete|Wipe|Redact/.test(btnText) ? 'btn-danger' : 'btn-primary');
489
+ modalCb = cb;
490
+ btn.onclick = async () => { const fn = modalCb; closeModal(); if(fn) await fn(); };
491
+ $('#mCancel').style.display = cb ? '' : 'none';
492
+ $('#modal').classList.add('active');
493
+ }
494
+ function closeModal() { $('#modal').classList.remove('active'); modalCb=null; }
495
+
496
+ // ===== Exclusion Management (localStorage) =====
497
+ const EXCLUSION_KEY = 'cct_trace_exclusions';
498
+ const DEFAULT_EXCLUSIONS = [
499
+ { id: 'default-file-history', type: 'category', value: 'file-history', description: 'Preserve file edit history for reverting changes' },
500
+ { id: 'default-conversations', type: 'category', value: 'conversations', description: 'Preserve conversation history' }
501
+ ];
502
+
503
+ function loadExclusions() {
504
+ try {
505
+ const raw = localStorage.getItem(EXCLUSION_KEY);
506
+ if (!raw) {
507
+ const config = { version: 1, exclusions: DEFAULT_EXCLUSIONS, lastUpdated: new Date().toISOString() };
508
+ saveExclusions(config);
509
+ return config;
510
+ }
511
+ return JSON.parse(raw);
512
+ } catch { return { version: 1, exclusions: DEFAULT_EXCLUSIONS, lastUpdated: new Date().toISOString() }; }
513
+ }
514
+
515
+ function saveExclusions(config) {
516
+ config.lastUpdated = new Date().toISOString();
517
+ localStorage.setItem(EXCLUSION_KEY, JSON.stringify(config));
518
+ }
519
+
520
+ function addExclusion(type, value, description) {
521
+ const config = loadExclusions();
522
+ config.exclusions.push({
523
+ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
524
+ type, value, description,
525
+ createdAt: new Date().toISOString()
526
+ });
527
+ saveExclusions(config);
528
+ }
529
+
530
+ function removeExclusion(id) {
531
+ const config = loadExclusions();
532
+ config.exclusions = config.exclusions.filter(e => e.id !== id);
533
+ saveExclusions(config);
534
+ }
535
+
536
+ function showExclusionManager() {
537
+ const config = loadExclusions();
538
+ let body = '<div class="detail-panel">';
539
+ body += '<div style="font-size:14px;font-weight:600;margin-bottom:12px">Protected from cleanup/wipe operations:</div>';
540
+ if (config.exclusions.length === 0) {
541
+ body += '<div style="text-align:center;color:var(--text3);padding:20px">No exclusions configured</div>';
542
+ } else {
543
+ body += '<div style="max-height:200px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius-sm)">';
544
+ config.exclusions.forEach(exc => {
545
+ const typeCol = exc.type === 'category' ? 'var(--blue)' : exc.type === 'project' ? 'var(--green)' : 'var(--orange)';
546
+ body += '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border)">';
547
+ body += '<div><span style="background:'+typeCol+';color:#fff;padding:2px 6px;border-radius:4px;font-size:10px;margin-right:8px">'+esc(exc.type)+'</span>' + esc(exc.value);
548
+ if (exc.description) body += '<span style="color:var(--text3);font-size:11px;margin-left:8px">' + esc(exc.description) + '</span>';
549
+ body += '</div>';
550
+ body += '<button class="btn btn-sm" onclick="removeExclusionAndRefresh(\\''+exc.id+'\\')" style="padding:2px 8px">×</button>';
551
+ body += '</div>';
552
+ });
553
+ body += '</div>';
554
+ }
555
+ body += '<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:16px">';
556
+ body += '<div style="font-size:13px;font-weight:600;margin-bottom:8px">Add Exclusion:</div>';
557
+ body += '<div style="display:flex;gap:8px;flex-wrap:wrap">';
558
+ body += '<select id="excType" class="search-input" style="width:120px"><option value="category">Category</option><option value="project">Project</option><option value="path">Path</option></select>';
559
+ body += '<input type="text" id="excValue" class="search-input" style="flex:1;min-width:200px" placeholder="Value (e.g., file-history, my-project, projects/work/*)">';
560
+ body += '<button class="btn btn-primary btn-sm" onclick="addExclusionFromForm()">Add</button>';
561
+ body += '</div></div></div>';
562
+ showModal('Manage Exclusions', body, 'Done', null, true);
563
+ }
564
+
565
+ function addExclusionFromForm() {
566
+ const type = $('#excType').value;
567
+ const value = $('#excValue').value.trim();
568
+ if (!value) { toast('Please enter a value', 'error'); return; }
569
+ addExclusion(type, value);
570
+ showExclusionManager();
571
+ toast('Exclusion added', 'success');
572
+ }
573
+
574
+ function removeExclusionAndRefresh(id) {
575
+ removeExclusion(id);
576
+ showExclusionManager();
577
+ toast('Exclusion removed', 'info');
578
+ }
579
+
580
+ // ===== Multi-Step Wipe Confirmation =====
581
+ let wipeState = { step: 1, preview: null, confirmPhrase: 'WIPE ALL', userInput: '' };
582
+
583
+ function showWipeStep1() {
584
+ const p = wipeState.preview;
585
+ let body = '<div class="detail-panel">';
586
+ body += '<div style="margin-bottom:16px;font-size:14px;color:var(--text2)">This operation will permanently delete:</div>';
587
+ body += '<div style="max-height:200px;overflow-y:auto;margin-bottom:16px">';
588
+ body += '<table style="width:100%"><tr><th style="text-align:left">Category</th><th>Sensitivity</th><th>Files</th><th>Size</th></tr>';
589
+ (p.byCategory || []).forEach(c => {
590
+ const col = c.sensitivity === 'critical' ? 'var(--red)' : c.sensitivity === 'high' ? 'var(--orange)' : c.sensitivity === 'medium' ? 'var(--yellow)' : 'var(--green)';
591
+ body += '<tr><td>' + esc(c.name) + '</td><td><span style="color:'+col+';font-weight:600">' + c.sensitivity.toUpperCase() + '</span></td><td style="text-align:center">' + c.fileCount + '</td><td style="text-align:right">' + fmtB(c.totalSize) + '</td></tr>';
592
+ });
593
+ body += '</table></div>';
594
+ body += '<div style="font-size:14px;font-weight:600;margin-bottom:8px">Total: ' + p.summary.totalFiles + ' files (' + fmtB(p.summary.totalSize) + ')</div>';
595
+ body += '<div style="font-size:12px;color:var(--text3);margin-bottom:16px">Critical: ' + p.summary.criticalFiles + ' | High: ' + p.summary.highFiles + '</div>';
596
+ if (p.preserved && p.preserved.totalPreserved > 0) {
597
+ body += '<div style="background:var(--green-bg);border:1px solid rgba(34,197,94,0.3);border-radius:var(--radius-sm);padding:12px;margin-bottom:16px">';
598
+ body += '<div style="font-size:12px;font-weight:600;color:var(--green);margin-bottom:8px">✓ Protected by exclusions: ' + p.preserved.totalPreserved + ' files</div>';
599
+ p.preserved.byExclusion.slice(0, 5).forEach(exc => {
600
+ body += '<div style="font-size:11px;color:var(--text2)">• ' + esc(exc.exclusion.type) + ' "' + esc(exc.exclusion.value) + '": ' + exc.matchedFiles + ' files</div>';
601
+ });
602
+ body += '</div>';
603
+ }
604
+ body += '<div style="display:flex;gap:10px;margin-top:16px">';
605
+ body += '<button class="btn btn-sm" onclick="closeModal();showExclusionManager()">Manage Exclusions</button>';
606
+ body += '</div></div>';
607
+ showModal('SECURE WIPE - Step 1 of 3', body, 'Next: Impact Review →', showWipeStep2, true);
608
+ }
609
+
610
+ function showWipeStep2() {
611
+ const p = wipeState.preview;
612
+ let body = '<div class="detail-panel" style="background:rgba(239,68,68,0.05);border-color:rgba(239,68,68,0.2)">';
613
+ body += '<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;font-size:16px;font-weight:600;color:var(--red)">⚠ IMPACT WARNING</div>';
614
+ body += '<div style="font-size:14px;color:var(--text);line-height:1.8">';
615
+ body += '<p style="margin-bottom:12px"><strong>This action will:</strong></p>';
616
+ body += '<ul style="padding-left:20px;margin-bottom:16px;list-style-type:disc">';
617
+ (p.byCategory || []).filter(c => c.sensitivity === 'critical' || c.sensitivity === 'high').slice(0, 5).forEach(c => {
618
+ body += '<li style="margin-bottom:8px">' + esc(c.impactWarning) + ' <span style="color:var(--text3)">(' + c.fileCount + ' files)</span></li>';
619
+ });
620
+ body += '</ul>';
621
+ body += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;font-size:13px">';
622
+ body += '<strong style="color:var(--red)">⚠ CANNOT BE UNDONE</strong><br>';
623
+ body += 'Files are securely overwritten with zeros before deletion. No recovery is possible.';
624
+ body += '</div></div></div>';
625
+ body += '<div style="margin-top:16px"><button class="btn" onclick="showWipeStep1()">← Back to Preview</button></div>';
626
+ showModal('SECURE WIPE - Step 2 of 3', body, 'Next: Confirm →', showWipeStep3, true);
627
+ }
628
+
629
+ function showWipeStep3() {
630
+ let body = '<div class="detail-panel">';
631
+ body += '<div style="margin-bottom:16px;font-size:14px;font-weight:600;color:var(--red)">⚠ FINAL CONFIRMATION</div>';
632
+ body += '<p style="margin-bottom:12px;font-size:14px">Type "<strong style="color:var(--red)">' + wipeState.confirmPhrase + '</strong>" to confirm:</p>';
633
+ body += '<input type="text" id="wipeConfirmInput" class="search-input" style="width:100%;max-width:100%;margin-bottom:16px" placeholder="Type confirmation phrase..." oninput="updateWipeButton(this.value)">';
634
+ body += '<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">';
635
+ body += '<input type="checkbox" id="keepSettingsCheck" checked> Preserve settings.json and CLAUDE.md';
636
+ body += '</label>';
637
+ body += '</div>';
638
+ body += '<div style="margin-top:16px"><button class="btn" onclick="showWipeStep2()">← Back to Impact</button></div>';
639
+ showModal('SECURE WIPE - Step 3 of 3', body, 'Wipe All Traces', executeWipe, true);
640
+ $('#mConfirm').disabled = true;
641
+ $('#mConfirm').style.opacity = '0.5';
642
+ }
643
+
644
+ function updateWipeButton(value) {
645
+ wipeState.userInput = value.toUpperCase().trim();
646
+ const match = wipeState.userInput === wipeState.confirmPhrase;
647
+ $('#mConfirm').disabled = !match;
648
+ $('#mConfirm').style.opacity = match ? '1' : '0.5';
649
+ }
650
+
651
+ async function executeWipe() {
652
+ if (wipeState.userInput !== wipeState.confirmPhrase) return;
653
+ showProgress('Securely wiping all traces...');
654
+ const keepSettings = $('#keepSettingsCheck')?.checked ?? true;
655
+ const exclusions = loadExclusions().exclusions;
656
+ const r = await post('wipe-traces', { confirm: true, keepSettings, exclusions });
657
+ if (r.success) {
658
+ showResult(true, 'Secure Wipe Complete',
659
+ '<span>Files wiped: <strong>' + r.filesWiped + '</strong></span>' +
660
+ '<span>Freed: <strong>' + fmtB(r.bytesFreed) + '</strong></span>' +
661
+ '<span>Categories: ' + (r.categoriesWiped?.join(', ') || 'all') + '</span>' +
662
+ (r.preserved?.length ? '<span class="c-green">Preserved: ' + r.preserved.join(', ') + '</span>' : ''),
663
+ ['traces', 'overview'], r.categoriesWiped);
664
+ } else {
665
+ showResult(false, 'Wipe Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>');
666
+ }
667
+ }
668
+
669
+ let autoTimer = null;
670
+ function toggleAutoRefresh() {
671
+ const btn = $('#autoToggle');
672
+ if(autoTimer) { clearInterval(autoTimer); autoTimer=null; btn.classList.remove('on'); }
673
+ else { btn.classList.add('on'); autoTimer=setInterval(refreshCurrent, 30000); }
674
+ }
675
+
676
+ function toggleTheme() {
677
+ const html = document.documentElement;
678
+ html.classList.toggle('light');
679
+ html.classList.toggle('dark');
680
+ const isLight = html.classList.contains('light');
681
+ localStorage.setItem('cct-dashboard-theme', isLight ? 'light' : 'dark');
682
+ }
683
+
684
+ (function initTheme() {
685
+ const saved = localStorage.getItem('cct-dashboard-theme');
686
+ const html = document.documentElement;
687
+ if(saved === 'light') { html.classList.add('light'); html.classList.remove('dark'); }
688
+ else if(saved === 'dark') { html.classList.add('dark'); html.classList.remove('light'); }
689
+ })();
690
+
691
+ let currentTab = 'overview';
692
+ const tabOrder=['overview','storage','sessions','security','traces','mcp','logs','config','analytics','backups','context','maintenance','snapshots','about'];
693
+ $('#nav').addEventListener('click', e => {
694
+ const t = e.target.dataset?.tab; if(!t) return;
695
+ $$('.nav-item').forEach(n=>n.classList.remove('active'));
696
+ e.target.classList.add('active');
697
+ $$('.section').forEach(s=>s.classList.remove('active'));
698
+ $('#sec-'+t).classList.add('active');
699
+ currentTab = t;
700
+ loadTab(t);
701
+ window.scrollTo({top:0,behavior:'smooth'});
702
+ });
703
+ document.addEventListener('keydown', e => {
704
+ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;
705
+ const kbdHelp=$('#kbdHelp');
706
+ if(kbdHelp.classList.contains('active')) { kbdHelp.classList.remove('active'); return; }
707
+ if(e.key==='Escape') { closeModal(); hideProgress(); return; }
708
+ if(e.key==='?'&&!e.metaKey&&!e.ctrlKey) { kbdHelp.classList.add('active'); return; }
709
+ if(e.key.toLowerCase()==='r'&&!e.metaKey&&!e.ctrlKey) { refreshCurrent(); return; }
710
+ const num=e.key==='0'?10:parseInt(e.key,10);
711
+ if(num>=1&&num<=11&&!e.metaKey&&!e.ctrlKey&&!e.altKey) { const t=tabOrder[num-1]; if(t) switchTab(t); }
712
+ });
713
+
714
+ const loaded = {};
715
+ function loadTab(t) { if(loaded[t]) return; loaded[t]=true; loaders[t]?.(); }
716
+ function refreshCurrent() { loaded[currentTab]=false; loadTab(currentTab); }
717
+ function refreshTime() { $('#lastRefresh').textContent = new Date().toLocaleTimeString(); }
718
+ function esc(s) { return s ? s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') : ''; }
719
+
720
+ let currentSearchQuery = '';
721
+ async function doGlobalSearch(q) {
722
+ if(!q) return;
723
+ currentSearchQuery = q;
724
+ document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
725
+ document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
726
+ const sec = document.getElementById('sec-search');
727
+ if(sec) sec.classList.add('active');
728
+ loadSearch();
729
+ }
730
+
731
+ async function loadSearch() {
732
+ const el = $('#sec-search');
733
+ set(el, '<div class="loading"><div class="spinner"></div><div>Searching for "'+esc(currentSearchQuery)+'"...</div></div>');
734
+ 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;
739
+ }
740
+ let h = '<h2>Search Results: "'+esc(currentSearchQuery)+'" ('+res.results.length+')</h2>';
741
+ h += '<div class="grid" style="grid-template-columns:1fr">';
742
+ 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>';
746
+ h += '</div>';
747
+ });
748
+ h += '</div>';
749
+ set(el, h);
750
+ } catch(e) {
751
+ set(el, '<div class="empty-state">Error: '+e.message+'</div>');
752
+ }
753
+ }
754
+
755
+ function healthRing(pct, color) {
756
+ const r=47, c=2*Math.PI*r, off=c-(pct/100)*c;
757
+ return '<div class="health-ring"><svg viewBox="0 0 110 110"><circle class="ring-bg" cx="55" cy="55" r="'+r+'"/><circle class="ring-fg" cx="55" cy="55" r="'+r+'" stroke="'+color+'" stroke-dasharray="'+c+'" stroke-dashoffset="'+off+'"/></svg><div class="score"><div class="score-val" style="color:'+color+'">'+pct+'</div><div class="score-lbl">Health</div></div></div>';
758
+ }
759
+
760
+ function sparklineSvg(data,width,height,color) {
761
+ if(!data||data.length<2) return '';
762
+ const max=Math.max(...data.map(d=>d.value));
763
+ const min=0;
764
+ const range=max-min||1;
765
+ const step=width/(data.length-1);
766
+ let path=\`M 0 \${ height - ((data[0].value - min) / range) * height } \`;
767
+ data.forEach((d,i)=>{const x=i*step;const y=height-((d.value-min)/range)*height;path+=\` L \${ x } \${ y } \`;});
768
+ const fillPath=\`\${ path } L \${ width } \${ height } L 0 \${ height } Z\`;
769
+ return \`<svg width="100%" height="\${height}" viewBox="0 0 \${width} \${height}" preserveAspectRatio="none" style="overflow:visible"><defs><linearGradient id="g-\${color.replace(/[^a-z0-9]/gi,'')}" x1="0" x2="0" y1="0" y2="1"><stop offset="0%" stop-color="\${color}" stop-opacity="0.2"/><stop offset="100%" stop-color="\${color}" stop-opacity="0"/></linearGradient></defs><path d="\${fillPath}" fill="url(#g-\${color.replace(/[^a-z0-9]/gi,'')})" stroke="none"/><path d="\${path}" fill="none" stroke="\${color}" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>\`;
770
+ }
771
+
772
+
773
+ async function loadOverview() {
774
+ const [ov, stor, sess, sec, tr] = await Promise.all([api('overview'), api('storage'), api('sessions'), api('security'), api('traces')]);
775
+ const el = $('#sec-overview');
776
+ if (!ov && !stor) { set(el, emptyState('&#128270;', 'Could not load data', 'Unable to fetch overview. Check that the .claude directory exists.', 'Retry', 'refreshCurrent()')); return; }
777
+ const sz = stor?.totalSize || 0, sc = sess?.length || 0, hc = sess?.filter(s => s.status === 'healthy').length || 0;
778
+ const cc = sess?.filter(s => s.status === 'corrupted').length || 0, sf = sec?.totalFindings || 0;
779
+ const tsz = tr?.totalSize || 0, tf = tr?.totalFiles || 0, ic = ov?.issueCount || 0;
780
+ const crit = tr?.criticalItems || 0;
781
+ const problems = ic + cc + sf;
782
+ const maxProblems = Math.max(sc, 10);
783
+ const healthPct = Math.max(0, Math.min(100, Math.round(100 - (problems / maxProblems) * 100)));
784
+ const hColor = healthPct >= 80 ? 'var(--green)' : healthPct >= 50 ? 'var(--yellow)' : 'var(--red)';
785
+ const hDot = $('#headerDot'); if (hDot) { hDot.style.background = hColor; hDot.style.boxShadow = '0 0 6px ' + hColor; hDot.classList.add('visible'); }
786
+ updateNavBadges({ secFindings: sf, corrupted: cc, maintenanceActions: ov?.maintenanceActions || 0, criticalTraces: crit });
787
+ const sysInfo = ov?.systemInfo;
788
+ const hsSessions = $('#hsSessions'); if (hsSessions) hsSessions.textContent = sc;
789
+ const hsProjects = $('#hsProjects'); if (hsProjects) hsProjects.textContent = sysInfo?.uniqueProjects || new Set(sess?.map(s => s.project) || []).size;
790
+ const hsStorage = $('#hsStorage'); if (hsStorage) hsStorage.textContent = fmtB(sz);
791
+ const hsIssues = $('#hsIssues'); if (hsIssues) { hsIssues.textContent = problems; hsIssues.style.color = problems > 0 ? 'var(--red)' : 'var(--green)'; }
792
+
793
+ let h = '<h2>Overview</h2>';
794
+ h += '<div class="action-bar">';
795
+ if (ic > 0) h += '<button class="btn btn-primary" onclick="doFixAll()">Fix All Issues (' + ic + ')</button>';
796
+ h += '<button class="btn" onclick="doCleanPreview()">Clean Directory</button>';
797
+ if (ov?.maintenanceActions > 0) h += '<button class="btn btn-warn" onclick="switchTab(\\'maintenance\\')">Maintenance (' + ov.maintenanceActions + ')</button>';
798
+ h += '</div>';
799
+ h += '<div class="grid">';
800
+ h += '<div class="card card-glow" style="grid-row:span 2;display:flex;flex-direction:column;align-items:center;justify-content:center">' + healthRing(healthPct, hColor) + '<div class="sub" style="margin-top:4px;text-align:center">' + (problems === 0 ? 'All systems healthy' : problems + ' issue(s)') + '</div></div>';
801
+ h += '<div class="card clickable-card" onclick="switchTab(\\'storage\\')"><div class="stat-icon">&#128230;</div><h3>Storage</h3><div class="value">' + fmtB(sz) + '</div><div class="sub">.claude directory</div></div>';
802
+ h += '<div class="card clickable-card" onclick="switchTab(\\'sessions\\')"><div class="stat-icon">&#128172;</div><h3>Sessions</h3><div class="value">' + sc + '</div><div class="sub">' + hc + ' healthy, ' + cc + ' corrupted</div></div>';
803
+ h += '<div class="card clickable-card" onclick="switchTab(\\'security\\')"><div class="stat-icon">&#128274;</div><h3>Secrets</h3><div class="value ' + (sf > 0 ? 'c-red' : 'c-green') + '">' + sf + '</div><div class="sub">in conversation data</div></div>';
804
+ h += '<div class="card clickable-card" onclick="switchTab(\\'traces\\')"><div class="stat-icon">&#128065;</div><h3>Traces</h3><div class="value">' + tf + '</div><div class="sub">' + fmtB(tsz) + ' on disk</div></div>';
805
+ h += '<div class="card clickable-card" onclick="switchTab(\\'backups\\')"><div class="stat-icon">&#128190;</div><h3>Backups</h3><div class="value">' + (ov?.backupCount || 0) + '</div><div class="sub">' + fmtB(ov?.backupSize || 0) + ' total</div></div>';
806
+ h += '<div class="card clickable-card" onclick="switchTab(\\'maintenance\\')"><div class="stat-icon">&#128451;</div><h3>Archive Candidates</h3><div class="value c-accent">' + (ov?.archiveCandidates || 0) + '</div><div class="sub">inactive &gt;30 days</div></div>';
807
+ h += '</div>';
808
+ set(el, h); staggerCards(el); refreshTime();
809
+ }
810
+
811
+ async function loadStorage() {
812
+ const [d, bk] = await Promise.all([api('storage'), api('backups')]);
813
+ const el = $('#sec-storage');
814
+ if (!d) { set(el, emptyState('&#128194;', 'Storage data unavailable', 'Could not analyze .claude directory storage.', 'Retry', 'refreshCurrent()')); return; }
815
+ const mx = Math.max(...d.categories.map(c => c.totalSize), 1);
816
+ const cols = ['#60a5fa', '#34d399', '#fbbf24', '#f87171', '#fb923c', '#a78bfa', '#38bdf8', '#4ade80'];
817
+ let h = '<h2>Storage</h2>';
818
+ h += '<div class="action-bar"><button class="btn btn-success" onclick="doCleanPreview()">Preview Cleanup</button><button class="btn btn-danger" onclick="doCleanExecute()">Clean Now</button></div>';
819
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#128194;</div><h3>Total Size</h3><div class="value">' + fmtB(d.totalSize) + '</div></div>';
820
+ const cleanable = d.categories.reduce((s, c) => s + c.cleanableSize, 0);
821
+ h += '<div class="card"><div class="stat-icon">&#9851;</div><h3>Cleanable</h3><div class="value c-green">' + fmtB(cleanable) + '</div><div class="sub">safe to remove</div></div>';
822
+ if (bk) { h += '<div class="card"><div class="stat-icon">&#128190;</div><h3>Backups</h3><div class="value">' + (bk.totalBackups || 0) + '</div><div class="sub">' + fmtB(bk.totalSize || 0) + ' <button class="btn btn-sm btn-danger" onclick="doDeleteBackups()" style="margin-left:8px">Clean</button></div></div>'; }
823
+ h += '</div>';
824
+ h += '<div class="card mt"><h3>By Category</h3><div class="bars">';
825
+ d.categories.filter(c => c.totalSize > 0).sort((a, b) => b.totalSize - a.totalSize).forEach((c, i) => {
826
+ const p = Math.max((c.totalSize / mx) * 100, 2);
827
+ h += '<div class="bar-row"><div class="bar-label">' + esc(c.name) + '</div><div class="bar-track"><div class="bar-fill" style="width:' + p + '%;background:' + cols[i % cols.length] + '"></div></div><div class="bar-val">' + fmtB(c.totalSize) + '</div></div>';
828
+ });
829
+ h += '</div></div>';
830
+ if (d.largestFiles?.length) {
831
+ h += '<div class="card mt"><h3>Largest Files</h3><table><tr><th>Project</th><th>File</th><th>Size</th></tr>';
832
+ d.largestFiles.slice(0, 10).forEach(f => {
833
+ const parts = f.path.split('/');
834
+ const fileName = parts.pop() || '';
835
+ const projPart = parts.find(p => p.startsWith('-')) || '';
836
+ const projPath = projPart.replace(/^-/, '/').replace(/-/g, '/');
837
+ const projParts = projPath.split('/').filter(Boolean);
838
+ const projName = projParts.length >= 2 ? projParts.slice(-2).join('/') : projParts[projParts.length - 1] || 'Unknown';
839
+ h += '<tr><td style="max-width:120px;overflow:hidden;text-overflow:ellipsis" title="' + esc(projPath) + '">' + esc(projName) + '</td><td class="mono" title="' + esc(f.path) + '" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">' + esc(fileName) + '</td><td>' + fmtB(f.size) + '</td></tr>';
840
+ });
841
+ h += '</table></div>';
842
+ }
843
+ if (d.recommendations?.length) { h += '<div class="card mt"><h3>Recommendations</h3>'; d.recommendations.forEach(r => { h += '<div style="padding:6px 0;font-size:13px;color:var(--text2)">' + esc(r) + '</div>'; }); h += '</div>'; }
844
+ set(el, h); staggerCards(el); refreshTime();
845
+ }
846
+
847
+ let allSessions = [];
848
+ let sessionSort = { col: 'modified', dir: 'desc' };
849
+ function filterSessions(q) {
850
+ const el = $('#sessionTableBody');
851
+ if (!el) return;
852
+ const rows = el.querySelectorAll('tr[data-sid]');
853
+ const lq = q.toLowerCase();
854
+ rows.forEach(r => {
855
+ const txt = (r.getAttribute('data-sid') + ' ' + r.getAttribute('data-proj') + ' ' + r.getAttribute('data-status')).toLowerCase();
856
+ r.style.display = txt.includes(lq) ? '' : 'none';
857
+ });
858
+ }
859
+ function sortSessions(col) {
860
+ if (sessionSort.col === col) sessionSort.dir = sessionSort.dir === 'asc' ? 'desc' : 'asc';
861
+ else { sessionSort.col = col; sessionSort.dir = 'desc'; }
862
+ renderSessionTable();
863
+ }
864
+ function statusOrder(s) { return s === 'corrupted' ? 0 : s === 'orphaned' ? 1 : s === 'empty' ? 2 : 3; }
865
+ function parseProjectPath(rawPath) {
866
+ if (!rawPath) return { name: 'Unknown', fullPath: '' };
867
+ let fullPath = rawPath;
868
+ if (rawPath.startsWith('-')) {
869
+ fullPath = rawPath.replace(/^-/, '/').replace(/-/g, '/');
870
+ }
871
+ const parts = fullPath.split('/').filter(Boolean);
872
+ const name = parts.length >= 2 ? parts.slice(-2).join('/') : parts[parts.length - 1] || 'Unknown';
873
+ return { name, fullPath };
874
+ }
875
+ function renderSessionTable() {
876
+ const d = [...allSessions];
877
+ const { col, dir } = sessionSort;
878
+ d.sort((a, b) => {
879
+ let v;
880
+ if (col === 'messages') v = (a.messageCount || 0) - (b.messageCount || 0);
881
+ else if (col === 'size') v = (a.sizeBytes || 0) - (b.sizeBytes || 0);
882
+ else if (col === 'status') v = statusOrder(a.status) - statusOrder(b.status);
883
+ else if (col === 'modified') v = new Date(a.modified || 0).getTime() - new Date(b.modified || 0).getTime();
884
+ else v = 0;
885
+ return dir === 'asc' ? v : -v;
886
+ });
887
+ const sb = s => { if (s === 'healthy') return badge('healthy', 'green'); if (s === 'corrupted') return badge('corrupted', 'red'); if (s === 'empty') return badge('empty', 'yellow'); return badge(s, 'blue'); };
888
+ const sc = (c) => c === col ? (dir === 'asc' ? ' asc' : ' desc') : '';
889
+ let h = '<tr><th>ID</th><th class="sort-header' + sc('status') + '" onclick="sortSessions(\\'status\\')">Status</th><th class="sort-header' + sc('messages') + '" onclick="sortSessions(\\'messages\\')">Messages</th><th class="sort-header' + sc('size') + '" onclick="sortSessions(\\'size\\')">Size</th><th style="min-width:200px">Project</th><th class="sort-header' + sc('modified') + '" onclick="sortSessions(\\'modified\\')">Last Active</th><th>Actions</th></tr>';
890
+ h += '<tbody id="sessionTableBody">';
891
+ d.slice(0, 100).forEach(s => {
892
+ const sid = esc(s.id.slice(0, 12));
893
+ const proj = parseProjectPath(s.projectPath || s.project);
894
+ const projDisplay = '<div class="project-cell" style="max-width:280px"><span class="project-name" style="font-weight:600;color:var(--accent)">' + esc(proj.name) + '</span>' + (proj.fullPath ? '<span class="project-path" style="display:block;font-size:10px;color:var(--text3);word-break:break-all;margin-top:2px" title="' + esc(proj.fullPath) + '">' + esc(proj.fullPath) + '</span>' : '') + '</div>';
895
+ let acts = '';
896
+ if (s.status === 'corrupted') acts += '<button class="btn btn-sm btn-danger" onclick="doRepair(\\''+esc(s.id)+'\\')">Repair</button> ';
897
+ acts += '<button class="btn btn-sm" onclick="doExtract(\\''+esc(s.id)+'\\')">Extract</button> ';
898
+ acts += '<button class="btn btn-sm btn-primary" onclick="doAudit(\\''+esc(s.id)+'\\')">Audit</button>';
899
+ h += '<tr data-sid="' + esc(s.id) + '" data-proj="' + esc(s.project || '') + '" data-status="' + esc(s.status) + '"><td class="mono">' + sid + '</td><td>' + sb(s.status) + '</td><td>' + s.messageCount + '</td><td>' + fmtB(s.sizeBytes) + '</td><td>' + projDisplay + '</td><td>' + ago(s.modified) + '</td><td>' + acts + '</td></tr>';
900
+ });
901
+ if (d.length > 100) h += '<tr><td colspan="7" style="text-align:center;color:var(--text3)">...and ' + (d.length - 100) + ' more</td></tr>';
902
+ h += '</tbody>';
903
+ const tbl = $('#sessionTable'); if (tbl) set(tbl, h);
904
+ }
905
+ async function loadSessions() {
906
+ const d = await api('sessions');
907
+ const el = $('#sec-sessions');
908
+ if (!d || !d.length) { set(el, emptyState('&#128172;', 'No sessions found', 'No Claude Code session files were found in the .claude directory.', 'Refresh', 'refreshCurrent()')); return; }
909
+ allSessions = d;
910
+ const sm = { healthy: 0, corrupted: 0, empty: 0, orphaned: 0 }; d.forEach(s => { sm[s.status] = (sm[s.status] || 0) + 1; });
911
+ let h = '<h2>Sessions (' + d.length + ')</h2>';
912
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#9989;</div><h3>Healthy</h3><div class="value c-green">' + sm.healthy + '</div></div>';
913
+ h += '<div class="card"><div class="stat-icon">&#9888;</div><h3>Corrupted</h3><div class="value c-red">' + sm.corrupted + '</div></div>';
914
+ h += '<div class="card"><div class="stat-icon">&#128196;</div><h3>Empty</h3><div class="value c-yellow">' + sm.empty + '</div></div>';
915
+ h += '<div class="card"><div class="stat-icon">&#128279;</div><h3>Orphaned</h3><div class="value c-orange">' + sm.orphaned + '</div></div></div>';
916
+ h += '<div class="search-bar"><input type="text" class="search-input" placeholder="Search sessions by ID, project, or status..." oninput="filterSessions(this.value)"></div>';
917
+ h += '<div class="card mt"><table id="sessionTable"></table></div>';
918
+ h += '<div id="sessionDetail"></div>';
919
+ set(el, h); staggerCards(el); renderSessionTable(); refreshTime();
920
+ }
921
+
922
+ async function loadSecurity() {
923
+ const [d, comp] = await Promise.all([api('security'), api('compliance')]);
924
+ const el = $('#sec-security');
925
+ if (!d) { set(el, emptyState('&#128274;', 'Security scan unavailable', 'Could not scan for secrets.', 'Retry', 'refreshCurrent()')); return; }
926
+ let h = '<h2>Security Scan</h2>';
927
+ h += '<div class="action-bar">';
928
+ if (d.totalFindings > 0) h += '<button class="btn btn-danger" onclick="doRedactAll()">Redact All Secrets (' + d.totalFindings + ')</button>';
929
+ h += '</div>';
930
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#128269;</div><h3>Files Scanned</h3><div class="value">' + d.filesScanned + '</div></div>';
931
+ h += '<div class="card"><div class="stat-icon">&#128680;</div><h3>Findings</h3><div class="value ' + (d.totalFindings > 0 ? 'c-red' : 'c-green') + '">' + d.totalFindings + '</div></div>';
932
+ if (comp) {
933
+ h += '<div class="card"><div class="stat-icon">&#128172;</div><h3>Sessions</h3><div class="value c-accent">' + comp.sessionCount + '</div><div class="sub">' + fmtB(comp.totalSessionSize || 0) + ' total</div></div>';
934
+ h += '<div class="card"><div class="stat-icon">&#128197;</div><h3>Oldest</h3><div class="value">' + (comp.oldestDays || 0) + 'd</div><div class="sub">Newest: ' + (comp.newestDays || 0) + 'd ago</div></div>';
935
+ }
936
+ h += '</div>';
937
+ if (d.totalFindings > 0 && d.summary) {
938
+ h += '<div class="card mt"><h3>By Type</h3><table><tr><th>Type</th><th>Count</th></tr>';
939
+ Object.entries(d.summary).forEach(([t, c]) => { h += '<tr><td>' + esc(t) + '</td><td>' + c + '</td></tr>'; });
940
+ h += '</table></div>';
941
+ }
942
+ if (d.findings?.length) {
943
+ h += '<div class="card mt"><h3>Details</h3><table><tr><th>Severity</th><th>Pattern</th><th>Preview</th><th>Location</th><th>Actions</th></tr>';
944
+ d.findings.slice(0, 100).forEach(f => {
945
+ const sb = f.severity === 'critical' ? badge('critical', 'red') : f.severity === 'high' ? badge('high', 'orange') : badge('medium', 'yellow');
946
+ const fp = esc(f.file); const ln = f.line;
947
+ h += '<tr><td>' + sb + '</td><td>' + esc(f.pattern) + '</td><td class="mono">' + esc(f.maskedPreview) + '</td><td class="mono">' + esc(f.file.split('/').pop()) + ':' + ln + '</td>';
948
+ h += '<td><button class="btn btn-sm" onclick="doPreviewFinding(\\''+fp+'\\',' + ln + ')">Preview</button> ';
949
+ h += '<button class="btn btn-sm btn-danger" onclick="doRedact(\\''+fp+'\\',' + ln + ',\\''+esc(f.type||'')+'\\')">Redact</button></td></tr>';
950
+ });
951
+ h += '</table></div>';
952
+ }
953
+ if (comp) {
954
+ h += '<div class="card mt"><h3>Compliance &amp; Retention Report</h3>';
955
+ h += '<div class="detail-row"><span class="detail-key">Generated</span><span class="detail-val">' + esc(new Date(comp.generatedAt).toLocaleString()) + '</span></div>';
956
+ h += '<div class="detail-row"><span class="detail-key">Total Sessions</span><span class="detail-val">' + comp.sessionCount + '</span></div>';
957
+ h += '<div class="detail-row"><span class="detail-key">Total Session Storage</span><span class="detail-val">' + fmtB(comp.totalSessionSize || 0) + '</span></div>';
958
+ h += '<div class="detail-row"><span class="detail-key">Oldest Session</span><span class="detail-val">' + (comp.oldestDays || 0) + ' days ago</span></div>';
959
+ h += '<div class="detail-row"><span class="detail-key">Newest Session</span><span class="detail-val">' + (comp.newestDays || 0) + ' days ago</span></div>';
960
+ h += '<div class="detail-row"><span class="detail-key">Status</span><span class="detail-val">' + esc(comp.retentionStatus || 'N/A') + '</span></div>';
961
+ if (comp.ageBrackets) {
962
+ const ab = comp.ageBrackets;
963
+ h += '<div style="margin-top:16px"><h4 style="font-size:13px;font-weight:600;margin-bottom:10px">Session Age Distribution</h4>';
964
+ h += '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px">';
965
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;text-align:center"><div style="font-size:11px;color:var(--text3);text-transform:uppercase;font-weight:600">Last 7d</div><div style="font-size:20px;font-weight:700;color:var(--green);margin-top:4px">' + (ab.week || 0) + '</div></div>';
966
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;text-align:center"><div style="font-size:11px;color:var(--text3);text-transform:uppercase;font-weight:600">8-30d</div><div style="font-size:20px;font-weight:700;color:var(--accent);margin-top:4px">' + (ab.month || 0) + '</div></div>';
967
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;text-align:center"><div style="font-size:11px;color:var(--text3);text-transform:uppercase;font-weight:600">31-90d</div><div style="font-size:20px;font-weight:700;color:var(--yellow);margin-top:4px">' + (ab.quarter || 0) + '</div></div>';
968
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;text-align:center"><div style="font-size:11px;color:var(--text3);text-transform:uppercase;font-weight:600">90d+</div><div style="font-size:20px;font-weight:700;color:var(--red);margin-top:4px">' + (ab.older || 0) + '</div></div>';
969
+ h += '</div></div>';
970
+ }
971
+ if (comp.secretsScan) {
972
+ const ss = comp.secretsScan;
973
+ h += '<div style="margin-top:16px"><div class="detail-row"><span class="detail-key">Files Scanned for Secrets</span><span class="detail-val">' + ss.filesScanned + '</span></div>';
974
+ h += '<div class="detail-row"><span class="detail-key">Secret Findings</span><span class="detail-val ' + (ss.totalFindings > 0 ? 'c-red' : 'c-green') + '">' + (ss.totalFindings || 0) + '</span></div></div>';
975
+ }
976
+ h += '</div>';
977
+ }
978
+ set(el, h); staggerCards(el); refreshTime();
979
+ }
980
+
981
+ async function loadTraces() {
982
+ const d = await api('traces');
983
+ const el = $('#sec-traces');
984
+ if (!d) { set(el, emptyState('&#128065;', 'Trace data unavailable', 'Could not inventory trace files.', 'Retry', 'refreshCurrent()')); return; }
985
+ const mx = Math.max(...d.categories.map(c => c.totalSize), 1);
986
+ const sc = { critical: 'var(--red)', high: 'var(--orange)', medium: 'var(--yellow)', low: 'var(--green)' };
987
+ const sb = { critical: badge('critical', 'red'), high: badge('high', 'orange'), medium: badge('medium', 'yellow'), low: badge('low', 'green') };
988
+ const exclusions = loadExclusions();
989
+ let h = '<h2>Trace Inventory</h2>';
990
+ h += '<div class="action-bar">';
991
+ h += '<button class="btn btn-success" onclick="doCleanTracesPreview()">Preview Cleanup</button>';
992
+ h += '<button class="btn btn-danger" onclick="doCleanTracesExecute()">Clean Traces</button>';
993
+ h += '<button class="btn" onclick="showExclusionManager()" style="margin-left:8px" title="Manage protected files/categories">Exclusions (' + exclusions.exclusions.length + ')</button>';
994
+ h += '<button class="btn btn-danger" onclick="doWipeTraces()" style="margin-left:auto">Secure Wipe All</button>';
995
+ h += '</div>';
996
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#128196;</div><h3>Total Files</h3><div class="value">' + d.totalFiles + '</div></div>';
997
+ h += '<div class="card"><div class="stat-icon">&#128230;</div><h3>Total Size</h3><div class="value">' + fmtB(d.totalSize) + '</div></div>';
998
+ h += '<div class="card"><div class="stat-icon">&#128308;</div><h3>Critical</h3><div class="value ' + (d.criticalItems > 0 ? 'c-red' : 'c-green') + '">' + d.criticalItems + '</div></div>';
999
+ h += '<div class="card"><div class="stat-icon">&#128992;</div><h3>High</h3><div class="value ' + (d.highItems > 0 ? 'c-orange' : 'c-green') + '">' + d.highItems + '</div></div></div>';
1000
+ window._traceCategories = d.categories;
1001
+ h += '<div class="card mt"><h3>Category Details</h3>';
1002
+ h += '<table><tr><th>Category</th><th>Sensitivity</th><th>Files</th><th>Size</th><th>Age Range</th><th>Sample Files</th><th></th></tr>';
1003
+ d.categories.filter(c => c.fileCount > 0).sort((a, b) => b.totalSize - a.totalSize).forEach((c, idx) => {
1004
+ const oldest = c.oldestFile ? ago(c.oldestFile) : '-';
1005
+ const newest = c.newestFile ? ago(c.newestFile) : '-';
1006
+ const ageRange = oldest === newest ? oldest : newest + ' - ' + oldest;
1007
+ let samples = '-';
1008
+ if (c.sampleFiles?.length) {
1009
+ samples = '<div style="font-size:10px;line-height:1.4">';
1010
+ c.sampleFiles.slice(0, 3).forEach(f => {
1011
+ samples += '<div title="' + esc(f.fullPath) + '" style="color:var(--text2)">' + esc(f.projectName || f.path) + ' <span style="color:var(--text3)">(' + fmtB(f.size) + ')</span></div>';
1012
+ });
1013
+ if (c.fileCount > 3) samples += '<div style="color:var(--text3)">+' + (c.fileCount - 3) + ' more</div>';
1014
+ samples += '</div>';
1015
+ }
1016
+ h += '<tr>';
1017
+ h += '<td title="' + esc(c.description || '') + '"><strong>' + esc(c.name) + '</strong></td>';
1018
+ h += '<td>' + (sb[c.sensitivity] || c.sensitivity) + '</td>';
1019
+ h += '<td>' + c.fileCount + '</td>';
1020
+ h += '<td>' + fmtB(c.totalSize) + '</td>';
1021
+ h += '<td style="font-size:11px;color:var(--text3)">' + ageRange + '</td>';
1022
+ h += '<td>' + samples + '</td>';
1023
+ h += '<td><button class="btn btn-sm" onclick="showTraceCategoryFiles(\\'' + esc(c.name) + '\\')">View All</button></td>';
1024
+ h += '</tr>';
1025
+ });
1026
+ h += '</table></div>';
1027
+ h += '<div class="card mt"><h3>Size Distribution</h3><div class="bars">';
1028
+ d.categories.filter(c => c.fileCount > 0).sort((a, b) => b.totalSize - a.totalSize).forEach(c => {
1029
+ const p = Math.max((c.totalSize / mx) * 100, 2);
1030
+ const col = sc[c.sensitivity] || 'var(--accent)';
1031
+ h += '<div class="bar-row"><div class="bar-label" title="' + esc(c.description || '') + '">' + esc(c.name) + '</div><div class="bar-track"><div class="bar-fill" style="width:' + p + '%;background:' + col + '"></div></div><div class="bar-val">' + c.fileCount + ' / ' + fmtB(c.totalSize) + '</div></div>';
1032
+ });
1033
+ h += '</div></div>';
1034
+ set(el, h); staggerCards(el); refreshTime();
1035
+ }
1036
+
1037
+ async function loadMcp() {
1038
+ const d = await api('mcp');
1039
+ const el = $('#sec-mcp');
1040
+ if (!d) { set(el, emptyState('&#9881;', 'MCP data unavailable', 'Could not diagnose MCP server configurations.', 'Retry', 'refreshCurrent()')); return; }
1041
+ let h = '<h2>MCP Servers</h2>';
1042
+ h += '<div class="action-bar"><button class="btn btn-primary" onclick="doTestMcp()">Test All Servers</button> <button class="btn btn-secondary" onclick="showAddMcpModal()">+ Add Server</button></div>';
1043
+ h += '<div class="grid">';
1044
+ h += '<div class="card"><div class="stat-icon">&#9881;</div><h3>Configs</h3><div class="value">' + d.configs.length + '</div></div>';
1045
+ h += '<div class="card"><div class="stat-icon">&#127760;</div><h3>Servers</h3><div class="value">' + d.totalServers + '</div></div>';
1046
+ h += '<div class="card"><div class="stat-icon">&#9989;</div><h3>Healthy</h3><div class="value c-green">' + d.healthyServers + '</div></div></div>';
1047
+ d.configs.forEach(cfg => {
1048
+ h += '<div class="card mt"><h3>' + esc(cfg.configPath.split('/').pop()) + '</h3>';
1049
+ h += '<div style="font-size:11px;color:var(--text3);margin-bottom:12px;word-break:break-all">' + esc(cfg.configPath) + '</div>';
1050
+ cfg.servers.forEach(srv => {
1051
+ const err = cfg.issues.some(i => i.server === srv.name && i.severity === 'error');
1052
+ const srvId = 'mcp-srv-' + esc(srv.name).replace(/[^a-zA-Z0-9]/g, '_');
1053
+ h += '<div class="mcp-server-card" style="border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:8px;overflow:hidden">';
1054
+ h += '<div onclick="toggleMcpServer(\\''+srvId+'\\')" style="padding:12px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;background:var(--bg)">';
1055
+ h += '<div style="display:flex;align-items:center;gap:10px"><span id="' + srvId + '-arrow" style="font-size:10px;color:var(--text3)">&#9654;</span><strong>' + esc(srv.name) + '</strong>' + (err ? badge('error', 'red') : badge('ok', 'green')) + '</div>';
1056
+ h += '<div style="display:flex;align-items:center;gap:8px"><span id="' + srvId + '-badge" style="font-size:11px;color:var(--text3)">...</span><button class="btn btn-sm btn-primary" onclick="event.stopPropagation();probeMcpServer(\\''+esc(srv.name)+'\\')">Probe</button></div>';
1057
+ h += '</div>';
1058
+ h += '<div id="' + srvId + '" style="display:none;padding:12px;background:var(--bg2);border-top:1px solid var(--border)">';
1059
+ h += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px">Command: <span class="mono">' + esc(srv.command) + (srv.args?.length ? ' ' + srv.args.map(a => esc(a)).join(' ') : '') + '</span></div>';
1060
+ if (srv.env && Object.keys(srv.env).length > 0) { h += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px">Env: ' + Object.keys(srv.env).map(k => esc(k)).join(', ') + '</div>'; }
1061
+ h += '<div id="' + srvId + '-caps" style="margin-top:8px"><div style="color:var(--text3);font-size:11px">Click "Probe" to discover tools, resources, and prompts</div></div>';
1062
+ h += '</div></div>';
1063
+ });
1064
+ if (cfg.issues.length) {
1065
+ h += '<div style="margin-top:12px">'; cfg.issues.forEach(i => {
1066
+ const ib = i.severity === 'error' ? badge('error', 'red') : i.severity === 'warning' ? badge('warn', 'yellow') : badge('info', 'blue');
1067
+ h += '<div style="padding:4px 0;font-size:12px">' + ib + ' <strong>' + esc(i.server) + ':</strong> ' + esc(i.message);
1068
+ if (i.fix) h += '<br><span style="color:var(--text3);margin-left:12px">Fix: ' + esc(i.fix) + '</span>';
1069
+ h += '</div>';
1070
+ }); h += '</div>';
1071
+ }
1072
+ h += '</div>';
1073
+ });
1074
+ if (d.recommendations?.length) { h += '<div class="card mt"><h3>Recommendations</h3>'; d.recommendations.forEach(r => { h += '<div style="padding:4px 0;font-size:13px;color:var(--text2)">' + esc(r) + '</div>'; }); h += '</div>'; }
1075
+ h += '<div class="card mt"><h3>CCT CLI Commands</h3>';
1076
+ h += '<div style="font-size:12px;color:var(--text3);margin-bottom:12px">Available commands for MCP and server management</div>';
1077
+ const cmds = [
1078
+ ['cct mcp', 'Diagnose MCP server configurations and connectivity'],
1079
+ ['cct health', 'Quick health check of Claude Code installation'],
1080
+ ['cct scan', 'Scan conversations for oversized images and content'],
1081
+ ['cct fix', 'Fix oversized content with automatic backups'],
1082
+ ['cct security', 'Scan for exposed secrets and API keys'],
1083
+ ['cct traces', 'Inventory trace files (logs, telemetry, crash reports)'],
1084
+ ['cct stats', 'Show conversation statistics and file sizes'],
1085
+ ['cct context', 'Estimate token/context usage per conversation'],
1086
+ ['cct analytics', 'Usage analytics dashboard (sessions, tools, activity)'],
1087
+ ['cct duplicates', 'Find duplicate conversations and content'],
1088
+ ['cct archive --dry-run', 'Preview archiving inactive conversations'],
1089
+ ['cct maintenance', 'Run maintenance checks (dry-run by default)'],
1090
+ ['cct export &lt;file&gt;', 'Export a conversation to markdown or JSON'],
1091
+ ['cct dashboard', 'Launch this web dashboard'],
1092
+ ];
1093
+ h += '<table><tr><th>Command</th><th>Description</th></tr>';
1094
+ cmds.forEach(c => { h += '<tr><td class="mono" style="white-space:nowrap;color:var(--accent)">' + c[0] + '</td><td style="color:var(--text2)">' + c[1] + '</td></tr>'; });
1095
+ h += '</table></div>';
1096
+ h += '<div id="mcpTestResults"></div>';
1097
+ set(el, h); staggerCards(el); refreshTime();
1098
+ }
1099
+
1100
+ let logsSearch = ''; let logsLevel = '';
1101
+ async function loadLogs() {
1102
+ const params = new URLSearchParams();
1103
+ if (logsSearch) params.set('search', logsSearch);
1104
+ if (logsLevel) params.set('level', logsLevel);
1105
+ params.set('limit', '200');
1106
+ const d = await api('logs?' + params.toString());
1107
+ const el = $('#sec-logs');
1108
+ if (!d) { set(el, emptyState('&#128196;', 'Logs unavailable', 'Could not load debug logs.', 'Retry', 'refreshCurrent()')); return; }
1109
+ let h = '<h2>Debug Logs</h2>';
1110
+ h += '<div class="action-bar" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">';
1111
+ h += '<input id="logSearchInput" type="text" placeholder="Search logs..." value="' + esc(logsSearch) + '" style="flex:1;min-width:200px;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)">';
1112
+ h += '<select id="logLevelFilter" style="padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)">';
1113
+ h += '<option value="">All Levels</option>';
1114
+ h += '<option value="ERROR"' + (logsLevel === 'ERROR' ? ' selected' : '') + '>ERROR</option>';
1115
+ h += '<option value="WARN"' + (logsLevel === 'WARN' ? ' selected' : '') + '>WARN</option>';
1116
+ h += '<option value="INFO"' + (logsLevel === 'INFO' ? ' selected' : '') + '>INFO</option>';
1117
+ h += '<option value="DEBUG"' + (logsLevel === 'DEBUG' ? ' selected' : '') + '>DEBUG</option>';
1118
+ h += '</select>';
1119
+ h += '<button class="btn btn-primary" onclick="applyLogFilters()">Apply</button>';
1120
+ h += '<button class="btn" onclick="logsSearch=\\'\\';logsLevel=\\'\\';loadLogs()">Clear</button>';
1121
+ h += '</div>';
1122
+ h += '<div class="grid" style="margin-top:16px">';
1123
+ h += '<div class="card"><div class="stat-icon">&#128196;</div><h3>Log Files</h3><div class="value">' + (d.summary?.totalFiles || 0) + '</div></div>';
1124
+ h += '<div class="card"><div class="stat-icon">&#128230;</div><h3>Total Size</h3><div class="value">' + fmtB(d.summary?.totalSize || 0) + '</div></div>';
1125
+ const errCnt = (d.summary?.levelCounts?.ERROR || 0) + (d.summary?.levelCounts?.WARN || 0);
1126
+ h += '<div class="card"><div class="stat-icon">&#9888;</div><h3>Errors/Warnings</h3><div class="value ' + (errCnt > 0 ? 'c-orange' : 'c-green') + '">' + errCnt + '</div></div></div>';
1127
+ if (d.summary?.topComponents?.length) {
1128
+ h += '<div class="card mt"><h3>Top Components</h3><div class="bars">';
1129
+ const mx = Math.max(...d.summary.topComponents.map(c => c.count), 1);
1130
+ d.summary.topComponents.slice(0, 8).forEach(c => {
1131
+ const p = Math.max((c.count / mx) * 100, 2);
1132
+ h += '<div class="bar-row"><div class="bar-label" style="max-width:150px;overflow:hidden;text-overflow:ellipsis">' + esc(c.name) + '</div><div class="bar-track"><div class="bar-fill" style="width:' + p + '%;background:var(--accent)"></div></div><div class="bar-val">' + c.count + '</div></div>';
1133
+ });
1134
+ h += '</div></div>';
1135
+ }
1136
+ h += '<div class="card mt"><h3>Recent Log Entries' + (d.entries?.length ? ' (' + d.entries.length + ')' : '') + '</h3>';
1137
+ if (d.entries?.length) {
1138
+ h += '<div style="max-height:500px;overflow:auto;font-family:var(--font-mono);font-size:11px">';
1139
+ d.entries.forEach(e => {
1140
+ const lvlCol = e.level === 'ERROR' ? 'var(--red)' : e.level === 'WARN' ? 'var(--orange)' : e.level === 'INFO' ? 'var(--green)' : 'var(--text3)';
1141
+ const ts = new Date(e.timestamp).toLocaleTimeString();
1142
+ h += '<div style="padding:4px 0;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:flex-start">';
1143
+ h += '<span style="color:var(--text3);white-space:nowrap">' + ts + '</span>';
1144
+ h += '<span style="color:' + lvlCol + ';font-weight:600;min-width:50px">[' + e.level + ']</span>';
1145
+ if (e.component) h += '<span style="color:var(--accent);white-space:nowrap">[' + esc(e.component) + ']</span>';
1146
+ h += '<span style="color:var(--text2);word-break:break-word">' + esc(e.message.slice(0, 500)) + (e.message.length > 500 ? '...' : '') + '</span>';
1147
+ h += '</div>';
1148
+ });
1149
+ h += '</div>';
1150
+ } else { h += '<div style="color:var(--text3);padding:20px;text-align:center">No log entries match the current filters</div>'; }
1151
+ h += '</div>';
1152
+ if (d.files?.length) {
1153
+ h += '<div class="card mt"><h3>Log Files</h3><table><tr><th>Session</th><th>Project</th><th>Size</th><th>Modified</th><th>Status</th></tr>';
1154
+ d.files.forEach(f => {
1155
+ const sessionShort = f.sessionId ? f.sessionId.slice(0, 8) + '...' : f.name;
1156
+ const projectDisplay = f.projectName || '<span style="color:var(--text3)">Unknown</span>';
1157
+ h += '<tr>';
1158
+ h += '<td class="mono" title="' + esc(f.sessionId || f.name) + '">' + esc(sessionShort) + '</td>';
1159
+ h += '<td>' + (f.projectName ? '<span title="' + esc(f.projectPath || '') + '">' + esc(f.projectName) + '</span>' : '<span style="color:var(--text3)">—</span>') + '</td>';
1160
+ h += '<td>' + fmtB(f.size) + '</td>';
1161
+ h += '<td>' + ago(f.modified) + '</td>';
1162
+ h += '<td>' + (f.isLatest ? badge('current', 'green') : '') + '</td>';
1163
+ h += '</tr>';
1164
+ });
1165
+ h += '</table></div>';
1166
+ }
1167
+ set(el, h); staggerCards(el); refreshTime();
1168
+ const searchInput = $('#logSearchInput');
1169
+ if (searchInput) { searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') applyLogFilters(); }); }
1170
+ }
1171
+ function applyLogFilters() {
1172
+ const searchEl = $('#logSearchInput');
1173
+ const levelEl = $('#logLevelFilter');
1174
+ logsSearch = searchEl?.value || '';
1175
+ logsLevel = levelEl?.value || '';
1176
+ loadLogs();
1177
+ }
1178
+
1179
+ let activeConfigTab = 'settings';
1180
+ async function loadConfig() {
1181
+ const d = await api('config');
1182
+ const el = $('#sec-config');
1183
+ if (!d) { set(el, emptyState('&#9881;', 'Config unavailable', 'Could not load configuration files.', 'Retry', 'refreshCurrent()')); return; }
1184
+ let h = '<h2>Configuration</h2>';
1185
+ h += '<div class="config-tabs" style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:16px">';
1186
+ const tabs = [
1187
+ { id: 'settings', label: 'Settings', icon: '&#9881;', exists: d.settings?.exists },
1188
+ { id: 'globalClaudeMd', label: 'Global CLAUDE.md', icon: '&#128196;', exists: d.globalClaudeMd?.exists },
1189
+ { id: 'globalMcp', label: 'Global MCP', icon: '&#127760;', exists: d.globalMcp?.exists },
1190
+ { id: 'projectClaudeMd', label: 'Project CLAUDE.md', icon: '&#128196;', exists: d.projectClaudeMd?.exists },
1191
+ { id: 'projectMcp', label: 'Project MCP', icon: '&#127760;', exists: d.projectMcp?.exists },
1192
+ ];
1193
+ tabs.forEach(t => {
1194
+ const active = activeConfigTab === t.id ? ' style="background:var(--accent);color:#fff"' : '';
1195
+ const existBadge = t.exists ? '' : '<span style="font-size:9px;color:var(--text3);margin-left:4px">(new)</span>';
1196
+ h += '<button class="btn btn-sm"' + active + ' onclick="switchConfigTab(\\''+t.id+'\\')">' + t.icon + ' ' + t.label + existBadge + '</button>';
1197
+ });
1198
+ h += '</div>';
1199
+ const cfg = d[activeConfigTab];
1200
+ h += '<div class="card">';
1201
+ h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">';
1202
+ h += '<h3>' + tabs.find(t => t.id === activeConfigTab)?.label || activeConfigTab + '</h3>';
1203
+ h += '<button class="btn btn-primary btn-sm" onclick="saveCurrentConfig()">Save Changes</button>';
1204
+ h += '</div>';
1205
+ if (cfg?.path) { h += '<div style="font-size:11px;color:var(--text3);margin-bottom:8px;word-break:break-all">' + esc(cfg.path) + '</div>'; }
1206
+ if (cfg?.exists && cfg?.modified) { h += '<div style="font-size:11px;color:var(--text3);margin-bottom:12px">Last modified: ' + ago(cfg.modified) + ' | Size: ' + fmtB(cfg.size || 0) + '</div>'; }
1207
+ const isJson = activeConfigTab === 'settings' || activeConfigTab.includes('Mcp');
1208
+
1209
+ let editorContent = '';
1210
+ if (cfg?.content) {
1211
+ editorContent = esc(cfg.content);
1212
+ // Auto-format JSON if possible
1213
+ if(isJson) {
1214
+ try {
1215
+ const parsed = JSON.parse(cfg.content);
1216
+ editorContent = JSON.stringify(parsed, null, 2);
1217
+ } catch {}
1218
+ }
1219
+ } else if (!cfg?.exists) {
1220
+ if (isJson) { editorContent = '{\\n}'; }
1221
+ else { editorContent = '# ' + (tabs.find(t => t.id === activeConfigTab)?.label || 'Config') + '\\n'; }
1222
+ }
1223
+
1224
+ h += '<textarea id="configEditor" class="code-editor" spellcheck="false">';
1225
+ h += editorContent;
1226
+ h += '</textarea>';
1227
+ if (isJson) { h += '<div style="margin-top:8px;font-size:11px;color:var(--text3)">JSON configuration file. Changes will be validated before saving.</div>'; }
1228
+ h += '</div>';
1229
+ h += '<div class="card mt"><h3>Configuration Files Summary</h3><table><tr><th>File</th><th>Path</th><th>Status</th><th>Size</th></tr>';
1230
+ tabs.forEach(t => {
1231
+ const c = d[t.id];
1232
+ h += '<tr><td>' + t.label + '</td><td class="mono" style="max-width:300px;overflow:hidden;text-overflow:ellipsis">' + esc(c?.path || '') + '</td>';
1233
+ h += '<td>' + (c?.exists ? badge('exists', 'green') : badge('missing', 'yellow')) + '</td>';
1234
+ h += '<td>' + (c?.exists ? fmtB(c.size || 0) : '-') + '</td></tr>';
1235
+ });
1236
+ h += '</table></div>';
1237
+ set(el, h); staggerCards(el); refreshTime();
1238
+ }
1239
+ function switchConfigTab(tab) { activeConfigTab = tab; loadConfig(); }
1240
+ async function saveCurrentConfig() {
1241
+ const editor = $('#configEditor');
1242
+ if (!editor) { toast('Editor not found', 'error'); return; }
1243
+ const content = editor.value;
1244
+ showProgress('Saving configuration...');
1245
+ const r = await post('save-config', { type: activeConfigTab, content });
1246
+ hideProgress();
1247
+ if (r.success) { toast('Configuration saved', 'success'); loadConfig(); }
1248
+ else { toast('Failed: ' + (r.error || 'Unknown error'), 'error'); }
1249
+ }
1250
+
1251
+ async function loadAnalytics() {
1252
+ const d = await api('analytics');
1253
+ const el = $('#sec-analytics');
1254
+ if (!d) { set(el, emptyState('&#128200;', 'Analytics unavailable', 'Could not generate usage analytics.', 'Retry', 'refreshCurrent()')); return; }
1255
+ let h = '<h2>Analytics</h2>';
1256
+
1257
+ // Cost Estimator
1258
+ const cachedCost = localStorage.getItem('cct_cost_per_m') || '15.00';
1259
+ const totalTokensM = (d.totalTokens || 0) / 1000000;
1260
+ const estimatedCost = (parseFloat(cachedCost) * totalTokensM).toFixed(2);
1261
+
1262
+ h += '<div class="card mb" style="margin-bottom:16px;background:var(--bg3);border-color:var(--border-light)">';
1263
+ h += '<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px">';
1264
+ h += '<div style="display:flex;align-items:center;gap:12px">';
1265
+ h += '<div class="stat-icon" style="float:none;font-size:18px">&#128178;</div>';
1266
+ h += '<div><div style="font-size:11px;font-weight:600;color:var(--text3);text-transform:uppercase">Estimated Cost of History</div><div style="font-size:20px;font-weight:700;color:var(--green)">$' + estimatedCost + '</div></div>';
1267
+ h += '</div>';
1268
+ h += '<div style="display:flex;align-items:center;gap:8px;font-size:12px">';
1269
+ h += '<label style="color:var(--text3)">Avg Price ($/1M tokens):</label>';
1270
+ h += '<input type="number" id="costInput" value="' + cachedCost + '" step="0.01" style="width:70px;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-family:var(--font)" onchange="updateCost(this.value, ' + totalTokensM + ')">';
1271
+ h += '</div></div></div>';
1272
+
1273
+ h += '<div class="grid">';
1274
+ h += '<div class="card"><div class="stat-icon">&#128172;</div><h3>Sessions</h3><div class="value">' + (d.totalSessions || 0) + '</div></div>';
1275
+ h += '<div class="card"><div class="stat-icon">&#128172;</div><h3>Messages</h3><div class="value">' + fmtK(d.totalMessages || 0) + '</div></div>';
1276
+ h += '<div class="card"><div class="stat-icon">&#129520;</div><h3>Tokens</h3><div class="value">' + fmtK(d.totalTokens || 0) + '</div></div>';
1277
+ h += '<div class="card"><div class="stat-icon">&#128197;</div><h3>Active Days</h3><div class="value">' + (d.activeDays || 0) + '</div></div>';
1278
+ h += '<div class="card"><div class="stat-icon">&#128200;</div><h3>Context Tokens</h3><div class="value">' + fmtK(d.contextTokens || 0) + '</div><div class="sub">avg ' + fmtK(d.avgTokensPerSession || 0) + '/session</div></div>';
1279
+ if (d.tokenWarnings > 0) h += '<div class="card"><div class="stat-icon">&#9888;</div><h3>Token Warnings</h3><div class="value c-orange">' + d.tokenWarnings + '</div><div class="sub">&gt;100K token sessions</div></div>';
1280
+ h += '</div>';
1281
+ if (d.dailyActivity?.length) {
1282
+ // SVG Chart
1283
+ const data = d.dailyActivity.slice(-30).map(day => ({ value: day.messages || 0, date: day.date }));
1284
+ const svg = sparklineSvg(data, 800, 100, '#60a5fa');
1285
+
1286
+ h += '<div class="card mt"><h3>Activity (last 30 days)</h3>';
1287
+ h += '<div style="height:120px;position:relative;margin-top:12px" onmousemove="showChartTooltip(event)" onmouseleave="hideChartTooltip()">';
1288
+ h += svg;
1289
+ h += '<div id="chartTooltip" style="display:none;position:absolute;background:var(--bg2);border:1px solid var(--border);padding:4px 8px;border-radius:4px;font-size:11px;pointer-events:none;transform:translate(-50%, -100%);top:0;left:0;box-shadow:var(--shadow)"></div>';
1290
+ // Set data directly
1291
+ window.chartData = data;
1292
+ h += '</div></div>';
1293
+ }
1294
+ if (d.topProjects?.length) {
1295
+ h += '<div class="card mt"><h3>Top Projects</h3><table><tr><th>Project</th><th>Sessions</th><th>Messages</th></tr>';
1296
+ d.topProjects.slice(0, 10).forEach(p => {
1297
+ const nm = p.name.length > 40 ? '...' + p.name.slice(-37) : p.name;
1298
+ h += '<tr><td>' + esc(nm) + '</td><td>' + (p.sessions || 0) + '</td><td>' + (p.messages || 0) + '</td></tr>';
1299
+ });
1300
+ h += '</table></div>';
1301
+ }
1302
+ if (d.toolUsage && Object.keys(d.toolUsage).length) {
1303
+ const mx2 = Math.max(...Object.values(d.toolUsage));
1304
+ const cols = ['#60a5fa', '#34d399', '#fbbf24', '#f87171', '#fb923c', '#a78bfa'];
1305
+ h += '<div class="card mt"><h3>Tool Usage</h3><div class="bars">';
1306
+ Object.entries(d.toolUsage).sort((a, b) => b[1] - a[1]).slice(0, 12).forEach(([t, c], i) => {
1307
+ const p = Math.max((c / mx2) * 100, 2);
1308
+ h += '<div class="bar-row"><div class="bar-label">' + esc(t) + '</div><div class="bar-track"><div class="bar-fill" style="width:' + p + '%;background:' + cols[i % cols.length] + '"></div></div><div class="bar-val">' + c + '</div></div>';
1309
+ });
1310
+ h += '</div></div>';
1311
+ }
1312
+ set(el, h); staggerCards(el); refreshTime();
1313
+ }
1314
+
1315
+ function updateCost(cost, totalM) {
1316
+ localStorage.setItem('cct_cost_per_m', cost);
1317
+ loadAnalytics(); // Refresh to recalculate
1318
+ }
1319
+
1320
+ function showChartTooltip(e) {
1321
+ const rect = e.currentTarget.getBoundingClientRect();
1322
+ const x = e.clientX - rect.left;
1323
+ const width = rect.width;
1324
+ const data = window.chartData || [];
1325
+ if (!data.length) return;
1326
+
1327
+ const index = Math.min(Math.max(0, Math.floor((x / width) * data.length)), data.length - 1);
1328
+ const item = data[index];
1329
+ const tip = document.getElementById('chartTooltip');
1330
+ if (tip && item) {
1331
+ tip.style.display = 'block';
1332
+ tip.style.left = x + 'px';
1333
+ tip.style.top = (rect.height / 2) + 'px'; // Center vertically relative to chart area
1334
+ tip.innerHTML = \`<div style="color:var(--text3)">\${item.date}</div><div style="font-weight:700;color:var(--accent)">\${item.value} msgs</div>\`;
1335
+ }
1336
+ }
1337
+
1338
+ function hideChartTooltip() {
1339
+ const tip = document.getElementById('chartTooltip');
1340
+ if (tip) tip.style.display = 'none';
1341
+ }
1342
+
1343
+ async function loadBackups() {
1344
+ const d = await api('backups');
1345
+ const el = $('#sec-backups');
1346
+ if (!d) { set(el, emptyState('&#128190;', 'Backup data unavailable', 'Could not scan for backup files.', 'Retry', 'refreshCurrent()')); return; }
1347
+ let h = '<h2>Backups</h2>';
1348
+ h += '<div class="action-bar"><button class="btn btn-danger" onclick="doDeleteBackups()">Delete Old Backups (&gt;7 days)</button></div>';
1349
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#128190;</div><h3>Total Backups</h3><div class="value">' + (d.totalBackups || 0) + '</div></div>';
1350
+ h += '<div class="card"><div class="stat-icon">&#128230;</div><h3>Total Size</h3><div class="value">' + fmtB(d.totalSize || 0) + '</div></div></div>';
1351
+ if (d.backups?.length) {
1352
+ h += '<div class="card mt"><table><tr><th>File</th><th>Directory</th><th>Size</th><th>Created</th><th>Actions</th></tr>';
1353
+ d.backups.slice(0, 100).forEach(b => {
1354
+ const bp = esc(b.path);
1355
+ h += '<tr><td class="mono">' + esc(b.file) + '</td><td class="mono" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">' + esc(b.dir) + '</td><td>' + fmtB(b.size) + '</td><td>' + ago(b.created) + '</td>';
1356
+ h += '<td><button class="btn btn-sm btn-success" onclick="doRestore(\\''+bp+'\\')">Restore</button></td></tr>';
1357
+ });
1358
+ h += '</table></div>';
1359
+ } else { h += emptyState('&#128190;', 'No backups found', 'No backup files were found. Backups are created automatically when fixing issues or redacting secrets.'); }
1360
+ set(el, h); staggerCards(el); refreshTime();
1361
+ }
1362
+
1363
+ async function loadContext() {
1364
+ const d = await api('context');
1365
+ const el = $('#sec-context');
1366
+ if (!d) { set(el, emptyState('&#129520;', 'Context data unavailable', 'Could not estimate token usage.', 'Retry', 'refreshCurrent()')); return; }
1367
+ let h = '<h2>Context Estimation</h2>';
1368
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#129520;</div><h3>Total Tokens</h3><div class="value">' + fmtK(d.totalTokens || 0) + '</div></div>';
1369
+ h += '<div class="card"><div class="stat-icon">&#128196;</div><h3>Sessions</h3><div class="value">' + (d.totalFiles || 0) + '</div></div>';
1370
+ h += '<div class="card"><div class="stat-icon">&#9888;</div><h3>Large Sessions</h3><div class="value ' + (d.warnings > 0 ? 'c-orange' : 'c-green') + '">' + (d.warnings || 0) + '</div><div class="sub">&gt;100K tokens</div></div></div>';
1371
+ if (d.estimates?.length) {
1372
+ h += '<div class="card mt"><h3>Sessions by Token Usage</h3>';
1373
+ h += '<div style="overflow-x:auto"><table><tr><th>Project</th><th>Session</th><th>Tokens</th><th>Msgs</th><th>Imgs</th><th>Tools</th><th>Status</th></tr>';
1374
+ d.estimates.forEach(e => {
1375
+ const warn = e.warnings?.length > 0;
1376
+ const tk = e.tokens || 0;
1377
+ const cls = tk > 100000 ? 'c-red' : tk > 50000 ? 'c-orange' : '';
1378
+ const shortId = (e.sessionId || '').slice(0, 8);
1379
+ const isAgent = e.sessionId?.startsWith('agent-');
1380
+ h += '<tr>';
1381
+ h += '<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis"><strong>' + esc(e.projectName || 'Unknown') + '</strong></td>';
1382
+ h += '<td style="max-width:200px">';
1383
+ h += '<span class="mono" style="font-size:10px;color:var(--text3)" title="' + esc(e.sessionId || '') + '">' + (isAgent ? '&#129302; ' : '') + (shortId || '?') + '</span>';
1384
+ if (e.firstPrompt) { h += '<div style="font-size:11px;color:var(--text2);margin-top:2px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(e.firstPrompt) + '">' + esc(e.firstPrompt) + '</div>'; }
1385
+ h += '</td>';
1386
+ h += '<td class="' + cls + '">' + fmtK(tk) + '</td>';
1387
+ h += '<td>' + (e.messages || 0) + '</td>';
1388
+ h += '<td>' + (e.images || 0) + '</td>';
1389
+ h += '<td>' + (e.tools || 0) + '</td>';
1390
+ h += '<td>' + (warn ? badge('warn', 'orange') : badge('ok', 'green')) + '</td>';
1391
+ h += '</tr>';
1392
+ });
1393
+ h += '</table></div></div>';
1394
+ }
1395
+ set(el, h); staggerCards(el); refreshTime();
1396
+ }
1397
+
1398
+ async function loadMaintenance() {
1399
+ const [maint, arch] = await Promise.all([api('maintenance'), api('archive/candidates')]);
1400
+ const el = $('#sec-maintenance');
1401
+ if (!maint) { set(el, emptyState('&#128736;', 'Maintenance data unavailable', 'Could not run maintenance checks.', 'Retry', 'refreshCurrent()')); return; }
1402
+ let h = '<h2>Maintenance</h2>';
1403
+ h += '<div class="action-bar">';
1404
+ if (maint.totalActions > 0) h += '<button class="btn btn-primary" onclick="doMaintenance()">Run Maintenance (' + maint.totalActions + ' actions)</button>';
1405
+ if (arch?.totalCandidates > 0) h += '<button class="btn btn-warn" onclick="doArchive()">Archive ' + arch.totalCandidates + ' Conversations</button>';
1406
+ h += '</div>';
1407
+ h += '<div class="grid"><div class="card"><div class="stat-icon">&#128736;</div><h3>Status</h3><div class="value ' + (maint.status === 'clean' ? 'c-green' : 'c-yellow') + '">' + esc(maint.status || 'unknown') + '</div></div>';
1408
+ h += '<div class="card"><div class="stat-icon">&#128221;</div><h3>Pending Actions</h3><div class="value">' + (maint.totalActions || 0) + '</div></div>';
1409
+ h += '<div class="card"><div class="stat-icon">&#128190;</div><h3>Reclaimable Space</h3><div class="value">' + fmtB(maint.estimatedSpace || 0) + '</div></div>';
1410
+ if (arch) h += '<div class="card"><div class="stat-icon">&#128451;</div><h3>Archive Candidates</h3><div class="value c-accent">' + (arch.totalCandidates || 0) + '</div><div class="sub">' + fmtB(arch.totalSize || 0) + ' reclaimable</div></div>';
1411
+ h += '</div>';
1412
+ if (maint.actions?.length) {
1413
+ h += '<div class="card mt"><h3>Actions</h3><table><tr><th>Type</th><th>Description</th><th>Space</th><th>Count</th></tr>';
1414
+ maint.actions.forEach(a => {
1415
+ h += '<tr><td>' + badge(esc(a.type), 'blue') + '</td><td>' + esc(a.description) + '</td><td>' + fmtB(a.sizeBytes || 0) + '</td><td>' + (a.count || '-') + '</td></tr>';
1416
+ });
1417
+ h += '</table></div>';
1418
+ }
1419
+ if (arch?.candidates?.length) {
1420
+ h += '<div class="card mt"><h3>Archive Candidates (inactive &gt;30 days)</h3><table><tr><th>File</th><th>Size</th><th>Messages</th><th>Days Inactive</th></tr>';
1421
+ arch.candidates.slice(0, 30).forEach(c => {
1422
+ h += '<tr><td class="mono" style="max-width:300px;overflow:hidden;text-overflow:ellipsis" title="' + esc(c.file) + '">' + esc(c.file.split('/').pop()) + '</td>';
1423
+ h += '<td>' + fmtB(c.size) + '</td><td>' + (c.messageCount || 0) + '</td><td>' + c.daysInactive + 'd</td></tr>';
1424
+ });
1425
+ h += '</table></div>';
1426
+ }
1427
+ set(el, h); staggerCards(el); refreshTime();
1428
+ }
1429
+
1430
+ function loadAbout() {
1431
+ const el = $('#sec-about');
1432
+ let h = '<div class="about-hero">';
1433
+ h += '<div class="about-logo">C</div>';
1434
+ h += '<h2>Claude Code Toolkit</h2>';
1435
+ h += '<p class="about-desc">MCP server and CLI toolkit for maintaining, optimizing, and troubleshooting Claude Code installations</p>';
1436
+ h += '<div class="about-meta"><span>v1.2.0</span><span>MIT License</span><span>by Asif Kibria</span></div>';
1437
+ h += '</div>';
1438
+ h += '<h3 style="font-size:16px;font-weight:700;margin-bottom:14px">Features</h3>';
1439
+ h += '<div class="feature-grid">';
1440
+ const features = [
1441
+ ['&#128269;', 'Health Check', 'Quick system diagnostics and status overview'],
1442
+ ['&#128230;', 'Storage Analysis', 'Analyze, visualize, and clean the .claude directory'],
1443
+ ['&#128736;', 'Session Recovery', 'Diagnose, repair, and extract data from sessions'],
1444
+ ['&#128274;', 'Security Scanning', 'Detect exposed secrets, API keys, and credentials'],
1445
+ ['&#128065;', 'Trace Management', 'Inventory, selective cleanup, and secure wipe of traces'],
1446
+ ['&#9881;', 'MCP Validation', 'Diagnose MCP server configurations and connectivity'],
1447
+ ['&#129520;', 'Context Estimation', 'Estimate token usage and context size per conversation'],
1448
+ ['&#128200;', 'Usage Analytics', 'Activity tracking, project rankings, and tool usage stats'],
1449
+ ['&#128270;', 'Duplicate Detection', 'Find redundant conversations and wasted storage'],
1450
+ ['&#128451;', 'Archive Management', 'Archive inactive conversations to free space'],
1451
+ ['&#128260;', 'Maintenance', 'Automated health checks with scheduling support'],
1452
+ ['&#128190;', 'Backup Management', 'Create, restore, and clean backup files'],
1453
+ ['&#128221;', 'Export', 'Export conversations to markdown or JSON format'],
1454
+ ['&#127760;', 'Web Dashboard', 'Real-time monitoring with 11 interactive tabs'],
1455
+ ];
1456
+ features.forEach(f => { h += '<div class="feature-card"><div class="fc-icon">' + f[0] + '</div><h4>' + f[1] + '</h4><p>' + f[2] + '</p></div>'; });
1457
+ h += '</div>';
1458
+ h += '<h3 style="font-size:16px;font-weight:700;margin-bottom:14px">Links</h3>';
1459
+ h += '<div class="link-grid">';
1460
+ h += '<a class="link-card" href="https://github.com/asifkibria/claude-code-toolkit" target="_blank"><div class="lc-icon">&#128187;</div><div><div class="lc-title">GitHub Repository</div><div class="lc-sub">Source code, README, and documentation</div></div></a>';
1461
+ h += '<a class="link-card" href="https://www.npmjs.com/package/@asifkibria/claude-code-toolkit" target="_blank"><div class="lc-icon">&#128230;</div><div><div class="lc-title">npm Package</div><div class="lc-sub">@asifkibria/claude-code-toolkit</div></div></a>';
1462
+ h += '<a class="link-card" href="https://github.com/asifkibria/claude-code-toolkit/issues" target="_blank"><div class="lc-icon">&#128030;</div><div><div class="lc-title">Issues &amp; Feedback</div><div class="lc-sub">Report bugs or request features</div></div></a>';
1463
+ h += '<a class="link-card" href="https://github.com/asifkibria/claude-code-toolkit#readme" target="_blank"><div class="lc-icon">&#128214;</div><div><div class="lc-title">Documentation</div><div class="lc-sub">Setup guide and API reference</div></div></a>';
1464
+ h += '</div>';
1465
+ h += '<div class="card mt"><h3>Quick Start</h3>';
1466
+ h += '<div style="font-family:var(--mono);font-size:12px;line-height:2;color:var(--text2)">';
1467
+ h += '<div><span style="color:var(--text3)"># Install globally</span></div>';
1468
+ h += '<div style="color:var(--accent)">npm install -g @asifkibria/claude-code-toolkit</div>';
1469
+ h += '<div style="margin-top:8px"><span style="color:var(--text3)"># Run health check</span></div>';
1470
+ h += '<div style="color:var(--accent)">cct health</div>';
1471
+ h += '<div style="margin-top:8px"><span style="color:var(--text3)"># Launch dashboard</span></div>';
1472
+ h += '<div style="color:var(--accent)">cct dashboard</div>';
1473
+ h += '<div style="margin-top:8px"><span style="color:var(--text3)"># Use as MCP server</span></div>';
1474
+ h += '<div style="color:var(--accent)">claude mcp add claude-code-toolkit -- npx @asifkibria/claude-code-toolkit</div>';
1475
+ h += '</div></div>';
1476
+ set(el, h); refreshTime();
1477
+ }
1478
+
1479
+ function showProgress(label) {
1480
+ $('#actionLabel').textContent = label || 'Processing...';
1481
+ $('#progressBar').classList.add('active');
1482
+ $('#actionOverlay').classList.add('active');
1483
+ }
1484
+ function hideProgress() {
1485
+ $('#progressBar').classList.remove('active');
1486
+ $('#actionOverlay').classList.remove('active');
1487
+ }
1488
+ function showResult(ok, title, details, tabs, items) {
1489
+ hideProgress();
1490
+ let body = '<div class="detail-panel">';
1491
+ body += '<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px"><div style="width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0;background:' + (ok ? 'var(--green-bg)' : 'var(--red-bg)') + ';color:' + (ok ? 'var(--green)' : 'var(--red)') + '">' + (ok ? '&#10003;' : '&#10007;') + '</div><div style="font-size:15px;font-weight:600">' + esc(title) + '</div></div>';
1492
+ if (details) body += '<div class="result-details" style="display:flex;flex-wrap:wrap;gap:6px 20px;margin-bottom:16px;font-size:13px">' + details + '</div>';
1493
+ if (items && items.length) {
1494
+ body += '<div style="font-size:12px;font-weight:600;color:var(--text2);margin-bottom:8px">Changes (' + items.length + ' item' + (items.length > 1 ? 's' : '') + ')</div>';
1495
+ body += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;max-height:300px;overflow-y:auto">';
1496
+ items.forEach(function (item, i) {
1497
+ const short = function (p) { const parts = p.split('/'); return parts.length > 3 ? '.../' + parts.slice(-3).join('/') : p; };
1498
+ body += '<div style="padding:7px 12px;font-family:var(--mono);font-size:12px;border-bottom:1px solid var(--border);color:var(--text2);display:flex;align-items:center;gap:8px">';
1499
+ body += '<span style="color:' + (ok ? 'var(--green)' : 'var(--red)') + ';font-size:10px;min-width:20px;text-align:right;opacity:0.6">' + (i + 1) + '</span>';
1500
+ body += '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(item) + '">' + esc(short(item)) + '</span></div>';
1501
+ });
1502
+ body += '</div>';
1503
+ }
1504
+ body += '</div>';
1505
+ showModal((ok ? 'Action Complete' : 'Action Failed'), body, 'Close', null, true);
1506
+ if (tabs) { tabs.forEach(t => { loaded[t] = false; }); reloadCurrent(); }
1507
+ }
1508
+ function reloadCurrent() { loaded[currentTab] = false; loadTab(currentTab); }
1509
+ async function runAction(label, fn) {
1510
+ showProgress(label);
1511
+ try { return await fn(); } catch (e) { hideProgress(); toast('Action failed: ' + (e.message || 'unknown'), 'error'); return null; }
1512
+ }
1513
+ function switchTab(t) {
1514
+ $$('.nav-item').forEach(n => { n.classList.toggle('active', n.dataset.tab === t); });
1515
+ $$('.section').forEach(s => s.classList.remove('active'));
1516
+ $('#sec-' + t).classList.add('active');
1517
+ currentTab = t; loadTab(t);
1518
+ }
1519
+
1520
+ async function doFixAll() {
1521
+ showModal('Fix All Issues', 'This will scan and fix all conversations with oversized content. Backups will be created automatically.', 'Fix All', async () => {
1522
+ showProgress('Fixing issues...');
1523
+ const r = await post('fix-all');
1524
+ if (r.success) { showResult(true, 'Fix All Complete', '<span>Fixed: <strong>' + r.fixed + '</strong> file(s)</span><span>Errors: ' + r.errors + '</span><span>Total scanned: ' + r.total + '</span>', ['overview', 'storage'], r.fixedFiles); }
1525
+ else { showResult(false, 'Fix Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>', null, r.errorFiles); }
1526
+ });
1527
+ }
1528
+ async function doCleanPreview() {
1529
+ showProgress('Analyzing directory...');
1530
+ const r = await post('clean', { dryRun: true });
1531
+ hideProgress();
1532
+ if (r.deleted > 0) {
1533
+ showModal('Clean Directory', 'Found ' + r.deleted + ' items to clean (' + fmtB(r.freed) + ' freeable). Proceed with cleanup?', 'Clean', async () => {
1534
+ showProgress('Cleaning directory...');
1535
+ const r2 = await post('clean', { dryRun: false });
1536
+ showResult(true, 'Cleanup Complete', '<span>Deleted: <strong>' + r2.deleted + '</strong> items</span><span>Freed: <strong>' + fmtB(r2.freed) + '</strong></span>', ['storage', 'overview'], r2.items);
1537
+ });
1538
+ } else toast('Nothing to clean', 'info');
1539
+ }
1540
+ async function doCleanExecute() {
1541
+ showModal('Clean Directory', 'This will permanently delete cleanable items (debug logs, empty files, old snapshots). Continue?', 'Clean Now', async () => {
1542
+ showProgress('Cleaning directory...');
1543
+ const r = await post('clean', { dryRun: false });
1544
+ showResult(true, 'Cleanup Complete', '<span>Deleted: <strong>' + r.deleted + '</strong> items</span><span>Freed: <strong>' + fmtB(r.freed) + '</strong></span>', ['storage', 'overview'], r.items);
1545
+ });
1546
+ }
1547
+ async function doRepair(sid) {
1548
+ showModal('Repair Session', 'This will remove corrupted lines from session ' + sid.slice(0, 12) + '... A backup will be created.', 'Repair', async () => {
1549
+ showProgress('Repairing session...');
1550
+ const r = await post('repair', { sessionId: sid });
1551
+ if (r.success) { showResult(true, 'Session Repaired', '<span>Lines removed: <strong>' + r.linesRemoved + '</strong></span><span>Lines fixed: <strong>' + r.linesFixed + '</strong></span><span>Backup: ' + esc(r.backupPath?.split('/').pop() || 'created') + '</span>', ['sessions', 'overview'], r.backupPath ? [r.backupPath] : null); }
1552
+ else { showResult(false, 'Repair Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1553
+ });
1554
+ }
1555
+ async function doExtract(sid) {
1556
+ showProgress('Extracting session data...');
1557
+ try {
1558
+ const r = await post('extract', { sessionId: sid });
1559
+ hideProgress();
1560
+ if (!r.success) {
1561
+ showModal('Extract Failed', '<div class="empty-state"><div class="empty-icon">&#9888;</div><div class="empty-title">Extract Error</div><div class="empty-sub">' + esc(r.error || 'Could not extract session data. The session may be corrupted or inaccessible.') + '</div></div>', 'Close', null, true);
1562
+ return;
1563
+ }
1564
+ const isEmpty = !r.userMessages && !r.assistantMessages && !r.fileEdits && !r.commandsRun;
1565
+ let body = '<div class="detail-panel">';
1566
+ if (isEmpty) {
1567
+ body += '<div class="empty-state" style="padding:32px 16px"><div class="empty-icon">&#128196;</div><div class="empty-title">Empty Session</div><div class="empty-sub">This session has no messages, file edits, or commands recorded.</div></div>';
1568
+ } else {
1569
+ body += '<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:12px">';
1570
+ const stats = [['User Messages', r.userMessages, 'var(--accent)'], ['Assistant Messages', r.assistantMessages, 'var(--green)'], ['File Edits', r.fileEdits, 'var(--yellow)'], ['Commands', r.commandsRun, 'var(--purple)']];
1571
+ stats.forEach(s => { body += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;text-align:center"><div style="font-size:10px;color:var(--text3);text-transform:uppercase;font-weight:600;letter-spacing:0.5px;margin-bottom:2px">' + s[0] + '</div><div style="font-size:20px;font-weight:700;color:' + s[2] + '">' + s[1] + '</div></div>'; });
1572
+ body += '</div>';
1573
+ if (r.sampleMessages?.length) { body += '<div style="font-size:12px;font-weight:600;color:var(--accent);margin-bottom:8px">Sample Messages</div>'; body += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;max-height:250px;overflow-y:auto">'; r.sampleMessages.forEach(m => { body += '<div style="padding:7px 12px;font-family:var(--mono);font-size:12px;border-bottom:1px solid var(--border);color:var(--text2)">' + esc(m) + '</div>'; }); body += '</div>'; }
1574
+ if (r.editedFiles?.length) { body += '<div style="font-size:12px;font-weight:600;color:var(--green);margin:12px 0 8px">Edited Files</div>'; body += auditList(r.editedFiles, 'var(--green)'); }
1575
+ }
1576
+ body += '</div>';
1577
+ showModal('Session Extract: ' + sid.slice(0, 12), body, 'Close', null, true);
1578
+ } catch (e) { hideProgress(); showModal('Extract Failed', '<div class="empty-state"><div class="empty-icon">&#9888;</div><div class="empty-title">Extract Error</div><div class="empty-sub">Could not complete the extraction. The session file may be corrupted.</div></div>', 'Close', null, true); }
1579
+ }
1580
+ function switchAuditTab(btn, paneId) {
1581
+ const panel = btn.closest('.detail-panel');
1582
+ panel.querySelectorAll('.audit-tab').forEach(t => t.classList.remove('active'));
1583
+ btn.classList.add('active');
1584
+ panel.querySelectorAll('.audit-pane').forEach(p => p.classList.remove('active'));
1585
+ panel.querySelector('#' + paneId).classList.add('active');
1586
+ }
1587
+ function auditList(items, color) {
1588
+ if (!items || !items.length) return '<div style="padding:16px;color:var(--text3);text-align:center;font-size:13px">None</div>';
1589
+ const short = p => { const parts = p.split('/'); return parts.length > 3 ? '.../' + parts.slice(-2).join('/') : p; };
1590
+ let h = '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;max-height:400px;overflow-y:auto">';
1591
+ items.forEach((item, i) => {
1592
+ h += '<div style="padding:7px 12px;font-family:var(--mono);font-size:12px;border-bottom:1px solid var(--border);color:var(--text2);display:flex;align-items:center;gap:8px">';
1593
+ h += '<span style="color:' + color + ';font-size:10px;min-width:24px;text-align:right;opacity:0.6">' + (i + 1) + '</span>';
1594
+ h += '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(item) + '">' + esc(short(item)) + '</span></div>';
1595
+ });
1596
+ h += '</div>';
1597
+ return h;
1598
+ }
1599
+ async function doAudit(sid) {
1600
+ showProgress('Auditing session...');
1601
+ try {
1602
+ const r = await fetch('/api/session/' + encodeURIComponent(sid) + '/audit');
1603
+ hideProgress();
1604
+ if (!r.ok) {
1605
+ const err = await r.json().catch(() => ({}));
1606
+ showModal('Audit Failed', '<div class="empty-state"><div class="empty-icon">&#9888;</div><div class="empty-title">' + (r.status === 404 ? 'Session Not Found' : 'Audit Error') + '</div><div class="empty-sub">' + (esc(err.error || 'Could not read session data. The session may be corrupted or inaccessible.')) + '</div></div>', 'Close', null, true);
1607
+ return;
1608
+ }
1609
+ const d = await r.json();
1610
+ const fr = Array.isArray(d.filesRead) ? d.filesRead : [];
1611
+ const fw = Array.isArray(d.filesWritten) ? d.filesWritten : [];
1612
+ const cr = Array.isArray(d.commandsRun) ? d.commandsRun : [];
1613
+ const mt = Array.isArray(d.mcpToolsUsed) ? d.mcpToolsUsed : [];
1614
+ const uf = Array.isArray(d.urlsFetched) ? d.urlsFetched : [];
1615
+ let body = '<div class="detail-panel">';
1616
+ if (d.project) body += '<div style="font-size:12px;color:var(--text3);margin-bottom:12px;font-family:var(--mono)">' + esc(d.project) + '</div>';
1617
+ if (d.totalActions === 0) {
1618
+ body += '<div class="empty-state" style="padding:32px 16px"><div class="empty-icon">&#128196;</div><div class="empty-title">No Activity Recorded</div><div class="empty-sub">This session has no file reads, writes, commands, or tool usage recorded.</div></div>';
1619
+ } else {
1620
+ body += '<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(90px,1fr));gap:8px;margin-bottom:12px">';
1621
+ const mc = [['Total', d.totalActions, 'var(--text)'], ['Read', fr.length, 'var(--accent)'], ['Written', fw.length, 'var(--green)'], ['Commands', cr.length, 'var(--yellow)'], ['MCP', mt.length, 'var(--purple)'], ['URLs', uf.length, 'var(--orange)']];
1622
+ mc.forEach(m => { body += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px;text-align:center"><div style="font-size:10px;color:var(--text3);text-transform:uppercase;font-weight:600;letter-spacing:0.5px;margin-bottom:2px">' + m[0] + '</div><div style="font-size:20px;font-weight:700;color:' + m[2] + '">' + m[1] + '</div></div>'; });
1623
+ body += '</div>';
1624
+ const tabs = [['files', 'Files (' + fr.length + '/' + fw.length + ')'], ['cmds', 'Commands (' + cr.length + ')'], ['mcp', 'MCP (' + mt.length + ')'], ['urls', 'URLs (' + uf.length + ')']];
1625
+ body += '<div class="audit-tabs">';
1626
+ tabs.forEach((t, i) => { body += '<div class="audit-tab' + (i === 0 ? ' active' : '') + '" onclick="switchAuditTab(this,\\'ap - '+t[0]+'\\')">' + t[1] + '</div>'; });
1627
+ body += '</div>';
1628
+ body += '<div class="audit-pane active" id="ap-files">';
1629
+ if (fr.length) { body += '<div style="font-size:12px;font-weight:600;color:var(--accent);margin-bottom:8px">Files Read (' + fr.length + ')</div>'; body += auditList(fr, 'var(--accent)'); }
1630
+ if (fw.length) { body += '<div style="font-size:12px;font-weight:600;color:var(--green);margin:' + (fr.length ? '12px' : '0') + ' 0 8px">Files Written (' + fw.length + ')</div>'; body += auditList(fw, 'var(--green)'); }
1631
+ if (!fr.length && !fw.length) body += '<div style="padding:24px;text-align:center;color:var(--text3)">No file operations</div>';
1632
+ body += '</div>';
1633
+ body += '<div class="audit-pane" id="ap-cmds">' + auditList(cr, 'var(--yellow)') + '</div>';
1634
+ body += '<div class="audit-pane" id="ap-mcp">' + auditList(mt, 'var(--purple)') + '</div>';
1635
+ body += '<div class="audit-pane" id="ap-urls">' + auditList(uf, 'var(--orange)') + '</div>';
1636
+ }
1637
+ body += '</div>';
1638
+ showModal('Session Audit: ' + sid.slice(0, 12), body, 'Close', null, true);
1639
+ $$('#sessionTableBody tr').forEach(r => r.classList.remove('session-row-active'));
1640
+ const row = $('#sessionTableBody tr[data-sid^="' + sid.slice(0, 12) + '"]') || $('#sessionTableBody tr[data-sid="' + sid + '"]');
1641
+ if (row) row.classList.add('session-row-active');
1642
+ } catch (e) { hideProgress(); showModal('Audit Failed', '<div class="empty-state"><div class="empty-icon">&#9888;</div><div class="empty-title">Audit Error</div><div class="empty-sub">Could not complete the audit. The session file may be corrupted.</div></div>', 'Close', null, true); }
1643
+ }
1644
+ function showTraceCategoryFiles(categoryName) {
1645
+ const categories = window._traceCategories || [];
1646
+ const cat = categories.find(c => c.name === categoryName);
1647
+ if (!cat || !cat.allFiles?.length) { toast('No files found for this category', 'info'); return; }
1648
+ let body = '<div style="max-height:500px;overflow-y:auto">';
1649
+ body += '<div style="font-size:12px;color:var(--text3);margin-bottom:12px">' + cat.fileCount + ' files, ' + fmtB(cat.totalSize) + ' total</div>';
1650
+ body += '<table style="width:100%"><tr><th style="text-align:left">Project/File</th><th>Size</th><th>Modified</th><th>Full Path</th></tr>';
1651
+ cat.allFiles.forEach(f => {
1652
+ body += '<tr>';
1653
+ body += '<td style="font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis" title="' + esc(f.fullPath) + '">' + esc(f.projectName || f.path) + '</td>';
1654
+ body += '<td style="font-size:11px;white-space:nowrap">' + fmtB(f.size) + '</td>';
1655
+ body += '<td style="font-size:11px;white-space:nowrap;color:var(--text3)">' + ago(f.modified) + '</td>';
1656
+ body += '<td style="font-size:9px;max-width:300px;word-break:break-all;color:var(--text3)">' + esc(f.fullPath) + '</td>';
1657
+ body += '</tr>';
1658
+ });
1659
+ body += '</table></div>';
1660
+ showModal('Files in ' + categoryName + ' (' + cat.fileCount + ')', body, 'Close', null, true);
1661
+ }
1662
+ async function doCleanTracesPreview() {
1663
+ showProgress('Analyzing traces...');
1664
+ let preview;
1665
+ const exclusions = loadExclusions().exclusions;
1666
+ try {
1667
+ preview = await post('preview-traces', { operation: 'clean', options: { exclusions } });
1668
+ } catch (e) { hideProgress(); toast('Error: ' + e.message, 'error'); return; }
1669
+ hideProgress();
1670
+ if (!preview.success) { toast('Error: ' + (preview.error || 'Preview failed'), 'error'); return; }
1671
+ if (!preview.summary || preview.summary.totalFiles === 0) { toast('Nothing to clean (exclusions may be protecting files)', 'info'); return; }
1672
+ let body = '<div class="detail-panel">';
1673
+ body += '<div style="font-size:14px;font-weight:600;margin-bottom:16px">Will delete ' + preview.summary.totalFiles + ' files (' + fmtB(preview.summary.totalSize) + '):</div>';
1674
+ body += '<div style="max-height:250px;overflow-y:auto;margin-bottom:16px">';
1675
+ (preview.byCategory || []).forEach(c => {
1676
+ if (c.fileCount === 0) return;
1677
+ const col = c.sensitivity === 'critical' ? 'var(--red)' : c.sensitivity === 'high' ? 'var(--orange)' : c.sensitivity === 'medium' ? 'var(--yellow)' : 'var(--green)';
1678
+ body += '<div style="margin-bottom:12px">';
1679
+ body += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">';
1680
+ body += '<span style="background:' + col + ';color:#fff;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600">' + c.sensitivity.toUpperCase() + '</span>';
1681
+ body += '<span style="font-weight:600">' + esc(c.name) + '</span>';
1682
+ body += '<span style="color:var(--text3);font-size:12px">(' + c.fileCount + ' files, ' + fmtB(c.totalSize) + ')</span>';
1683
+ body += '</div>';
1684
+ body += '<div style="font-size:12px;color:var(--text3);margin-bottom:4px">' + esc(c.description) + '</div>';
1685
+ if (c.samplePaths?.length) {
1686
+ body += '<div style="font-family:var(--mono);font-size:11px;color:var(--text3);padding-left:12px">';
1687
+ c.samplePaths.slice(0, 3).forEach(p => { body += '> ' + esc(p) + '<br>'; });
1688
+ if (c.fileCount > 3) body += '...and ' + (c.fileCount - 3) + ' more<br>';
1689
+ body += '</div>';
1690
+ }
1691
+ body += '</div>';
1692
+ });
1693
+ body += '</div>';
1694
+ if (preview.preserved && preview.preserved.totalPreserved > 0) {
1695
+ body += '<div style="border-top:1px solid var(--border);padding-top:12px;margin-top:12px">';
1696
+ body += '<div style="font-size:13px;font-weight:600;color:var(--green);margin-bottom:8px">✓ Will PRESERVE (' + preview.preserved.byExclusion.length + ' exclusions):</div>';
1697
+ preview.preserved.byExclusion.forEach(exc => {
1698
+ body += '<div style="font-size:12px;color:var(--text2);margin-bottom:4px">• ' + esc(exc.exclusion.type) + ' "' + esc(exc.exclusion.value) + '": ' + exc.matchedFiles + ' files</div>';
1699
+ });
1700
+ body += '</div>';
1701
+ }
1702
+ body += '</div>';
1703
+ showModal('Clean Traces Preview', body, 'Clean ' + preview.summary.totalFiles + ' Files', async () => {
1704
+ showProgress('Cleaning traces...');
1705
+ const r = await post('clean-traces', { dryRun: false, exclusions });
1706
+ showResult(true, 'Traces Cleaned',
1707
+ '<span>Deleted: <strong>' + r.deleted + '</strong> files</span>' +
1708
+ '<span>Freed: <strong>' + fmtB(r.freed) + '</strong></span>' +
1709
+ '<span>Categories: ' + (r.categoriesAffected?.join(', ') || 'all') + '</span>',
1710
+ ['traces', 'overview'], r.items);
1711
+ }, true);
1712
+ }
1713
+ async function doCleanTracesExecute() {
1714
+ const exclusions = loadExclusions().exclusions;
1715
+ showModal('Clean All Traces', 'This will permanently delete trace files (respecting your exclusions). Continue?', 'Clean Now', async () => {
1716
+ showProgress('Cleaning traces...');
1717
+ const r = await post('clean-traces', { dryRun: false, exclusions });
1718
+ showResult(true, 'Traces Cleaned', '<span>Deleted: <strong>' + r.deleted + '</strong> files</span><span>Freed: <strong>' + fmtB(r.freed) + '</strong></span>', ['traces', 'overview'], r.items);
1719
+ });
1720
+ }
1721
+ async function doWipeTraces() {
1722
+ showProgress('Analyzing traces...');
1723
+ wipeState = { step: 1, preview: null, confirmPhrase: 'WIPE ALL', userInput: '' };
1724
+ const exclusions = loadExclusions().exclusions;
1725
+ const preview = await post('preview-traces', { operation: 'wipe', options: { exclusions } });
1726
+ hideProgress();
1727
+ if (!preview.success) {
1728
+ toast('Could not analyze traces', 'error');
1729
+ return;
1730
+ }
1731
+ if (preview.summary.totalFiles === 0) {
1732
+ toast('Nothing to wipe (exclusions may be protecting all files)', 'info');
1733
+ return;
1734
+ }
1735
+ wipeState.preview = preview;
1736
+ showWipeStep1();
1737
+ }
1738
+ async function doPreviewFinding(file, line) {
1739
+ showProgress('Loading preview...');
1740
+ try {
1741
+ const r = await fetch('/api/security/finding/' + encodeURIComponent(file) + '/' + line);
1742
+ hideProgress();
1743
+ if (!r.ok) { toast('Could not load preview', 'error'); return; }
1744
+ const d = await r.json();
1745
+ const body = '<p style="font-size:12px;color:var(--text3)">File: ' + esc(file.split('/').pop()) + ' | Line: ' + line + ' | Total lines: ' + (d.totalLines || '?') + '</p><pre>' + esc(d.preview || d.raw || 'No content') + '</pre>';
1746
+ showModal('Finding Preview', body, 'Close', () => { }, true);
1747
+ } catch (e) { hideProgress(); toast('Preview failed', 'error'); }
1748
+ }
1749
+ async function doRedact(file, line, pattern) {
1750
+ showModal('Redact Secret', 'This will replace the secret on line ' + line + ' with [REDACTED]. A backup of the file will be created first.', 'Redact', async () => {
1751
+ showProgress('Redacting secret...');
1752
+ const r = await post('redact', { file: file, line: line, pattern: pattern });
1753
+ if (r.success) { showResult(true, 'Secret Redacted', '<span>Secrets redacted: <strong>' + r.redactedCount + '</strong></span><span>Backup: ' + esc(r.backupPath?.split('/').pop() || 'created') + '</span>', ['security', 'overview'], r.backupPath ? [r.backupPath] : null); }
1754
+ else { showResult(false, 'Redaction Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1755
+ });
1756
+ }
1757
+ async function doRedactAll() {
1758
+ showModal('Redact ALL Secrets', 'This will redact ALL detected secrets across ALL conversation files. Backups will be created for each modified file. This action cannot be undone (except by restoring backups).', 'Redact All', async () => {
1759
+ showProgress('Redacting all secrets...');
1760
+ const r = await post('redact-all');
1761
+ if (r.success) { showResult(true, 'All Secrets Redacted', '<span>Files modified: <strong>' + r.filesModified + '</strong></span><span>Secrets redacted: <strong>' + r.secretsRedacted + '</strong></span>' + (r.errors?.length ? '<span class="c-orange">Errors: ' + r.errors.length + '</span>' : ''), ['security', 'overview', 'backups'], r.items); }
1762
+ else { showResult(false, 'Redaction Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1763
+ });
1764
+ }
1765
+ async function doTestMcp() {
1766
+ showProgress('Testing MCP servers...');
1767
+ const r = await post('test-mcp');
1768
+ hideProgress();
1769
+ let h = '<div class="detail-panel">';
1770
+ h += '<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:16px">';
1771
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;text-align:center"><div style="font-size:10px;color:var(--text3);text-transform:uppercase;font-weight:600;letter-spacing:0.5px;margin-bottom:2px">Total</div><div style="font-size:20px;font-weight:700">' + r.totalServers + '</div></div>';
1772
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;text-align:center"><div style="font-size:10px;color:var(--text3);text-transform:uppercase;font-weight:600;letter-spacing:0.5px;margin-bottom:2px">Healthy</div><div style="font-size:20px;font-weight:700;color:var(--green)">' + r.healthyServers + '</div></div>';
1773
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px;text-align:center"><div style="font-size:10px;color:var(--text3);text-transform:uppercase;font-weight:600;letter-spacing:0.5px;margin-bottom:2px">Failed</div><div style="font-size:20px;font-weight:700;color:var(--red)">' + (r.totalServers - r.healthyServers) + '</div></div>';
1774
+ h += '</div>';
1775
+ if (r.configs) {
1776
+ h += '<div style="background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden">';
1777
+ r.configs.forEach(c => {
1778
+ c.servers.forEach(s => {
1779
+ const ok = !c.issues.some(i => i.server === s.name && i.severity === 'error');
1780
+ h += '<div style="padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">';
1781
+ h += '<div style="display:flex;align-items:center;gap:8px"><span class="status-dot ' + (ok ? 'dot-green' : 'dot-red') + '"></span><span style="font-weight:500;font-size:13px">' + esc(s.name) + '</span></div>';
1782
+ h += '<span class="badge ' + (ok ? 'b-green' : 'b-red') + '">' + (ok ? 'Pass' : 'Fail') + '</span>';
1783
+ h += '</div>';
1784
+ });
1785
+ });
1786
+ h += '</div>';
1787
+ }
1788
+ if (r.recommendations?.length) {
1789
+ h += '<div style="margin-top:12px;font-size:12px;font-weight:600;color:var(--text2);margin-bottom:6px">Recommendations</div>';
1790
+ r.recommendations.forEach(rec => { h += '<div style="padding:4px 0;font-size:12px;color:var(--text3)">&#8226; ' + esc(rec) + '</div>'; });
1791
+ }
1792
+ h += '</div>';
1793
+ showModal('MCP Server Test Results', h, 'Close', null, true);
1794
+ }
1795
+ async function doDeleteBackups() {
1796
+ showModal('Delete Old Backups', 'This will delete all backup files older than 7 days. This cannot be undone.', 'Delete', async () => {
1797
+ showProgress('Deleting old backups...');
1798
+ const r = await post('delete-backups', { days: 7 });
1799
+ if (r.success) { showResult(true, 'Backups Cleaned', '<span>Deleted: <strong>' + r.deleted + '</strong> backup(s)</span>', ['backups', 'storage', 'overview'], r.items); }
1800
+ else { showResult(false, 'Delete Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1801
+ });
1802
+ }
1803
+ async function doRestore(backupPath) {
1804
+ showModal('Restore Backup', 'This will restore the conversation from this backup, replacing the current file. Continue?', 'Restore', async () => {
1805
+ showProgress('Restoring from backup...');
1806
+ const r = await post('restore', { backupPath: backupPath });
1807
+ if (r.success) { showResult(true, 'Backup Restored', '<span>Restored to: <strong>' + esc(r.originalPath?.split('/').pop() || 'original') + '</strong></span>', ['backups', 'sessions', 'overview'], r.originalPath ? [r.originalPath] : null); }
1808
+ else { showResult(false, 'Restore Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1809
+ });
1810
+ }
1811
+ async function doArchive() {
1812
+ showModal('Archive Conversations', 'This will archive conversations inactive for 30+ days. They can be restored later from backups.', 'Archive', async () => {
1813
+ showProgress('Archiving conversations...');
1814
+ const r = await post('archive', { dryRun: false, days: 30 });
1815
+ if (r.success) { showResult(true, 'Archive Complete', '<span>Archived: <strong>' + r.archived + '</strong> conversations</span><span>Space freed: <strong>' + fmtB(r.spaceFreed) + '</strong></span>', ['maintenance', 'overview', 'sessions'], r.items); }
1816
+ else { showResult(false, 'Archive Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1817
+ });
1818
+ }
1819
+ async function doMaintenance() {
1820
+ showModal('Run Maintenance', 'This will execute all pending maintenance actions (cleanup empty files, remove orphans, optimize storage). Continue?', 'Run Maintenance', async () => {
1821
+ showProgress('Running maintenance...');
1822
+ const r = await post('maintenance', { auto: true });
1823
+ if (r.success) {
1824
+ let det = '<span>Actions performed: <strong>' + r.actionsPerformed + '</strong></span>';
1825
+ const actionItems = r.actions?.map(a => a.type + ': ' + a.description) || [];
1826
+ showResult(true, 'Maintenance Complete', det, ['maintenance', 'overview', 'storage'], actionItems);
1827
+ } else { showResult(false, 'Maintenance Failed', '<span>' + esc(r.error || 'Unknown error') + '</span>'); }
1828
+ });
1829
+ }
1830
+
1831
+ function toggleMcpServer(id) {
1832
+ const el = $('#' + id); const arrow = $('#' + id + '-arrow');
1833
+ if (!el) return;
1834
+ if (el.style.display === 'none') { el.style.display = 'block'; if (arrow) arrow.innerHTML = '&#9660;'; }
1835
+ else { el.style.display = 'none'; if (arrow) arrow.innerHTML = '&#9654;'; }
1836
+ }
1837
+ async function probeMcpServer(name) {
1838
+ const srvId = 'mcp-srv-' + name.replace(/[^a-zA-Z0-9]/g, '_');
1839
+ const capsEl = $('#' + srvId + '-caps'); const badgeEl = $('#' + srvId + '-badge');
1840
+ if (capsEl) capsEl.innerHTML = '<div style="color:var(--text3);font-size:11px">Probing server...</div>';
1841
+ try {
1842
+ const resp = await fetch('/api/mcp/server/' + encodeURIComponent(name) + '/capabilities');
1843
+ const data = await resp.json();
1844
+ if (!resp.ok || data.error) {
1845
+ if (capsEl) capsEl.innerHTML = '<div style="color:var(--red);font-size:11px">Error: ' + (data.error || 'Failed to probe') + '</div>';
1846
+ if (badgeEl) badgeEl.textContent = 'error';
1847
+ return;
1848
+ }
1849
+ let h = '';
1850
+ const toolCount = data.tools?.length || 0;
1851
+ const resCount = data.resources?.length || 0;
1852
+ const promptCount = data.prompts?.length || 0;
1853
+ if (badgeEl) badgeEl.textContent = toolCount + ' tools, ' + resCount + ' res';
1854
+ if (data.serverInfo?.name || data.serverInfo?.version) {
1855
+ h += '<div style="font-size:11px;color:var(--text2);margin-bottom:8px"><strong>Server:</strong> ' + (data.serverInfo.name || 'Unknown') + ' v' + (data.serverInfo.version || '?') + '</div>';
1856
+ }
1857
+ h += '<div style="font-size:11px;color:var(--text3);margin-bottom:4px">Probe time: ' + data.probeTime + 'ms</div>';
1858
+ if (toolCount > 0) {
1859
+ h += '<div style="margin-top:10px"><div style="font-size:11px;font-weight:600;color:var(--accent);margin-bottom:4px">Tools (' + toolCount + ')</div>';
1860
+ h += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
1861
+ data.tools.slice(0, 20).forEach(t => { h += '<span title="' + (t.description || 'No description').replace(/"/g, '&quot;') + '" style="background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:10px;cursor:help">' + esc(t.name) + '</span>'; });
1862
+ if (toolCount > 20) h += '<span style="font-size:10px;color:var(--text3)">+' + Math.max(toolCount - 20, 0) + ' more</span>';
1863
+ h += '</div></div>';
1864
+ }
1865
+ if (resCount > 0) {
1866
+ h += '<div style="margin-top:10px"><div style="font-size:11px;font-weight:600;color:var(--green);margin-bottom:4px">Resources (' + resCount + ')</div>';
1867
+ h += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
1868
+ data.resources.slice(0, 10).forEach(r => { h += '<span title="' + (r.description || r.uri || '').replace(/"/g, '&quot;') + '" style="background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:10px;cursor:help">' + (r.name || r.uri.split('/').pop() || r.uri) + '</span>'; });
1869
+ if (resCount > 10) h += '<span style="font-size:10px;color:var(--text3)">+' + Math.max(resCount - 10, 0) + ' more</span>';
1870
+ h += '</div></div>';
1871
+ }
1872
+ if (promptCount > 0) {
1873
+ h += '<div style="margin-top:10px"><div style="font-size:11px;font-weight:600;color:var(--orange);margin-bottom:4px">Prompts (' + promptCount + ')</div>';
1874
+ h += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
1875
+ data.prompts.slice(0, 10).forEach(p => { h += '<span title="' + (p.description || 'No description').replace(/"/g, '&quot;') + '" style="background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:10px;cursor:help">' + esc(p.name) + '</span>'; });
1876
+ if (promptCount > 10) h += '<span style="font-size:10px;color:var(--text3)">+' + Math.max(promptCount - 10, 0) + ' more</span>';
1877
+ h += '</div></div>';
1878
+ }
1879
+ if (toolCount === 0 && resCount === 0 && promptCount === 0) { h += '<div style="color:var(--text3);font-size:11px;margin-top:8px">No tools, resources, or prompts discovered</div>'; }
1880
+ if (capsEl) capsEl.innerHTML = h;
1881
+ const parentEl = $('#' + srvId);
1882
+ if (parentEl && parentEl.style.display === 'none') toggleMcpServer(srvId);
1883
+ } catch (e) {
1884
+ if (capsEl) capsEl.innerHTML = '<div style="color:var(--red);font-size:11px">Error: ' + e.message + '</div>';
1885
+ if (badgeEl) badgeEl.textContent = 'error';
1886
+ }
1887
+ }
1888
+ function showAddMcpModal() {
1889
+ let body = '<div style="text-align:left">';
1890
+ body += '<div style="margin-bottom:12px"><label style="display:block;font-size:12px;font-weight:500;margin-bottom:4px">Server Name *</label><input id="addMcpName" type="text" placeholder="my-server" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)"></div>';
1891
+ body += '<div style="margin-bottom:12px"><label style="display:block;font-size:12px;font-weight:500;margin-bottom:4px">Command *</label><input id="addMcpCmd" type="text" placeholder="node, npx, python, etc." style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)"></div>';
1892
+ body += '<div style="margin-bottom:12px"><label style="display:block;font-size:12px;font-weight:500;margin-bottom:4px">Arguments (space separated)</label><input id="addMcpArgs" type="text" placeholder="/path/to/server.js --port 3000" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)"></div>';
1893
+ body += '<div style="margin-bottom:12px"><label style="display:block;font-size:12px;font-weight:500;margin-bottom:4px">Environment Variables (KEY=value, one per line)</label><textarea id="addMcpEnv" placeholder="API_KEY=xxx\\nDEBUG=true" rows="3" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text);font-family:monospace"></textarea></div>';
1894
+ body += '<div style="margin-bottom:12px"><label style="display:block;font-size:12px;font-weight:500;margin-bottom:4px">Config Target</label><select id="addMcpTarget" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text)"><option value="global">Global (~/.claude.json)</option><option value="project">Project (.mcp.json)</option></select></div>';
1895
+ body += '<div style="font-size:11px;color:var(--text3);margin-top:8px">MCP servers extend Claude Code with custom tools and resources.</div>';
1896
+ body += '</div>';
1897
+ showModal('Add MCP Server', body, 'Add Server', async () => {
1898
+ const name = $('#addMcpName')?.value?.trim();
1899
+ const cmd = $('#addMcpCmd')?.value?.trim();
1900
+ const argsRaw = $('#addMcpArgs')?.value?.trim();
1901
+ const envRaw = $('#addMcpEnv')?.value?.trim();
1902
+ const target = $('#addMcpTarget')?.value || 'global';
1903
+ if (!name || !cmd) { toast('Name and command are required', 'error'); return; }
1904
+ const args = argsRaw ? argsRaw.split(/\\s+/).filter(a => a) : [];
1905
+ const env = {};
1906
+ if (envRaw) { envRaw.split('\\n').forEach(line => { const [k, ...v] = line.split('='); if (k) env[k.trim()] = v.join('=').trim(); }); }
1907
+ showProgress('Adding MCP server...');
1908
+ const r = await post('add-mcp-server', { name, command: cmd, args, env, target });
1909
+ hideProgress();
1910
+ if (r.success) { toast('MCP server added: ' + name, 'success'); loadTab('mcp'); }
1911
+ else { toast('Failed: ' + (r.error || 'Unknown error'), 'error'); }
1912
+ }, true);
1913
+ }
1914
+
1915
+ function showSettings() {
1916
+ $('#settingsModal').classList.add('active');
1917
+ api('config').then(c => {
1918
+ if (c.settings && c.settings.content) {
1919
+ try {
1920
+ const s = JSON.parse(c.settings.content);
1921
+ if (s.scanner) {
1922
+ $('#setConfigMinText').value = s.scanner.minTextSize || '';
1923
+ $('#setConfigMinBase64').value = s.scanner.minBase64Size || '';
1924
+ }
1925
+ } catch { }
1926
+ }
1927
+ });
1928
+ }
1929
+
1930
+ async function saveSettings() {
1931
+ const minText = parseInt($('#setConfigMinText').value, 10);
1932
+ const minBase64 = parseInt($('#setConfigMinBase64').value, 10);
1933
+
1934
+ if (isNaN(minText) && isNaN(minBase64)) { toast('Please enter valid numbers', 'error'); return; }
1935
+
1936
+ showProgress('Saving settings...');
1937
+ try {
1938
+ const c = await api('config');
1939
+ let settings = {};
1940
+ if (c.settings && c.settings.content) {
1941
+ try { settings = JSON.parse(c.settings.content); } catch { }
1942
+ }
1943
+
1944
+ if (!settings.scanner) settings.scanner = {};
1945
+ if (!isNaN(minText)) settings.scanner.minTextSize = minText;
1946
+ if (!isNaN(minBase64)) settings.scanner.minBase64Size = minBase64;
1947
+
1948
+ const r = await post('action/save-config', { type: 'settings', content: JSON.stringify(settings, null, 2) });
1949
+ hideProgress();
1950
+ if (r.success) {
1951
+ toast('Settings saved successfully', 'success');
1952
+ $('#settingsModal').classList.remove('active');
1953
+ } else {
1954
+ toast('Failed to save: ' + r.error, 'error');
1955
+ }
1956
+ } catch (e) {
1957
+ hideProgress();
1958
+ toast('Error: ' + e.message, 'error');
1959
+ }
1960
+ }
1961
+
1962
+
1963
+ async function loadSnapshots() {
1964
+ showProgress('Loading snapshots...');
1965
+ const r = await api('snapshots');
1966
+ hideProgress();
1967
+ const tbody = $('#snapshotTableBody');
1968
+ if(!tbody) return;
1969
+ tbody.innerHTML = '';
1970
+
1971
+ if(!r.snapshots || r.snapshots.length === 0) {
1972
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:20px;color:var(--text3)">No snapshots found</td></tr>';
1973
+ return;
1974
+ }
1975
+
1976
+ r.snapshots.forEach(s => {
1977
+ const tr = document.createElement('tr');
1978
+ tr.innerHTML = \`
1979
+ <td>\${new Date(s.date).toLocaleString()}</td>
1980
+ <td>\${esc(s.label)}</td>
1981
+ <td>\${fmtB(s.size)}</td>
1982
+ <td style="font-family:var(--mono);font-size:12px">\${s.id}</td>
1983
+ <td>
1984
+ <button class="btn-small" onclick="doCompareSelection('\${s.id}')">Compare</button>
1985
+ <button class="btn-small btn-danger" onclick="doDeleteSnapshot('\${s.id}')">Delete</button>
1986
+ </td>
1987
+ \`;
1988
+ tbody.appendChild(tr);
1989
+ });
1990
+ }
1991
+
1992
+ async function doTakeSnapshot() {
1993
+ showModal('Take Snapshot', 'Label for this snapshot:', 'Create', async () => {
1994
+ const label = $('#snapshotLabelInput')?.value || 'Manual Snapshot';
1995
+ showProgress('Taking snapshot...');
1996
+ const r = await post('action/snapshot', { label });
1997
+ hideProgress();
1998
+ if(r.success) { toast('Snapshot created', 'success'); loadSnapshots(); }
1999
+ else { toast('Failed: '+r.error, 'error'); }
2000
+ });
2001
+ // Inject input into modal body (hacky but works with current showModal)
2002
+ setTimeout(() => {
2003
+ const body = document.querySelector('.modal-body');
2004
+ if(body) body.innerHTML = '<input id="snapshotLabelInput" type="text" placeholder="e.g. Before cleanup" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);color:var(--text);margin-top:8px">';
2005
+ }, 50);
2006
+ }
2007
+
2008
+ async function doDeleteSnapshot(id) {
2009
+ if(!confirm('Delete this snapshot?')) return;
2010
+ showProgress('Deleting...');
2011
+ const r = await post('action/delete-snapshot', { id });
2012
+ hideProgress();
2013
+ if(r.success) { toast('Deleted', 'success'); loadSnapshots(); }
2014
+ else { toast('Failed: '+r.error, 'error'); }
2015
+ }
2016
+
2017
+ let compareBase = null;
2018
+ function doCompareSelection(id) {
2019
+ if(!compareBase) {
2020
+ compareBase = id;
2021
+ toast('Select another snapshot to compare with', 'info');
2022
+ // Visual feedback?
2023
+ $$('#snapshotTableBody tr').forEach(tr => {
2024
+ if(tr.innerHTML.includes(id)) tr.style.background = 'rgba(var(--accent-rgb), 0.1)';
2025
+ });
2026
+ } else {
2027
+ doCompareSnapshots(compareBase, id);
2028
+ compareBase = null;
2029
+ loadSnapshots(); // reset visual state
2030
+ }
2031
+ }
2032
+
2033
+ async function doCompareSnapshots(id1, id2) {
2034
+ showProgress('Comparing...');
2035
+ const r = await fetch(\`/api/compare?base=\${id1}&current=\${id2}\`).then(res => res.json());
2036
+ hideProgress();
2037
+
2038
+ if(!r.success) { toast('Comparison failed: '+r.error, 'error'); return; }
2039
+
2040
+ const d = r.diff;
2041
+ let html = \`<div class="detail-panel">\`;
2042
+ html += \`<div style="display:flex;justify-content:space-between;margin-bottom:16px">
2043
+ <div><strong>Base:</strong> \${new Date(r.baseDate).toLocaleString()}</div>
2044
+ <div><strong>Current:</strong> \${new Date(r.currentDate).toLocaleString()}</div>
2045
+ </div>\`;
2046
+
2047
+ const sizeDiffStr = (d.sizeDiff > 0 ? '+' : '') + fmtB(d.sizeDiff);
2048
+ const color = d.sizeDiff > 0 ? 'var(--red)' : (d.sizeDiff < 0 ? 'var(--green)' : 'var(--text)');
2049
+
2050
+ html += \`<div style="text-align:center;margin-bottom:20px">
2051
+ <div style="font-size:12px;color:var(--text3)">Total Size Change</div>
2052
+ <div style="font-size:24px;font-weight:700;color:\${color}">\${sizeDiffStr}</div>
2053
+ <div style="font-size:12px;color:var(--text3)">\${d.fileCountDiff > 0 ? '+' : ''}\${d.fileCountDiff} files</div>
2054
+ </div>\`;
2055
+
2056
+ html += \`<table class="data-table" style="font-size:13px"><thead><tr><th>Category</th><th>Size Diff</th><th>Files Diff</th></tr></thead><tbody>\`;
2057
+ d.categoryDiffs.forEach(c => {
2058
+ if(c.sizeDiff === 0 && c.fileDiff === 0) return;
2059
+ const sDiff = (c.sizeDiff > 0 ? '+' : '') + fmtB(c.sizeDiff);
2060
+ const fDiff = (c.fileDiff > 0 ? '+' : '') + c.fileDiff;
2061
+ const sColor = c.sizeDiff > 0 ? 'var(--red)' : (c.sizeDiff < 0 ? 'var(--green)' : 'var(--text2)');
2062
+ html += \`<tr><td>\${esc(c.name)}</td><td style="color:\${sColor}">\${sDiff}</td><td>\${fDiff}</td></tr>\`;
2063
+ });
2064
+ html += \`</tbody></table></div>\`;
2065
+
2066
+ showModal('Snapshot Comparison', html, 'Close', null, true);
2067
+ }
2068
+
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 };
2070
+ loadTab('overview');
2071
+ </script>
2072
+ </body>
2073
+ </html>`;
2074
+ }
2075
+ //# sourceMappingURL=dashboard-ui.js.map