cursor-guard 4.7.6 → 4.7.9

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 (27) hide show
  1. package/ROADMAP.md +24 -2
  2. package/package.json +1 -1
  3. package/references/dashboard/public/app.js +16 -3
  4. package/references/dashboard/server.js +11 -0
  5. package/references/lib/core/dashboard.js +8 -1
  6. package/references/lib/core/doctor.js +1 -1
  7. package/references/lib/core/snapshot.js +3 -5
  8. package/references/lib/utils.js +12 -5
  9. package/references/vscode-extension/dist/{cursor-guard-ide-4.7.5.vsix → cursor-guard-ide-4.7.8.vsix} +0 -0
  10. package/references/vscode-extension/dist/dashboard/public/app.js +16 -3
  11. package/references/vscode-extension/dist/dashboard/server.js +11 -0
  12. package/references/vscode-extension/dist/extension.js +165 -3
  13. package/references/vscode-extension/dist/guard-version.json +1 -1
  14. package/references/vscode-extension/dist/lib/core/dashboard.js +10 -9
  15. package/references/vscode-extension/dist/lib/core/doctor.js +1 -1
  16. package/references/vscode-extension/dist/lib/core/snapshot.js +3 -5
  17. package/references/vscode-extension/dist/lib/dashboard-manager.js +7 -0
  18. package/references/vscode-extension/dist/lib/sidebar-webview.js +272 -222
  19. package/references/vscode-extension/dist/lib/utils.js +12 -5
  20. package/references/vscode-extension/dist/lib/webview-provider.js +70 -27
  21. package/references/vscode-extension/dist/package.json +49 -2
  22. package/references/vscode-extension/dist/skill/ROADMAP.md +34 -2
  23. package/references/vscode-extension/extension.js +101 -3
  24. package/references/vscode-extension/lib/dashboard-manager.js +7 -0
  25. package/references/vscode-extension/lib/sidebar-webview.js +129 -5
  26. package/references/vscode-extension/lib/webview-provider.js +48 -30
  27. package/references/vscode-extension/package.json +37 -2
@@ -31,7 +31,7 @@ class SidebarDashboardProvider {
31
31
  payload[id] = {
32
32
  name: p.name || id,
33
33
  dashboard: p.dashboard,
34
- backups: (p.backups || []).slice(0, 6),
34
+ backups: (p.backups || []).slice(0, 5),
35
35
  };
36
36
  }
37
37
  this._view.webview.postMessage({ type: 'update', data: payload });
@@ -61,166 +61,185 @@ function _getHtml() {
61
61
  --purple: #cba6f7;
62
62
  --orange: #fab387;
63
63
  --teal: #94e2d5;
64
- --radius: 6px;
64
+ --radius: 8px;
65
65
  }
66
66
  * { margin: 0; padding: 0; box-sizing: border-box; }
67
67
  body {
68
- font: 11px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
68
+ font: 12px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
69
69
  color: var(--text);
70
70
  background: transparent;
71
- padding: 8px;
71
+ padding: 10px;
72
72
  }
73
- .card {
74
- background: var(--surface);
75
- border: 1px solid var(--border);
73
+
74
+ /* ── Big status indicator ── */
75
+ .status-hero {
76
+ text-align: center;
77
+ padding: 14px 10px;
76
78
  border-radius: var(--radius);
77
- padding: 8px 10px;
78
- margin-bottom: 6px;
79
+ margin-bottom: 10px;
79
80
  }
80
- .card-title {
81
- font-size: 9px;
82
- font-weight: 700;
83
- text-transform: uppercase;
84
- letter-spacing: 0.8px;
85
- color: var(--dim);
86
- margin-bottom: 6px;
81
+ .status-hero.protected {
82
+ background: rgba(166,227,161,0.1);
83
+ border: 1px solid rgba(166,227,161,0.3);
87
84
  }
88
- .status-row {
89
- display: flex;
90
- gap: 6px;
91
- margin-bottom: 6px;
85
+ .status-hero.alert {
86
+ background: rgba(243,139,168,0.12);
87
+ border: 1px solid rgba(243,139,168,0.4);
92
88
  }
93
- .status-badge {
94
- flex: 1;
95
- text-align: center;
96
- padding: 6px 4px;
97
- border-radius: var(--radius);
98
- background: var(--bg);
99
- border: 1px solid var(--border);
89
+ .status-hero.stopped {
90
+ background: rgba(249,226,175,0.1);
91
+ border: 1px solid rgba(249,226,175,0.3);
100
92
  }
101
- .status-badge .icon { font-size: 16px; display: block; }
102
- .status-badge .label { font-size: 9px; color: var(--dim); margin-top: 2px; }
103
- .status-badge .value { font-size: 11px; font-weight: 700; }
104
- .status-badge.ok { border-color: var(--green); }
105
- .status-badge.ok .value { color: var(--green); }
106
- .status-badge.warn { border-color: var(--yellow); }
107
- .status-badge.warn .value { color: var(--yellow); }
108
- .status-badge.danger { border-color: var(--red); }
109
- .status-badge.danger .value { color: var(--red); }
110
- .status-badge.info { border-color: var(--blue); }
111
- .status-badge.info .value { color: var(--blue); }
112
-
113
- .alert-bar {
93
+ .status-hero.critical {
114
94
  background: rgba(243,139,168,0.15);
115
95
  border: 1px solid var(--red);
116
- border-radius: var(--radius);
117
- padding: 6px 10px;
118
- margin-bottom: 6px;
119
- text-align: center;
120
96
  }
121
- .alert-bar .alert-title { color: var(--red); font-weight: 700; font-size: 12px; }
122
- .alert-bar .alert-detail { color: var(--dim); font-size: 10px; margin-top: 2px; }
123
- .alert-bar.hidden { display: none; }
97
+ .status-icon { font-size: 28px; display: block; margin-bottom: 4px; }
98
+ .status-text { font-size: 15px; font-weight: 700; }
99
+ .status-sub { font-size: 11px; color: var(--dim); margin-top: 2px; }
124
100
 
125
- .bar-group { margin-bottom: 4px; }
126
- .bar-label {
127
- display: flex;
128
- justify-content: space-between;
129
- font-size: 10px;
130
- margin-bottom: 2px;
131
- }
132
- .bar-label .name { color: var(--text); }
133
- .bar-label .val { color: var(--dim); font-weight: 600; }
134
- .bar-track {
135
- height: 6px;
136
- background: var(--bg);
137
- border-radius: 3px;
138
- overflow: hidden;
101
+ /* ── Alert card ── */
102
+ .alert-card {
103
+ background: rgba(243,139,168,0.1);
104
+ border: 1px solid rgba(243,139,168,0.35);
105
+ border-radius: var(--radius);
106
+ padding: 10px 12px;
107
+ margin-bottom: 10px;
139
108
  }
140
- .bar-fill {
141
- height: 100%;
142
- border-radius: 3px;
143
- transition: width 0.4s ease;
109
+ .alert-card .title { color: var(--red); font-weight: 700; font-size: 12px; }
110
+ .alert-card .detail { color: var(--dim); font-size: 11px; margin-top: 3px; }
111
+ .alert-card .actions { margin-top: 6px; display: flex; gap: 6px; }
112
+ .alert-card .btn-sm {
113
+ font-size: 10px; padding: 3px 8px; border-radius: 4px;
114
+ border: 1px solid var(--border); background: var(--surface);
115
+ color: var(--text); cursor: pointer;
144
116
  }
145
- .bar-fill.blue { background: var(--blue); }
146
- .bar-fill.purple { background: var(--purple); }
147
- .bar-fill.green { background: var(--green); }
148
- .bar-fill.orange { background: var(--orange); }
149
- .bar-fill.teal { background: var(--teal); }
117
+ .alert-card .btn-sm:hover { border-color: var(--blue); color: var(--blue); }
150
118
 
151
- .backup-list { list-style: none; }
152
- .backup-item {
119
+ /* ── Quick stats ── */
120
+ .stats-card {
121
+ background: var(--surface);
122
+ border: 1px solid var(--border);
123
+ border-radius: var(--radius);
124
+ padding: 10px 12px;
125
+ margin-bottom: 10px;
126
+ }
127
+ .stats-card .label-sm {
128
+ font-size: 9px; font-weight: 700; text-transform: uppercase;
129
+ letter-spacing: 0.8px; color: var(--dim); margin-bottom: 6px;
130
+ }
131
+ .stat-row {
153
132
  display: flex;
133
+ justify-content: space-between;
154
134
  align-items: center;
155
- gap: 6px;
156
135
  padding: 3px 0;
157
- border-bottom: 1px solid var(--border);
158
- font-size: 10px;
159
- }
160
- .backup-item:last-child { border: none; }
161
- .backup-dot {
162
- width: 6px; height: 6px;
163
- border-radius: 50%;
164
- flex-shrink: 0;
136
+ font-size: 11px;
165
137
  }
166
- .backup-dot.auto { background: var(--blue); }
167
- .backup-dot.snapshot { background: var(--purple); }
168
- .backup-dot.restore { background: var(--orange); }
169
- .backup-time { color: var(--dim); white-space: nowrap; }
170
- .backup-type { font-weight: 600; min-width: 36px; }
171
- .backup-summary { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
138
+ .stat-row .name { color: var(--dim); }
139
+ .stat-row .val { font-weight: 600; color: var(--text); }
140
+ .stat-row .val.green { color: var(--green); }
141
+ .stat-row .val.blue { color: var(--blue); }
142
+ .stat-row .val.yellow { color: var(--yellow); }
172
143
 
173
- .scope-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
174
- .scope-tag {
175
- font-size: 9px;
176
- padding: 1px 6px;
177
- border-radius: 10px;
178
- background: var(--bg);
144
+ /* ── Action buttons ── */
145
+ .actions-section {
146
+ margin-top: 8px;
147
+ }
148
+ .actions-grid {
149
+ display: grid;
150
+ grid-template-columns: 1fr 1fr;
151
+ gap: 6px;
179
152
  }
180
- .scope-tag.protect { color: var(--green); border: 1px solid var(--green); }
181
- .scope-tag.ignore { color: var(--red); border: 1px solid var(--red); }
182
-
183
- .health-row { display: flex; align-items: center; gap: 4px; font-size: 10px; padding: 2px 0; }
184
- .health-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
185
-
186
- .actions-row { display: flex; gap: 4px; flex-wrap: wrap; }
187
153
  .action-btn {
188
- flex: 1;
189
- min-width: 70px;
190
- padding: 5px 4px;
191
- font-size: 9px;
154
+ padding: 8px 6px;
155
+ font-size: 11px;
192
156
  font-weight: 600;
193
157
  text-align: center;
194
158
  border: 1px solid var(--border);
195
159
  border-radius: var(--radius);
196
- background: var(--bg);
160
+ background: var(--surface);
197
161
  color: var(--text);
198
162
  cursor: pointer;
199
163
  transition: all 0.15s;
200
164
  }
201
- .action-btn:hover { border-color: var(--blue); color: var(--blue); }
165
+ .action-btn:hover { border-color: var(--blue); color: var(--blue); background: rgba(137,180,250,0.08); }
166
+ .action-btn.primary {
167
+ border-color: rgba(137,180,250,0.3);
168
+ background: rgba(137,180,250,0.08);
169
+ }
170
+ .action-btn.full { grid-column: 1 / -1; }
171
+ .action-btn .icon { margin-right: 3px; }
202
172
 
203
- .empty-state {
204
- text-align: center;
205
- padding: 20px;
206
- color: var(--dim);
173
+ /* ── Scope display ── */
174
+ .scope-summary {
175
+ display: flex;
176
+ flex-wrap: wrap;
177
+ gap: 6px;
178
+ margin-bottom: 8px;
179
+ }
180
+ .scope-chip {
207
181
  font-size: 11px;
182
+ font-weight: 600;
183
+ padding: 3px 8px;
184
+ border-radius: 10px;
208
185
  }
209
-
210
- .ring-chart {
211
- position: relative;
212
- width: 56px; height: 56px;
213
- margin: 0 auto 4px;
186
+ .scope-chip.protected {
187
+ background: rgba(166,227,161,0.12);
188
+ color: var(--green);
214
189
  }
215
- .ring-chart svg { transform: rotate(-90deg); }
216
- .ring-chart .ring-bg { stroke: var(--bg); }
217
- .ring-chart .ring-fill { transition: stroke-dashoffset 0.6s ease; }
218
- .ring-label {
219
- position: absolute;
220
- top: 50%; left: 50%;
221
- transform: translate(-50%, -50%);
222
- font-size: 12px;
190
+ .scope-chip.excluded {
191
+ background: rgba(243,139,168,0.12);
192
+ color: var(--red);
193
+ }
194
+ .scope-chip.total {
195
+ background: rgba(108,112,134,0.15);
196
+ color: var(--dim);
197
+ }
198
+ .scope-block { margin-bottom: 6px; }
199
+ .scope-label {
200
+ font-size: 10px;
223
201
  font-weight: 700;
202
+ text-transform: uppercase;
203
+ letter-spacing: 0.5px;
204
+ display: block;
205
+ margin-bottom: 4px;
206
+ }
207
+ .scope-label.green { color: var(--green); }
208
+ .scope-label.red { color: var(--red); }
209
+ .scope-tags {
210
+ display: flex;
211
+ flex-wrap: wrap;
212
+ gap: 4px;
213
+ }
214
+ .scope-tag {
215
+ font-size: 10px;
216
+ padding: 2px 6px;
217
+ border-radius: 4px;
218
+ font-family: 'Cascadia Code', 'Fira Code', monospace;
219
+ max-width: 100%;
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ white-space: nowrap;
223
+ }
224
+ .scope-tag.green {
225
+ background: rgba(166,227,161,0.1);
226
+ color: var(--green);
227
+ border: 1px solid rgba(166,227,161,0.2);
228
+ }
229
+ .scope-tag.red {
230
+ background: rgba(243,139,168,0.08);
231
+ color: var(--red);
232
+ border: 1px solid rgba(243,139,168,0.2);
233
+ }
234
+ .scope-tag.dim {
235
+ background: rgba(108,112,134,0.1);
236
+ color: var(--dim);
237
+ border: 1px solid var(--border);
238
+ }
239
+
240
+ .empty-state {
241
+ text-align: center; padding: 24px 10px;
242
+ color: var(--dim); font-size: 12px;
224
243
  }
225
244
  </style>
226
245
  </head>
@@ -230,151 +249,182 @@ body {
230
249
  </div>
231
250
  <script>
232
251
  const vscode = acquireVsCodeApi();
252
+ let _alertExpiresAt = 0;
253
+
233
254
  window.addEventListener('message', e => {
234
255
  if (e.data.type === 'update') render(e.data.data);
235
256
  });
236
257
  vscode.postMessage({ cmd: 'ready' });
237
258
 
259
+ setInterval(() => {
260
+ if (!_alertExpiresAt) return;
261
+ const el = document.querySelector('.alert-countdown');
262
+ if (!el) return;
263
+ const remain = Math.max(0, Math.ceil((_alertExpiresAt - Date.now()) / 1000));
264
+ if (remain <= 0) {
265
+ el.textContent = '0s';
266
+ _alertExpiresAt = 0;
267
+ return;
268
+ }
269
+ el.textContent = remain > 60 ? Math.floor(remain / 60) + 'm ' + (remain % 60) + 's' : remain + 's';
270
+ }, 1000);
271
+
238
272
  function render(projects) {
239
273
  const ids = Object.keys(projects);
240
274
  if (ids.length === 0) {
241
- document.getElementById('root').innerHTML = '<div class="empty-state">No projects detected</div>';
275
+ root.innerHTML = '<div class="empty-state">No projects detected.<br>Add .cursor-guard.json to get started.</div>';
242
276
  return;
243
277
  }
244
278
  let html = '';
245
279
  for (const id of ids) {
246
280
  const p = projects[id];
247
281
  const d = p.dashboard;
248
- if (!d) { html += '<div class="empty-state">Loading ' + esc(p.name) + '...</div>'; continue; }
249
- html += renderProject(p.name, d, p.backups || []);
282
+ if (!d) { html += '<div class="empty-state">Loading...</div>'; continue; }
283
+ html += renderProject(d);
250
284
  }
251
- html += renderActions();
252
- document.getElementById('root').innerHTML = html;
253
- document.querySelectorAll('.action-btn').forEach(btn => {
285
+ html += renderActions(projects);
286
+ root.innerHTML = html;
287
+
288
+ const alertCard = root.querySelector('.alert-card[data-expires]');
289
+ _alertExpiresAt = alertCard ? parseInt(alertCard.dataset.expires, 10) || 0 : 0;
290
+
291
+ root.querySelectorAll('[data-cmd]').forEach(btn => {
254
292
  btn.addEventListener('click', () => vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd }));
255
293
  });
256
294
  }
257
295
 
258
- function renderProject(name, d, backups) {
259
- let h = '';
260
-
261
- // Status badges row
296
+ function renderProject(d) {
262
297
  const wOk = d.watcher?.running;
263
298
  const hasAlert = d.alerts?.active;
264
299
  const health = d.health?.status || 'unknown';
300
+ const isCritical = health === 'critical';
301
+ let h = '';
265
302
 
266
- h += '<div class="status-row">';
267
- h += badge(wOk ? '👁' : '🚫', 'Watcher', wOk ? 'Running' : 'Stopped', wOk ? 'ok' : 'danger');
268
- h += badge(hasAlert ? '🔔' : '✅', 'Alerts', hasAlert ? (d.alerts.latest?.fileCount || '!') : 'None', hasAlert ? 'danger' : 'ok');
269
- h += badge('💚', 'Health', health, health === 'healthy' ? 'ok' : health === 'critical' ? 'danger' : 'warn');
270
- h += badge('📁', 'Files', d.protectionScope?.fileCount || 0, 'info');
271
- h += '</div>';
303
+ // ── Big status hero ──
304
+ if (hasAlert) {
305
+ const fc = d.alerts.latest?.fileCount || '?';
306
+ h += '<div class="status-hero alert">';
307
+ h += '<span class="status-icon">🔴</span>';
308
+ h += '<span class="status-text">' + fc + ' files alert</span>';
309
+ h += '<span class="status-sub">Abnormal change velocity detected</span>';
310
+ h += '</div>';
311
+ } else if (!wOk) {
312
+ h += '<div class="status-hero stopped">';
313
+ h += '<span class="status-icon">🟡</span>';
314
+ h += '<span class="status-text">Watcher Stopped</span>';
315
+ h += '<span class="status-sub">Start watcher to enable protection</span>';
316
+ h += '</div>';
317
+ } else if (isCritical) {
318
+ h += '<div class="status-hero critical">';
319
+ h += '<span class="status-icon">🔴</span>';
320
+ h += '<span class="status-text">Critical Issue</span>';
321
+ h += '<span class="status-sub">' + esc(d.health.issues?.[0] || 'Check diagnostics') + '</span>';
322
+ h += '</div>';
323
+ } else {
324
+ h += '<div class="status-hero protected">';
325
+ h += '<span class="status-icon">🟢</span>';
326
+ h += '<span class="status-text">Protected</span>';
327
+ h += '<span class="status-sub">Watcher running · All systems OK</span>';
328
+ h += '</div>';
329
+ }
272
330
 
273
- // Alert bar
331
+ // ── Alert detail card (only when active) ──
274
332
  if (hasAlert) {
275
333
  const a = d.alerts.latest;
276
- const remain = a.expiresAt ? Math.max(0, Math.ceil((new Date(a.expiresAt).getTime() - Date.now()) / 1000)) : 0;
334
+ const expiresTs = a.expiresAt ? new Date(a.expiresAt).getTime() : 0;
335
+ const remain = expiresTs ? Math.max(0, Math.ceil((expiresTs - Date.now()) / 1000)) : 0;
277
336
  const display = remain > 60 ? Math.floor(remain/60) + 'm ' + (remain%60) + 's' : remain + 's';
278
- h += '<div class="alert-bar">';
279
- h += '<div class="alert-title">⚠ ' + (a.fileCount||'?') + ' files changed in ' + (a.windowSeconds||'?') + 's</div>';
280
- h += '<div class="alert-detail">Threshold: ' + (a.threshold||'?') + ' · Expires: ' + display + '</div>';
337
+ h += '<div class="alert-card" data-expires="' + expiresTs + '">';
338
+ h += '<div class="title">\u26a0 ' + (a.fileCount||'?') + ' files in ' + (a.windowSeconds||'?') + 's</div>';
339
+ h += '<div class="detail">Threshold: ' + (a.threshold||'?') + ' \xb7 Expires: <span class="alert-countdown">' + display + '</span></div>';
340
+ h += '<div class="actions">';
341
+ h += '<button class="btn-sm" data-cmd="cursorGuard.openDashboard">View Details</button>';
342
+ h += '</div>';
281
343
  h += '</div>';
282
344
  }
283
345
 
284
- // Backup stats bars
346
+ // ── Quick stats ──
285
347
  const gitC = d.counts?.git?.commits || 0;
286
348
  const shadowC = d.counts?.shadow?.snapshots || 0;
287
- const maxC = Math.max(gitC, shadowC, 1);
288
- const gitDisk = d.diskUsage?.git?.display || '0B';
289
- const shadowDisk = d.diskUsage?.shadow?.display || '0B';
290
- const gitBytes = d.diskUsage?.git?.bytes || 0;
291
- const shadowBytes = d.diskUsage?.shadow?.bytes || 0;
292
- const maxBytes = Math.max(gitBytes, shadowBytes, 1);
293
-
294
- h += '<div class="card">';
295
- h += '<div class="card-title">Backup Statistics</div>';
296
- h += bar('Git backups', gitC, gitC / maxC * 100, 'blue');
297
- h += bar('Shadow snapshots', shadowC, shadowC / maxC * 100, 'purple');
298
- h += bar('Git disk', gitDisk, gitBytes / maxBytes * 100, 'teal');
299
- h += bar('Shadow disk', shadowDisk, shadowBytes / maxBytes * 100, 'orange');
300
- if (d.disk) {
301
- h += '<div class="bar-label" style="margin-top:4px"><span class="name">System free</span><span class="val">' + d.disk.freeGB + ' GB</span></div>';
302
- }
349
+ const lastGit = d.lastBackup?.git?.relativeTime || 'never';
350
+ const freeGB = d.disk?.freeGB;
351
+ const freeDisplay = typeof freeGB === 'number' ? freeGB.toFixed(1) + ' GB' : 'N/A';
352
+ const diskWarn = d.disk?.warning;
353
+
354
+ h += '<div class="stats-card">';
355
+ h += '<div class="label-sm">Quick Stats</div>';
356
+ h += statRow('Last backup', lastGit, 'green');
357
+ h += statRow('Git backups', gitC, 'blue');
358
+ if (shadowC > 0) h += statRow('Shadow copies', shadowC, 'blue');
359
+ h += statRow('Disk free', freeDisplay, diskWarn ? 'yellow' : 'green');
303
360
  h += '</div>';
304
361
 
305
- // Recent backups timeline
306
- h += '<div class="card">';
307
- h += '<div class="card-title">Recent Backups</div>';
308
- if (backups.length === 0) {
309
- h += '<div style="color:var(--dim);font-size:10px">No backups yet</div>';
310
- } else {
311
- h += '<ul class="backup-list">';
312
- for (const b of backups) {
313
- const time = b.timestamp ? new Date(b.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '?';
314
- const type = b.type || 'auto';
315
- const dotClass = type === 'git-snapshot' ? 'snapshot' : type === 'pre-restore' ? 'restore' : 'auto';
316
- const typeLabel = type === 'git-snapshot' ? 'snap' : type === 'pre-restore' ? 'pre-rst' : 'auto';
317
- const summary = b.summary ? truncate(b.summary, 30) : '';
318
- const files = b.filesChanged ? b.filesChanged + ' files' : '';
319
- h += '<li class="backup-item">';
320
- h += '<span class="backup-dot ' + dotClass + '"></span>';
321
- h += '<span class="backup-time">' + time + '</span>';
322
- h += '<span class="backup-type">' + typeLabel + '</span>';
323
- h += '<span class="backup-summary">' + esc(files + (files && summary ? ' · ' : '') + summary) + '</span>';
324
- h += '</li>';
325
- }
326
- h += '</ul>';
327
- }
362
+ // ── Protection Scope ──
363
+ const scope = d.protectionScope || {};
364
+ const pCount = scope.fileCount || 0;
365
+ const exCount = scope.excludedCount || 0;
366
+ const totalF = scope.totalFiles || 0;
367
+ const protectPats = scope.protect || [];
368
+ const ignorePats = scope.ignore || [];
369
+
370
+ h += '<div class="stats-card">';
371
+ h += '<div class="label-sm">Protection Scope</div>';
372
+ h += '<div class="scope-summary">';
373
+ h += '<span class="scope-chip protected">\u{1f6e1}\ufe0f ' + pCount + ' protected</span>';
374
+ if (exCount > 0) h += '<span class="scope-chip excluded">\u{1f6ab} ' + exCount + ' excluded</span>';
375
+ h += '<span class="scope-chip total">' + totalF + ' total</span>';
328
376
  h += '</div>';
329
377
 
330
- // Health issues
331
- if (d.health?.issues?.length > 0) {
332
- h += '<div class="card">';
333
- h += '<div class="card-title">Health Issues</div>';
334
- for (const issue of d.health.issues) {
335
- const critical = issue.includes('critically') || issue.includes('requires Git');
336
- h += '<div class="health-row"><span class="health-dot" style="background:' + (critical ? 'var(--red)' : 'var(--yellow)') + '"></span>' + esc(issue) + '</div>';
337
- }
338
- h += '</div>';
378
+ if (protectPats.length > 0) {
379
+ h += '<div class="scope-block">';
380
+ h += '<span class="scope-label green">Protect (' + protectPats.length + ')</span>';
381
+ h += '<div class="scope-tags">';
382
+ const showP = protectPats.slice(0, 6);
383
+ for (const p of showP) h += '<span class="scope-tag green">' + esc(p) + '</span>';
384
+ if (protectPats.length > 6) h += '<span class="scope-tag dim">+' + (protectPats.length - 6) + ' more</span>';
385
+ h += '</div></div>';
339
386
  }
340
387
 
341
- // Protection scope
342
- h += '<div class="card">';
343
- h += '<div class="card-title">Protection Scope</div>';
344
- const protect = d.protectionScope?.protect || ['**'];
345
- const ignore = d.protectionScope?.ignore || [];
346
- h += '<div style="font-size:10px;margin-bottom:4px">' + (d.protectionScope?.fileCount || 0) + ' files monitored</div>';
347
- h += '<div class="scope-tags">';
348
- for (const p of protect) h += '<span class="scope-tag protect">✓ ' + esc(p) + '</span>';
349
- for (const i of ignore.slice(0, 6)) h += '<span class="scope-tag ignore">✗ ' + esc(i) + '</span>';
350
- if (ignore.length > 6) h += '<span class="scope-tag ignore">+' + (ignore.length - 6) + ' more</span>';
351
- h += '</div></div>';
388
+ if (ignorePats.length > 0) {
389
+ h += '<div class="scope-block">';
390
+ h += '<span class="scope-label red">Ignore (' + ignorePats.length + ')</span>';
391
+ h += '<div class="scope-tags">';
392
+ const showI = ignorePats.slice(0, 6);
393
+ for (const ig of showI) h += '<span class="scope-tag red">' + esc(ig) + '</span>';
394
+ if (ignorePats.length > 6) h += '<span class="scope-tag dim">+' + (ignorePats.length - 6) + ' more</span>';
395
+ h += '</div></div>';
396
+ }
397
+
398
+ h += '</div>';
352
399
 
353
400
  return h;
354
401
  }
355
402
 
356
- function renderActions() {
357
- return '<div class="card"><div class="card-title">Quick Actions</div><div class="actions-row">'
358
- + '<button class="action-btn" data-cmd="cursorGuard.openDashboard">🖥 Dashboard</button>'
359
- + '<button class="action-btn" data-cmd="cursorGuard.snapshotNow">📸 Snapshot</button>'
360
- + '<button class="action-btn" data-cmd="cursorGuard.startWatcher">▶ Start</button>'
361
- + '<button class="action-btn" data-cmd="cursorGuard.stopWatcher">⏹ Stop</button>'
362
- + '</div></div>';
363
- }
403
+ function renderActions(projects) {
404
+ const ids = Object.keys(projects);
405
+ const d = ids.length > 0 ? projects[ids[0]]?.dashboard : null;
406
+ const wOk = d?.watcher?.running;
407
+
408
+ let h = '<div class="actions-section"><div class="actions-grid">';
409
+ h += '<button class="action-btn primary" data-cmd="cursorGuard.snapshotNow"><span class="icon">📸</span>Snapshot</button>';
410
+ h += '<button class="action-btn" data-cmd="cursorGuard.quickRestore"><span class="icon">⏪</span>Restore</button>';
364
411
 
365
- function badge(icon, label, value, cls) {
366
- return '<div class="status-badge ' + cls + '">'
367
- + '<span class="icon">' + icon + '</span>'
368
- + '<span class="value">' + esc(String(value)) + '</span>'
369
- + '<span class="label">' + label + '</span>'
370
- + '</div>';
412
+ if (wOk) {
413
+ h += '<button class="action-btn" data-cmd="cursorGuard.stopWatcher"><span class="icon">🟢</span>Watcher ON</button>';
414
+ } else {
415
+ h += '<button class="action-btn" data-cmd="cursorGuard.startWatcher"><span class="icon">⚪</span>Watcher OFF</button>';
416
+ }
417
+ h += '<button class="action-btn" data-cmd="cursorGuard.doctor"><span class="icon">🔍</span>Doctor</button>';
418
+
419
+ h += '<button class="action-btn full primary" data-cmd="cursorGuard.openDashboard"><span class="icon">📊</span>Open Dashboard</button>';
420
+ h += '</div></div>';
421
+ return h;
371
422
  }
372
- function bar(name, val, pct, color) {
373
- return '<div class="bar-group"><div class="bar-label"><span class="name">' + name + '</span><span class="val">' + val + '</span></div>'
374
- + '<div class="bar-track"><div class="bar-fill ' + color + '" style="width:' + Math.max(pct, 2) + '%"></div></div></div>';
423
+
424
+ function statRow(name, val, cls) {
425
+ return '<div class="stat-row"><span class="name">' + name + '</span><span class="val ' + cls + '">' + esc(String(val)) + '</span></div>';
375
426
  }
376
427
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
377
- function truncate(s, n) { return s.length > n ? s.slice(0, n) + '...' : s; }
378
428
  </script>
379
429
  </body>
380
430
  </html>`;
@@ -36,12 +36,19 @@ function globMatch(pattern, relPath) {
36
36
 
37
37
  /**
38
38
  * Check if a relative file path matches any pattern in a list.
39
- * Also checks leaf filename for patterns like "*.log".
39
+ *
40
+ * @param {string[]} patterns
41
+ * @param {string} relPath
42
+ * @param {{ strict?: boolean }} [opts]
43
+ * strict = true → only match against full relPath (for `protect`)
44
+ * strict = false → also match against basename (for `ignore` / `secrets`)
40
45
  */
41
- function matchesAny(patterns, relPath) {
42
- const leaf = path.basename(relPath);
46
+ function matchesAny(patterns, relPath, opts) {
47
+ const strict = opts?.strict === true;
48
+ const leaf = strict ? null : path.basename(relPath);
43
49
  for (const pat of patterns) {
44
- if (globMatch(pat, relPath) || globMatch(pat, leaf)) return true;
50
+ if (globMatch(pat, relPath)) return true;
51
+ if (!strict && globMatch(pat, leaf)) return true;
45
52
  }
46
53
  return false;
47
54
  }
@@ -427,7 +434,7 @@ function parseArgs(argv) {
427
434
  function filterFiles(files, cfg) {
428
435
  let result = files;
429
436
  if (cfg.protect.length > 0) {
430
- result = result.filter(f => matchesAny(cfg.protect, f.rel));
437
+ result = result.filter(f => matchesAny(cfg.protect, f.rel, { strict: true }));
431
438
  }
432
439
  result = result.filter(f => {
433
440
  if (cfg.ignore.length > 0 && matchesAny(cfg.ignore, f.rel)) return false;