cursor-guard 4.9.6 → 4.9.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 (32) hide show
  1. package/README.md +124 -61
  2. package/README.zh-CN.md +121 -58
  3. package/ROADMAP.md +48 -20
  4. package/SKILL.md +1 -1
  5. package/docs/RELEASE.md +196 -0
  6. package/package.json +4 -2
  7. package/references/dashboard/public/app.js +79 -79
  8. package/references/dashboard/public/style.css +264 -159
  9. package/references/lib/core/core.test.js +139 -101
  10. package/references/lib/core/snapshot.js +8 -4
  11. package/references/mcp/server.js +73 -72
  12. package/references/vscode-extension/build-vsix.js +7 -5
  13. package/references/vscode-extension/dist/{cursor-guard-ide-4.9.6.vsix → cursor-guard-ide-4.9.9.vsix} +0 -0
  14. package/references/vscode-extension/dist/dashboard/public/app.js +79 -79
  15. package/references/vscode-extension/dist/dashboard/public/style.css +264 -159
  16. package/references/vscode-extension/dist/extension.js +704 -498
  17. package/references/vscode-extension/dist/guard-version.json +1 -1
  18. package/references/vscode-extension/dist/lib/core/snapshot.js +8 -4
  19. package/references/vscode-extension/dist/lib/dashboard-manager.js +70 -13
  20. package/references/vscode-extension/dist/lib/locale.js +36 -0
  21. package/references/vscode-extension/dist/lib/sidebar-webview.js +1484 -502
  22. package/references/vscode-extension/dist/mcp/server.js +11 -7
  23. package/references/vscode-extension/dist/media/brand-placeholder.png +0 -0
  24. package/references/vscode-extension/dist/package.json +1 -1
  25. package/references/vscode-extension/dist/skill/ROADMAP.md +48 -20
  26. package/references/vscode-extension/dist/skill/SKILL.md +1 -1
  27. package/references/vscode-extension/extension.js +704 -498
  28. package/references/vscode-extension/lib/dashboard-manager.js +70 -13
  29. package/references/vscode-extension/lib/locale.js +36 -0
  30. package/references/vscode-extension/lib/sidebar-webview.js +1484 -502
  31. package/references/vscode-extension/media/brand-placeholder.png +0 -0
  32. package/references/vscode-extension/package.json +1 -1
@@ -1,502 +1,1484 @@
1
- 'use strict';
2
-
3
- const vscode = require('vscode');
4
-
5
- class SidebarDashboardProvider {
6
- constructor(poller) {
7
- this._poller = poller;
8
- this._view = null;
9
- this._sub = poller.onChange(data => this._push(data));
10
- }
11
-
12
- resolveWebviewView(webviewView) {
13
- this._view = webviewView;
14
- webviewView.webview.options = { enableScripts: true };
15
- webviewView.webview.html = _getHtml();
16
-
17
- webviewView.webview.onDidReceiveMessage(msg => {
18
- if (msg.cmd === 'ready') this._push(this._poller.data);
19
- if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
20
- });
21
-
22
- webviewView.onDidChangeVisibility(() => {
23
- if (webviewView.visible) this._push(this._poller.data);
24
- });
25
- }
26
-
27
- _push(data) {
28
- if (!this._view?.visible) return;
29
-
30
- const payload = {};
31
- for (const [id, project] of data) {
32
- payload[id] = {
33
- name: project.name || id,
34
- dashboard: project.dashboard,
35
- backups: (project.backups || []).slice(0, 5),
36
- };
37
- }
38
-
39
- this._view.webview.postMessage({ type: 'update', data: payload });
40
- }
41
-
42
- dispose() {
43
- this._sub?.dispose();
44
- }
45
- }
46
-
47
- function _getHtml() {
48
- return `<!DOCTYPE html>
49
- <html lang="en">
50
- <head>
51
- <meta charset="UTF-8">
52
- <style>
53
- :root {
54
- --surface: #1f2430;
55
- --surface-2: #2b3141;
56
- --border: #3b4357;
57
- --text: #eef2ff;
58
- --muted: #9aa4bd;
59
- --green: #9ad7a2;
60
- --yellow: #f5d585;
61
- --red: #f29f9f;
62
- --orange: #f4b36e;
63
- --blue: #9fc3ff;
64
- --radius: 10px;
65
- }
66
-
67
- * { box-sizing: border-box; }
68
- body {
69
- margin: 0;
70
- padding: 10px;
71
- font: 12px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
72
- color: var(--text);
73
- background: transparent;
74
- }
75
-
76
- .empty {
77
- padding: 26px 12px;
78
- text-align: center;
79
- color: var(--muted);
80
- }
81
-
82
- .hero {
83
- margin-bottom: 10px;
84
- padding: 14px 12px;
85
- border-radius: var(--radius);
86
- border: 1px solid var(--border);
87
- background: rgba(255, 255, 255, 0.04);
88
- }
89
-
90
- .hero.risk {
91
- border-color: rgba(244, 179, 110, 0.45);
92
- background: rgba(244, 179, 110, 0.12);
93
- }
94
-
95
- .hero.alert {
96
- border-color: rgba(242, 159, 159, 0.45);
97
- background: rgba(242, 159, 159, 0.12);
98
- }
99
-
100
- .hero.stopped {
101
- border-color: rgba(245, 213, 133, 0.45);
102
- background: rgba(245, 213, 133, 0.10);
103
- }
104
-
105
- .hero.critical {
106
- border-color: rgba(242, 159, 159, 0.60);
107
- background: rgba(242, 159, 159, 0.16);
108
- }
109
-
110
- .hero.protected {
111
- border-color: rgba(154, 215, 162, 0.45);
112
- background: rgba(154, 215, 162, 0.10);
113
- }
114
-
115
- .hero-kicker {
116
- font-size: 10px;
117
- letter-spacing: 0.08em;
118
- text-transform: uppercase;
119
- color: var(--muted);
120
- }
121
-
122
- .hero-title {
123
- margin-top: 3px;
124
- font-size: 16px;
125
- font-weight: 700;
126
- }
127
-
128
- .hero-sub {
129
- margin-top: 4px;
130
- color: var(--muted);
131
- }
132
-
133
- .card {
134
- margin-bottom: 10px;
135
- padding: 10px 12px;
136
- border-radius: var(--radius);
137
- border: 1px solid var(--border);
138
- background: var(--surface-2);
139
- }
140
-
141
- .card.risk-card {
142
- border-color: rgba(244, 179, 110, 0.45);
143
- }
144
-
145
- .card.alert-card {
146
- border-color: rgba(242, 159, 159, 0.45);
147
- }
148
-
149
- .card-title {
150
- margin-bottom: 8px;
151
- font-size: 10px;
152
- font-weight: 700;
153
- letter-spacing: 0.08em;
154
- text-transform: uppercase;
155
- color: var(--muted);
156
- }
157
-
158
- .row {
159
- display: flex;
160
- justify-content: space-between;
161
- align-items: flex-start;
162
- gap: 10px;
163
- padding: 3px 0;
164
- }
165
-
166
- .row-name {
167
- color: var(--muted);
168
- }
169
-
170
- .row-value {
171
- text-align: right;
172
- font-weight: 600;
173
- }
174
-
175
- .row-value.green { color: var(--green); }
176
- .row-value.blue { color: var(--blue); }
177
- .row-value.yellow { color: var(--yellow); }
178
- .row-value.orange { color: var(--orange); }
179
- .row-value.red { color: var(--red); }
180
-
181
- .pill-wrap {
182
- display: flex;
183
- flex-wrap: wrap;
184
- gap: 6px;
185
- margin-bottom: 8px;
186
- }
187
-
188
- .pill {
189
- padding: 3px 8px;
190
- border-radius: 999px;
191
- font-size: 11px;
192
- font-weight: 600;
193
- }
194
-
195
- .pill.green { background: rgba(154, 215, 162, 0.12); color: var(--green); }
196
- .pill.red { background: rgba(242, 159, 159, 0.12); color: var(--red); }
197
- .pill.orange { background: rgba(244, 179, 110, 0.12); color: var(--orange); }
198
- .pill.dim { background: rgba(154, 164, 189, 0.12); color: var(--muted); }
199
-
200
- .tag-group { margin-top: 8px; }
201
- .tag-label {
202
- margin-bottom: 4px;
203
- font-size: 10px;
204
- font-weight: 700;
205
- letter-spacing: 0.06em;
206
- text-transform: uppercase;
207
- color: var(--muted);
208
- }
209
-
210
- .tag-list {
211
- display: flex;
212
- flex-wrap: wrap;
213
- gap: 4px;
214
- }
215
-
216
- .tag {
217
- max-width: 100%;
218
- overflow: hidden;
219
- text-overflow: ellipsis;
220
- white-space: nowrap;
221
- padding: 2px 6px;
222
- border-radius: 6px;
223
- border: 1px solid var(--border);
224
- font: 10px/1.4 Consolas, "Cascadia Code", monospace;
225
- }
226
-
227
- .tag.green {
228
- color: var(--green);
229
- border-color: rgba(154, 215, 162, 0.3);
230
- background: rgba(154, 215, 162, 0.08);
231
- }
232
-
233
- .tag.red {
234
- color: var(--red);
235
- border-color: rgba(242, 159, 159, 0.3);
236
- background: rgba(242, 159, 159, 0.08);
237
- }
238
-
239
- .tag.dim {
240
- color: var(--muted);
241
- }
242
-
243
- .actions {
244
- display: grid;
245
- grid-template-columns: 1fr 1fr;
246
- gap: 6px;
247
- margin-top: 8px;
248
- }
249
-
250
- .btn {
251
- border: 1px solid var(--border);
252
- border-radius: var(--radius);
253
- background: var(--surface-2);
254
- color: var(--text);
255
- padding: 8px 6px;
256
- font: inherit;
257
- font-weight: 600;
258
- cursor: pointer;
259
- }
260
-
261
- .btn:hover {
262
- border-color: var(--blue);
263
- color: var(--blue);
264
- }
265
-
266
- .btn.primary {
267
- background: rgba(159, 195, 255, 0.10);
268
- border-color: rgba(159, 195, 255, 0.35);
269
- }
270
-
271
- .btn.full {
272
- grid-column: 1 / -1;
273
- }
274
- </style>
275
- </head>
276
- <body>
277
- <div id="root">
278
- <div class="empty">Waiting for data...</div>
279
- </div>
280
- <script>
281
- const vscode = acquireVsCodeApi();
282
- let _alertExpiresAt = 0;
283
-
284
- window.addEventListener('message', event => {
285
- if (event.data.type === 'update') render(event.data.data);
286
- });
287
-
288
- vscode.postMessage({ cmd: 'ready' });
289
-
290
- setInterval(() => {
291
- if (_alertExpiresAt) {
292
- const el = document.querySelector('.alert-countdown');
293
- if (el) {
294
- const remain = Math.max(0, Math.ceil((_alertExpiresAt - Date.now()) / 1000));
295
- if (remain <= 0) {
296
- el.textContent = '0s';
297
- _alertExpiresAt = 0;
298
- } else if (remain > 60) {
299
- el.textContent = Math.floor(remain / 60) + 'm ' + (remain % 60) + 's';
300
- } else {
301
- el.textContent = remain + 's';
302
- }
303
- }
304
- }
305
-
306
- const ageEl = document.querySelector('.backup-age[data-backup-ts]');
307
- if (!ageEl) return;
308
- const ts = parseInt(ageEl.dataset.backupTs, 10);
309
- if (!ts) return;
310
-
311
- const sec = Math.floor((Date.now() - ts) / 1000);
312
- if (sec < 60) ageEl.textContent = sec + 's ago';
313
- else if (sec < 3600) ageEl.textContent = Math.floor(sec / 60) + 'm ' + (sec % 60) + 's ago';
314
- else if (sec < 86400) ageEl.textContent = Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm ago';
315
- else ageEl.textContent = Math.floor(sec / 86400) + 'd ago';
316
- }, 1000);
317
-
318
- function render(projects) {
319
- const ids = Object.keys(projects);
320
- if (ids.length === 0) {
321
- root.innerHTML = '<div class="empty">No projects detected.<br>Add .cursor-guard.json to get started.</div>';
322
- return;
323
- }
324
-
325
- let html = '';
326
- for (const id of ids) {
327
- const project = projects[id];
328
- const dashboard = project.dashboard;
329
- if (!dashboard) {
330
- html += '<div class="empty">Loading...</div>';
331
- continue;
332
- }
333
- html += renderProject(dashboard);
334
- }
335
- html += renderActions(projects);
336
- root.innerHTML = html;
337
-
338
- const alertCard = root.querySelector('.alert-card[data-expires]');
339
- _alertExpiresAt = alertCard ? parseInt(alertCard.dataset.expires, 10) || 0 : 0;
340
-
341
- root.querySelectorAll('[data-cmd]').forEach(btn => {
342
- btn.addEventListener('click', () => {
343
- vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd });
344
- });
345
- });
346
- }
347
-
348
- function renderProject(dashboard) {
349
- const watcherRunning = dashboard.watcher?.running;
350
- const preWarning = dashboard.preWarnings?.active ? dashboard.preWarnings.latest : null;
351
- const alert = dashboard.alerts?.active ? dashboard.alerts.latest : null;
352
- const health = dashboard.health?.status || 'unknown';
353
- const critical = health === 'critical';
354
- let html = '';
355
-
356
- if (preWarning) {
357
- html += hero('risk', 'Pre-Warning', 'Delete Risk', preWarning.summary || 'Review pending destructive edit');
358
- } else if (alert) {
359
- html += hero('alert', 'Change Alert', (alert.fileCount || '?') + ' files changed fast', 'Abnormal change velocity detected');
360
- } else if (!watcherRunning) {
361
- html += hero('stopped', 'Protection', 'Watcher Stopped', 'Start watcher to enable continuous protection');
362
- } else if (critical) {
363
- html += hero('critical', 'Health', 'Critical Issue', esc(dashboard.health.issues?.[0] || 'Check diagnostics'));
364
- } else {
365
- html += hero('protected', 'Protection', 'Protected', 'Watcher running and backups healthy');
366
- }
367
-
368
- if (preWarning) {
369
- html += '<div class="card risk-card">';
370
- html += '<div class="card-title">Deletion Risk</div>';
371
- html += row('File', esc(preWarning.file || 'Unknown'), 'orange');
372
- html += row('Risk', esc(String(preWarning.riskPercent || '?')) + '%', 'orange');
373
- if (preWarning.removedMethodCount) {
374
- html += row('Methods removed', esc(String(preWarning.removedMethodCount)), 'red');
375
- }
376
- html += row('Summary', esc(preWarning.summary || 'Pending destructive edit warning'), 'orange');
377
- html += '<div class="actions">';
378
- html += '<button class="btn" data-cmd="cursorGuard.openDashboard">Open Dashboard</button>';
379
- html += '<button class="btn" data-cmd="cursorGuard.quickRestore">Restore</button>';
380
- html += '</div>';
381
- html += '</div>';
382
- }
383
-
384
- if (alert) {
385
- const expiresTs = alert.expiresAt ? new Date(alert.expiresAt).getTime() : 0;
386
- const remain = expiresTs ? Math.max(0, Math.ceil((expiresTs - Date.now()) / 1000)) : 0;
387
- const display = remain > 60 ? Math.floor(remain / 60) + 'm ' + (remain % 60) + 's' : remain + 's';
388
- html += '<div class="card alert-card" data-expires="' + expiresTs + '">';
389
- html += '<div class="card-title">Active Alert</div>';
390
- html += row('Window', (alert.windowSeconds || '?') + 's', 'red');
391
- html += row('Files', String(alert.fileCount || '?'), 'red');
392
- html += row('Threshold', String(alert.threshold || '?'), 'yellow');
393
- html += row('Expires', '<span class="alert-countdown">' + display + '</span>', 'yellow', true);
394
- html += '<div class="actions">';
395
- html += '<button class="btn" data-cmd="cursorGuard.openDashboard">View Details</button>';
396
- html += '</div>';
397
- html += '</div>';
398
- }
399
-
400
- const gitCount = dashboard.counts?.git?.commits || 0;
401
- const shadowCount = dashboard.counts?.shadow?.snapshots || 0;
402
- const lastGitTs = dashboard.lastBackup?.git?.timestamp || '';
403
- const lastGit = dashboard.lastBackup?.git?.relativeTime || 'never';
404
- const freeGB = dashboard.disk?.freeGB;
405
- const freeDisplay = typeof freeGB === 'number' ? freeGB.toFixed(1) + ' GB' : 'N/A';
406
- const diskWarn = dashboard.disk?.warning;
407
-
408
- html += '<div class="card">';
409
- html += '<div class="card-title">Quick Stats</div>';
410
- if (lastGitTs) {
411
- html += '<div class="row"><span class="row-name">Last backup</span><span class="row-value green backup-age" data-backup-ts="' + new Date(lastGitTs).getTime() + '">' + esc(lastGit) + '</span></div>';
412
- } else {
413
- html += row('Last backup', lastGit, 'green');
414
- }
415
- html += row('Git backups', String(gitCount), 'blue');
416
- if (shadowCount > 0) html += row('Shadow copies', String(shadowCount), 'blue');
417
- html += row('Disk free', freeDisplay, diskWarn ? 'yellow' : 'green');
418
- html += '</div>';
419
-
420
- const scope = dashboard.protectionScope || {};
421
- const protect = scope.protect || [];
422
- const ignore = scope.ignore || [];
423
-
424
- html += '<div class="card">';
425
- html += '<div class="card-title">Protection Scope</div>';
426
- html += '<div class="pill-wrap">';
427
- html += '<span class="pill green">' + esc(String(scope.fileCount || 0)) + ' protected</span>';
428
- if ((scope.excludedCount || 0) > 0) {
429
- html += '<span class="pill red">' + esc(String(scope.excludedCount || 0)) + ' excluded</span>';
430
- }
431
- html += '<span class="pill dim">' + esc(String(scope.totalFiles || 0)) + ' total</span>';
432
- html += '</div>';
433
-
434
- if (protect.length > 0) {
435
- html += renderTags('Protect', protect, 'green');
436
- }
437
- if (ignore.length > 0) {
438
- html += renderTags('Ignore', ignore, 'red');
439
- }
440
- html += '</div>';
441
-
442
- return html;
443
- }
444
-
445
- function renderTags(label, values, tone) {
446
- let html = '<div class="tag-group">';
447
- html += '<div class="tag-label">' + esc(label) + ' (' + values.length + ')</div>';
448
- html += '<div class="tag-list">';
449
- const shown = values.slice(0, 6);
450
- for (const value of shown) {
451
- html += '<span class="tag ' + tone + '">' + esc(value) + '</span>';
452
- }
453
- if (values.length > 6) {
454
- html += '<span class="tag dim">+' + (values.length - 6) + ' more</span>';
455
- }
456
- html += '</div></div>';
457
- return html;
458
- }
459
-
460
- function renderActions(projects) {
461
- const ids = Object.keys(projects);
462
- const dashboard = ids.length > 0 ? projects[ids[0]]?.dashboard : null;
463
- const watcherRunning = dashboard?.watcher?.running;
464
-
465
- let html = '<div class="actions">';
466
- html += '<button class="btn primary" data-cmd="cursorGuard.snapshotNow">Snapshot</button>';
467
- html += '<button class="btn" data-cmd="cursorGuard.quickRestore">Restore</button>';
468
- html += watcherRunning
469
- ? '<button class="btn" data-cmd="cursorGuard.stopWatcher">Watcher On</button>'
470
- : '<button class="btn" data-cmd="cursorGuard.startWatcher">Watcher Off</button>';
471
- html += '<button class="btn" data-cmd="cursorGuard.doctor">Doctor</button>';
472
- html += '<button class="btn primary full" data-cmd="cursorGuard.openDashboard">Open Dashboard</button>';
473
- html += '</div>';
474
- return html;
475
- }
476
-
477
- function hero(tone, kicker, title, subtitle) {
478
- let html = '<div class="hero ' + tone + '">';
479
- html += '<div class="hero-kicker">' + esc(kicker) + '</div>';
480
- html += '<div class="hero-title">' + esc(title) + '</div>';
481
- html += '<div class="hero-sub">' + esc(subtitle) + '</div>';
482
- html += '</div>';
483
- return html;
484
- }
485
-
486
- function row(name, value, tone, rawValue) {
487
- return '<div class="row"><span class="row-name">' + esc(name) + '</span><span class="row-value ' + tone + '">' + (rawValue ? value : esc(String(value))) + '</span></div>';
488
- }
489
-
490
- function esc(value) {
491
- return String(value)
492
- .replace(/&/g, '&amp;')
493
- .replace(/</g, '&lt;')
494
- .replace(/>/g, '&gt;')
495
- .replace(/"/g, '&quot;');
496
- }
497
- </script>
498
- </body>
499
- </html>`;
500
- }
501
-
502
- module.exports = { SidebarDashboardProvider };
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+ const { getLocale, setLocale } = require('./locale');
5
+
6
+ class SidebarDashboardProvider {
7
+ constructor(poller, context) {
8
+ this._poller = poller;
9
+ this._extensionUri = context?.extensionUri;
10
+ this._localeStorage = context?.globalState;
11
+ this._locale = getLocale(this._localeStorage);
12
+ this._view = null;
13
+ this._sub = poller.onChange(data => this._push(data));
14
+ }
15
+
16
+ resolveWebviewView(webviewView) {
17
+ this._view = webviewView;
18
+ const webview = webviewView.webview;
19
+ webview.options = { enableScripts: true };
20
+
21
+ let brandInnerHtml = '';
22
+ if (this._extensionUri) {
23
+ const logoUri = webview.asWebviewUri(
24
+ vscode.Uri.joinPath(this._extensionUri, 'media', 'brand-placeholder.png')
25
+ );
26
+ brandInnerHtml =
27
+ '<img class="cg-brand-mark-img" src="' +
28
+ escHtmlAttr(logoUri.toString()) +
29
+ '" alt="" draggable="false" />';
30
+ }
31
+
32
+ webview.html = _getHtml(brandInnerHtml);
33
+
34
+ webviewView.webview.onDidReceiveMessage(async msg => {
35
+ if (msg.cmd === 'ready') {
36
+ this._postLocale();
37
+ this._push(this._poller.data);
38
+ }
39
+ if (msg.cmd === 'setLocale') {
40
+ this._locale = await setLocale(this._localeStorage, msg.locale);
41
+ this._postLocale();
42
+ }
43
+ if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
44
+ });
45
+
46
+ webviewView.onDidChangeVisibility(() => {
47
+ if (webviewView.visible) {
48
+ this._postLocale();
49
+ this._push(this._poller.data);
50
+ }
51
+ });
52
+ }
53
+
54
+ _postLocale() {
55
+ if (!this._view) return;
56
+ this._view.webview.postMessage({ type: 'locale', locale: this._locale });
57
+ }
58
+
59
+ _push(data) {
60
+ if (!this._view) return;
61
+ // Do not gate on webviewView.visible: on first load, `ready` can arrive while
62
+ // visible is still false, and we would never post `update` → stuck on "Waiting for data...".
63
+
64
+ const payload = {};
65
+ for (const [id, project] of data) {
66
+ payload[id] = {
67
+ name: project.name || id,
68
+ dashboard: project.dashboard,
69
+ backups: (project.backups || []).slice(0, 5),
70
+ };
71
+ }
72
+
73
+ this._view.webview.postMessage({ type: 'update', data: payload });
74
+ }
75
+
76
+ dispose() {
77
+ this._sub?.dispose();
78
+ }
79
+ }
80
+
81
+ function escHtmlAttr(value) {
82
+ return String(value)
83
+ .replace(/&/g, '&amp;')
84
+ .replace(/"/g, '&quot;');
85
+ }
86
+
87
+ function _getHtml(brandInnerHtml) {
88
+ brandInnerHtml = brandInnerHtml || '';
89
+ const brandMarkClass =
90
+ 'cg-brand-mark' + (brandInnerHtml ? ' cg-brand-mark--has-img' : '');
91
+ return `<!DOCTYPE html>
92
+ <html lang="en">
93
+ <head>
94
+ <meta charset="UTF-8">
95
+ <style>
96
+ :root {
97
+ --surface: var(--vscode-sideBar-background, #1f2430);
98
+ --surface-2: var(--vscode-editorWidget-background, #252a38);
99
+ --border: var(--vscode-widget-border, rgba(120, 130, 160, 0.22));
100
+ --text: var(--vscode-foreground, #e8eaf0);
101
+ --muted: var(--vscode-descriptionForeground, #9aa4bd);
102
+ --green: var(--vscode-testing-iconPassed, #89d18a);
103
+ --yellow: var(--vscode-editorWarning-foreground, #e4c06a);
104
+ --red: var(--vscode-testing-iconFailed, #f0a0a0);
105
+ --orange: var(--vscode-charts-orange, #f0b070);
106
+ --blue: var(--vscode-textLink-foreground, #8eb6ff);
107
+ --radius: 12px;
108
+ --radius-lg: 14px;
109
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
110
+ --shadow-soft: 0 4px 14px rgba(0, 0, 0, 0.12);
111
+ --accent: var(--blue);
112
+ --glow-green: color-mix(in srgb, var(--green) 38%, transparent);
113
+ /* Shell: dark base + green haze (no blue wash) */
114
+ --shell-green-1: color-mix(in srgb, var(--green) 16%, #070907);
115
+ --shell-green-2: color-mix(in srgb, var(--green) 9%, #050805);
116
+ }
117
+
118
+ * { box-sizing: border-box; }
119
+ body {
120
+ margin: 0;
121
+ padding: 0;
122
+ font: 13px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
123
+ color: var(--text);
124
+ background: transparent;
125
+ -webkit-font-smoothing: antialiased;
126
+ }
127
+
128
+ .cg-shell {
129
+ position: relative;
130
+ padding: 8px 8px 14px;
131
+ min-height: 100%;
132
+ background:
133
+ radial-gradient(110% 75% at 8% -15%, var(--shell-green-1), transparent 56%),
134
+ radial-gradient(95% 70% at 102% 108%, var(--shell-green-2), transparent 52%),
135
+ linear-gradient(168deg, #0a0c0f 0%, var(--surface) 48%, #0c100e 100%);
136
+ }
137
+
138
+ .cg-dashboard-scroll {
139
+ margin-top: 4px;
140
+ }
141
+
142
+ .cg-section-fold {
143
+ margin-bottom: 8px;
144
+ }
145
+
146
+ .cg-section-fold .cg-main-fold {
147
+ border-radius: 10px;
148
+ }
149
+
150
+ .cg-section-fold .cg-main-fold-head {
151
+ padding: 5px 8px;
152
+ }
153
+
154
+ .cg-section-fold .cg-main-fold-title {
155
+ font-size: 10px;
156
+ letter-spacing: 0.08em;
157
+ }
158
+
159
+ .cg-section-fold .cg-main-fold-chevron {
160
+ width: 18px;
161
+ height: 18px;
162
+ font-size: 10px;
163
+ }
164
+
165
+ .cg-section-fold .cg-main-fold-body {
166
+ padding: 0 6px 6px;
167
+ }
168
+
169
+ .cg-section-fold-body .hero {
170
+ margin-bottom: 0;
171
+ margin-top: 2px;
172
+ padding: 12px 10px;
173
+ }
174
+
175
+ .cg-section-fold-body .hero-title {
176
+ font-size: 16px;
177
+ }
178
+
179
+ .cg-section-fold-body > .card {
180
+ margin-bottom: 0;
181
+ margin-top: 2px;
182
+ }
183
+
184
+ .cg-section-fold-body .cg-actions-wrap {
185
+ margin-top: 0;
186
+ padding-top: 8px;
187
+ }
188
+
189
+ .cg-main-fold {
190
+ border-radius: var(--radius-lg);
191
+ border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
192
+ background: color-mix(in srgb, var(--surface-2) 45%, transparent);
193
+ box-shadow: var(--shadow);
194
+ overflow: hidden;
195
+ }
196
+
197
+ .cg-main-fold-head {
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: space-between;
201
+ gap: 10px;
202
+ width: 100%;
203
+ padding: 10px 12px;
204
+ border: none;
205
+ background: color-mix(in srgb, var(--text) 4%, transparent);
206
+ color: inherit;
207
+ cursor: pointer;
208
+ font: inherit;
209
+ text-align: left;
210
+ transition: background 0.15s ease;
211
+ }
212
+
213
+ .cg-main-fold-head:hover {
214
+ background: color-mix(in srgb, var(--text) 8%, transparent);
215
+ }
216
+
217
+ .cg-main-fold-title {
218
+ font-size: 11px;
219
+ font-weight: 700;
220
+ letter-spacing: 0.1em;
221
+ text-transform: uppercase;
222
+ color: var(--muted);
223
+ flex: 1;
224
+ min-width: 0;
225
+ }
226
+
227
+ .cg-main-fold-chevron {
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ width: 22px;
232
+ height: 22px;
233
+ border-radius: 6px;
234
+ font-size: 11px;
235
+ line-height: 1;
236
+ color: var(--muted);
237
+ background: color-mix(in srgb, var(--text) 6%, transparent);
238
+ transition: transform 0.2s ease, background 0.15s ease;
239
+ flex-shrink: 0;
240
+ }
241
+
242
+ .cg-main-fold-head:hover .cg-main-fold-chevron {
243
+ background: color-mix(in srgb, var(--text) 10%, transparent);
244
+ color: var(--text);
245
+ }
246
+
247
+ .cg-main-fold--collapsed .cg-main-fold-chevron {
248
+ transform: rotate(-90deg);
249
+ }
250
+
251
+ .cg-main-fold-body {
252
+ border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
253
+ }
254
+
255
+ .cg-main-fold--collapsed .cg-main-fold-body {
256
+ display: none;
257
+ }
258
+
259
+ .cg-main-fold--collapsed .cg-main-fold-head {
260
+ border-bottom: none;
261
+ }
262
+
263
+ .cg-brand-section {
264
+ margin-bottom: 4px;
265
+ }
266
+
267
+ .cg-brand-section--compact .cg-brand-topbar {
268
+ padding: 4px 8px 2px;
269
+ gap: 6px;
270
+ }
271
+
272
+ .cg-brand-section--compact .cg-brand-topbar .lang-btn {
273
+ padding: 3px 8px;
274
+ font-size: 10px;
275
+ }
276
+
277
+ .cg-brand-section--compact .cg-brand-mark {
278
+ width: 28px;
279
+ height: 28px;
280
+ border-radius: 8px;
281
+ }
282
+
283
+ .cg-brand-section--compact .cg-brand-mark--has-img {
284
+ padding: 3px;
285
+ }
286
+
287
+ .cg-brand-section--compact .cg-brand {
288
+ padding: 6px 8px;
289
+ gap: 8px;
290
+ border-radius: 10px;
291
+ }
292
+
293
+ .cg-brand-section--compact .cg-brand-title {
294
+ font-size: 12px;
295
+ }
296
+
297
+ .cg-brand-section--compact .cg-brand-sub--project {
298
+ font-size: 10px;
299
+ }
300
+
301
+ .cg-brand-section--compact .cg-brand-sub--backup {
302
+ font-size: 9px;
303
+ letter-spacing: 0.06em;
304
+ }
305
+
306
+ .cg-brand-section--compact .cg-brand {
307
+ margin-bottom: 0;
308
+ }
309
+
310
+ .cg-brand--details-only .cg-brand-text {
311
+ flex: 1;
312
+ }
313
+
314
+ .cg-brand-topbar {
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: flex-end;
318
+ gap: 8px;
319
+ width: 100%;
320
+ padding: 6px 12px 4px;
321
+ background: transparent;
322
+ }
323
+
324
+ .cg-brand {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 10px;
328
+ margin-bottom: 14px;
329
+ padding: 10px 12px;
330
+ border-radius: var(--radius-lg);
331
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
332
+ background: color-mix(in srgb, var(--surface-2) 75%, transparent);
333
+ box-shadow: var(--shadow-soft);
334
+ backdrop-filter: blur(8px);
335
+ }
336
+
337
+ .cg-brand-mark {
338
+ width: 36px;
339
+ height: 36px;
340
+ border-radius: 10px;
341
+ flex-shrink: 0;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ overflow: hidden;
346
+ background: linear-gradient(135deg, color-mix(in srgb, var(--blue) 55%, #1a1a2e), color-mix(in srgb, var(--green) 40%, #1a1a2e));
347
+ box-shadow:
348
+ 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent) inset,
349
+ 0 4px 12px color-mix(in srgb, var(--blue) 25%, transparent);
350
+ }
351
+
352
+ .cg-brand-mark--has-img {
353
+ background: color-mix(in srgb, var(--surface-2) 88%, var(--text));
354
+ padding: 4px;
355
+ box-shadow:
356
+ 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent) inset,
357
+ 0 2px 10px color-mix(in srgb, var(--blue) 18%, transparent);
358
+ }
359
+
360
+ .cg-brand-mark-img {
361
+ width: 100%;
362
+ height: 100%;
363
+ object-fit: contain;
364
+ display: block;
365
+ pointer-events: none;
366
+ user-select: none;
367
+ }
368
+
369
+ .cg-brand-text {
370
+ display: flex;
371
+ flex-direction: column;
372
+ gap: 2px;
373
+ min-width: 0;
374
+ flex: 1;
375
+ }
376
+
377
+ .cg-brand-title {
378
+ font-size: 14px;
379
+ font-weight: 800;
380
+ letter-spacing: -0.03em;
381
+ line-height: 1.2;
382
+ background: linear-gradient(90deg, var(--text), color-mix(in srgb, var(--text) 72%, var(--blue)));
383
+ -webkit-background-clip: text;
384
+ background-clip: text;
385
+ color: transparent;
386
+ }
387
+
388
+ .cg-brand-meta {
389
+ display: flex;
390
+ flex-direction: column;
391
+ gap: 3px;
392
+ min-width: 0;
393
+ margin-top: 1px;
394
+ }
395
+
396
+ .cg-brand-sub {
397
+ font-weight: 600;
398
+ opacity: 0.88;
399
+ }
400
+
401
+ .cg-brand-sub--project {
402
+ font-size: 11px;
403
+ letter-spacing: 0.02em;
404
+ color: color-mix(in srgb, var(--text) 92%, var(--muted));
405
+ overflow: hidden;
406
+ text-overflow: ellipsis;
407
+ white-space: nowrap;
408
+ }
409
+
410
+ .cg-brand-sub--backup {
411
+ font-size: 10px;
412
+ letter-spacing: 0.1em;
413
+ text-transform: uppercase;
414
+ color: var(--muted);
415
+ }
416
+
417
+ .cg-brand-backup-prefix {
418
+ font-weight: 600;
419
+ color: var(--muted);
420
+ margin-right: 2px;
421
+ }
422
+
423
+ #cg-brand-backup .backup-age[data-backup-ts] {
424
+ color: var(--green);
425
+ font-weight: 700;
426
+ letter-spacing: 0.06em;
427
+ }
428
+
429
+ .cg-brand-tools {
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: flex-end;
433
+ flex-shrink: 0;
434
+ }
435
+
436
+ .lang-btn {
437
+ border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
438
+ border-radius: 999px;
439
+ background: color-mix(in srgb, var(--surface-2) 85%, transparent);
440
+ color: var(--text);
441
+ padding: 6px 11px;
442
+ font: inherit;
443
+ font-size: 11px;
444
+ font-weight: 700;
445
+ letter-spacing: 0.03em;
446
+ cursor: pointer;
447
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
448
+ }
449
+
450
+ .lang-btn:hover {
451
+ border-color: color-mix(in srgb, var(--blue) 55%, var(--border));
452
+ color: var(--blue);
453
+ box-shadow: var(--shadow-soft);
454
+ }
455
+
456
+ .lang-btn:focus-visible {
457
+ outline: 2px solid color-mix(in srgb, var(--blue) 70%, transparent);
458
+ outline-offset: 2px;
459
+ }
460
+
461
+ .empty {
462
+ padding: 26px 12px;
463
+ text-align: center;
464
+ color: var(--muted);
465
+ }
466
+
467
+ .hero {
468
+ position: relative;
469
+ margin-bottom: 12px;
470
+ padding: 16px 14px;
471
+ border-radius: var(--radius-lg);
472
+ border: 1px solid var(--border);
473
+ background: color-mix(in srgb, var(--vscode-editor-background, #1e1e1e) 92%, transparent);
474
+ box-shadow: var(--shadow);
475
+ overflow: hidden;
476
+ }
477
+
478
+ .hero::before {
479
+ content: "";
480
+ position: absolute;
481
+ left: 0;
482
+ top: 0;
483
+ right: 0;
484
+ height: 3px;
485
+ opacity: 0.85;
486
+ background: linear-gradient(90deg, var(--muted), var(--muted));
487
+ pointer-events: none;
488
+ }
489
+
490
+ .hero.protected::before {
491
+ background: linear-gradient(90deg, var(--green), var(--blue));
492
+ box-shadow: 0 0 16px var(--glow-green);
493
+ }
494
+
495
+ .hero.risk::before {
496
+ background: linear-gradient(90deg, var(--orange), var(--yellow));
497
+ }
498
+
499
+ .hero.alert::before,
500
+ .hero.critical::before {
501
+ background: linear-gradient(90deg, var(--red), var(--orange));
502
+ }
503
+
504
+ .hero.stopped::before {
505
+ background: linear-gradient(90deg, var(--yellow), var(--muted));
506
+ }
507
+
508
+ .hero-top {
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: space-between;
512
+ gap: 8px;
513
+ }
514
+
515
+ .hero-top .hero-kicker {
516
+ margin: 0;
517
+ }
518
+
519
+ .cg-pulse-dot {
520
+ width: 8px;
521
+ height: 8px;
522
+ border-radius: 50%;
523
+ background: var(--green);
524
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent);
525
+ animation: cg-pulse 2.2s ease-out infinite;
526
+ flex-shrink: 0;
527
+ }
528
+
529
+ @keyframes cg-pulse {
530
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent); opacity: 1; }
531
+ 70% { box-shadow: 0 0 0 8px transparent; opacity: 0.85; }
532
+ 100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
533
+ }
534
+
535
+ @media (prefers-reduced-motion: reduce) {
536
+ .cg-pulse-dot { animation: none; box-shadow: 0 0 6px color-mix(in srgb, var(--green) 35%, transparent); }
537
+ .card { transition: none; }
538
+ .card-chevron { transition: none; }
539
+ .card-head { transition: none; }
540
+ .btn { transition: none; }
541
+ .lang-btn { transition: none; }
542
+ .cg-main-fold-chevron { transition: none; }
543
+ .cg-section-fold .cg-main-fold-chevron { transition: none; }
544
+ }
545
+
546
+ .hero.risk {
547
+ border-color: rgba(244, 179, 110, 0.45);
548
+ background: rgba(244, 179, 110, 0.12);
549
+ }
550
+
551
+ .hero.alert {
552
+ border-color: rgba(242, 159, 159, 0.45);
553
+ background: rgba(242, 159, 159, 0.12);
554
+ }
555
+
556
+ .hero.stopped {
557
+ border-color: rgba(245, 213, 133, 0.45);
558
+ background: rgba(245, 213, 133, 0.10);
559
+ }
560
+
561
+ .hero.critical {
562
+ border-color: rgba(242, 159, 159, 0.60);
563
+ background: rgba(242, 159, 159, 0.16);
564
+ }
565
+
566
+ .hero.protected {
567
+ border-color: rgba(154, 215, 162, 0.45);
568
+ background: rgba(154, 215, 162, 0.10);
569
+ }
570
+
571
+ .hero-kicker {
572
+ font-size: 10px;
573
+ letter-spacing: 0.1em;
574
+ text-transform: uppercase;
575
+ color: var(--muted);
576
+ font-weight: 600;
577
+ opacity: 0.92;
578
+ }
579
+
580
+ .hero-title {
581
+ margin-top: 8px;
582
+ font-size: 18px;
583
+ font-weight: 800;
584
+ letter-spacing: -0.03em;
585
+ line-height: 1.2;
586
+ }
587
+
588
+ .hero-sub {
589
+ margin-top: 6px;
590
+ color: var(--muted);
591
+ font-size: 12px;
592
+ line-height: 1.45;
593
+ opacity: 0.95;
594
+ }
595
+
596
+ .card {
597
+ position: relative;
598
+ margin-bottom: 12px;
599
+ padding: 12px 14px;
600
+ border-radius: var(--radius-lg);
601
+ border: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
602
+ background: linear-gradient(
603
+ 165deg,
604
+ color-mix(in srgb, var(--surface-2) 100%, var(--text)) 0%,
605
+ color-mix(in srgb, var(--surface-2) 96%, transparent) 100%
606
+ );
607
+ box-shadow: var(--shadow);
608
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
609
+ }
610
+
611
+ .card:hover {
612
+ border-color: color-mix(in srgb, var(--border) 70%, var(--blue));
613
+ box-shadow: var(--shadow-soft);
614
+ }
615
+
616
+ .card.risk-card {
617
+ border-color: rgba(244, 179, 110, 0.45);
618
+ }
619
+
620
+ .card.alert-card {
621
+ border-color: rgba(242, 159, 159, 0.45);
622
+ }
623
+
624
+ .card.cg-collapsible {
625
+ padding-top: 0;
626
+ padding-bottom: 12px;
627
+ }
628
+
629
+ .card-head {
630
+ display: flex;
631
+ align-items: center;
632
+ justify-content: space-between;
633
+ gap: 10px;
634
+ width: calc(100% + 28px);
635
+ margin: 0 -14px 0 -14px;
636
+ padding: 12px 14px;
637
+ border: none;
638
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
639
+ background: transparent;
640
+ color: inherit;
641
+ cursor: pointer;
642
+ text-align: left;
643
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
644
+ transition: background 0.15s ease;
645
+ }
646
+
647
+ .card-head:hover {
648
+ background: color-mix(in srgb, var(--text) 5%, transparent);
649
+ }
650
+
651
+ .card.cg-collapsible.is-collapsed .card-head {
652
+ border-bottom-color: transparent;
653
+ }
654
+
655
+ .card-head .card-title {
656
+ margin: 0;
657
+ padding: 0;
658
+ border: none;
659
+ flex: 1;
660
+ min-width: 0;
661
+ }
662
+
663
+ .card-title {
664
+ margin-bottom: 10px;
665
+ font-size: 10px;
666
+ font-weight: 700;
667
+ letter-spacing: 0.1em;
668
+ text-transform: uppercase;
669
+ color: var(--muted);
670
+ padding-bottom: 8px;
671
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
672
+ }
673
+
674
+ .card-chevron {
675
+ display: flex;
676
+ align-items: center;
677
+ justify-content: center;
678
+ width: 22px;
679
+ height: 22px;
680
+ border-radius: 6px;
681
+ font-size: 11px;
682
+ line-height: 1;
683
+ color: var(--muted);
684
+ background: color-mix(in srgb, var(--text) 6%, transparent);
685
+ transition: transform 0.2s ease, background 0.15s ease;
686
+ flex-shrink: 0;
687
+ }
688
+
689
+ .card-head:hover .card-chevron {
690
+ background: color-mix(in srgb, var(--text) 10%, transparent);
691
+ color: var(--text);
692
+ }
693
+
694
+ .card.is-collapsed .card-chevron {
695
+ transform: rotate(-90deg);
696
+ }
697
+
698
+ .card-panel {
699
+ padding-top: 12px;
700
+ }
701
+
702
+ .card-panel .actions {
703
+ margin-top: 10px;
704
+ }
705
+
706
+ .card.is-collapsed .card-panel {
707
+ display: none;
708
+ }
709
+
710
+ .row {
711
+ display: flex;
712
+ justify-content: space-between;
713
+ align-items: flex-start;
714
+ gap: 12px;
715
+ padding: 5px 0;
716
+ }
717
+
718
+ .row-name {
719
+ color: var(--muted);
720
+ }
721
+
722
+ .row-value {
723
+ text-align: right;
724
+ font-weight: 600;
725
+ font-variant-numeric: tabular-nums;
726
+ }
727
+
728
+ .row-value.green { color: var(--green); }
729
+ .row-value.blue { color: var(--blue); }
730
+ .row-value.yellow { color: var(--yellow); }
731
+ .row-value.orange { color: var(--orange); }
732
+ .row-value.red { color: var(--red); }
733
+
734
+ .pill-wrap {
735
+ display: flex;
736
+ flex-wrap: wrap;
737
+ gap: 6px;
738
+ margin-bottom: 8px;
739
+ }
740
+
741
+ .pill {
742
+ padding: 4px 10px;
743
+ border-radius: 999px;
744
+ font-size: 11px;
745
+ font-weight: 600;
746
+ border: 1px solid transparent;
747
+ }
748
+
749
+ .pill.green {
750
+ background: color-mix(in srgb, var(--green) 18%, transparent);
751
+ color: var(--green);
752
+ border-color: color-mix(in srgb, var(--green) 42%, transparent);
753
+ }
754
+ .pill.ignore {
755
+ background: color-mix(in srgb, var(--muted) 14%, transparent);
756
+ color: color-mix(in srgb, var(--muted) 92%, var(--text));
757
+ border-color: color-mix(in srgb, var(--muted) 28%, transparent);
758
+ }
759
+ .pill.red { background: rgba(242, 159, 159, 0.12); color: var(--red); }
760
+ .pill.orange { background: rgba(244, 179, 110, 0.12); color: var(--orange); }
761
+ .pill.dim { background: rgba(154, 164, 189, 0.1); color: var(--muted); border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); }
762
+
763
+ .tag-group { margin-top: 12px; }
764
+ .pill-wrap + .tag-group { margin-top: 10px; }
765
+ .tag-group--protect {
766
+ padding: 8px 8px 10px;
767
+ margin-left: -4px;
768
+ margin-right: -4px;
769
+ border-radius: 10px;
770
+ border: 1px solid color-mix(in srgb, var(--green) 32%, var(--border));
771
+ background: color-mix(in srgb, var(--green) 7%, transparent);
772
+ }
773
+ .tag-group--ignore {
774
+ padding: 8px 8px 10px;
775
+ margin-left: -4px;
776
+ margin-right: -4px;
777
+ border-radius: 10px;
778
+ border: 1px solid color-mix(in srgb, var(--muted) 22%, var(--border));
779
+ background: color-mix(in srgb, var(--muted) 6%, transparent);
780
+ }
781
+ .tag-label {
782
+ margin-bottom: 6px;
783
+ font-size: 10px;
784
+ font-weight: 700;
785
+ letter-spacing: 0.08em;
786
+ text-transform: uppercase;
787
+ color: var(--muted);
788
+ }
789
+ .tag-group--protect .tag-label { color: var(--green); opacity: 0.95; }
790
+ .tag-group--ignore .tag-label { color: color-mix(in srgb, var(--muted) 88%, var(--text)); }
791
+
792
+ .tag-list {
793
+ display: flex;
794
+ flex-wrap: wrap;
795
+ gap: 4px;
796
+ }
797
+
798
+ .tag {
799
+ max-width: 100%;
800
+ overflow: hidden;
801
+ text-overflow: ellipsis;
802
+ white-space: nowrap;
803
+ padding: 4px 8px;
804
+ border-radius: 8px;
805
+ border: 1px solid var(--border);
806
+ font: 10px/1.45 ui-monospace, Consolas, "Cascadia Code", monospace;
807
+ }
808
+
809
+ .tag.green {
810
+ color: color-mix(in srgb, var(--green) 92%, #fff);
811
+ border-color: color-mix(in srgb, var(--green) 48%, transparent);
812
+ background: color-mix(in srgb, var(--green) 14%, transparent);
813
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--green) 12%, transparent);
814
+ }
815
+
816
+ .tag.ignore {
817
+ color: color-mix(in srgb, var(--muted) 95%, var(--text));
818
+ border-color: color-mix(in srgb, var(--muted) 38%, var(--border));
819
+ background: color-mix(in srgb, var(--muted) 10%, var(--surface-2));
820
+ }
821
+
822
+ .tag.red {
823
+ color: var(--red);
824
+ border-color: rgba(242, 159, 159, 0.3);
825
+ background: rgba(242, 159, 159, 0.08);
826
+ }
827
+
828
+ .tag.dim {
829
+ color: var(--muted);
830
+ border-color: color-mix(in srgb, var(--border) 80%, transparent);
831
+ background: color-mix(in srgb, var(--surface-2) 60%, transparent);
832
+ }
833
+
834
+ .cg-actions-wrap {
835
+ margin-top: 4px;
836
+ padding-top: 14px;
837
+ border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
838
+ }
839
+
840
+ .actions {
841
+ display: grid;
842
+ grid-template-columns: 1fr 1fr;
843
+ gap: 8px;
844
+ margin-top: 0;
845
+ }
846
+
847
+ .btn {
848
+ border: 1px solid var(--border);
849
+ border-radius: var(--radius);
850
+ background: color-mix(in srgb, var(--surface-2) 88%, var(--text));
851
+ color: var(--text);
852
+ padding: 9px 8px;
853
+ font: inherit;
854
+ font-weight: 600;
855
+ font-size: 12px;
856
+ cursor: pointer;
857
+ transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
858
+ }
859
+
860
+ .btn:hover {
861
+ border-color: color-mix(in srgb, var(--blue) 55%, var(--border));
862
+ color: var(--blue);
863
+ box-shadow: var(--shadow-soft);
864
+ }
865
+
866
+ .btn:focus-visible {
867
+ outline: 2px solid color-mix(in srgb, var(--blue) 70%, transparent);
868
+ outline-offset: 2px;
869
+ }
870
+
871
+ .btn.primary {
872
+ background: linear-gradient(
873
+ 165deg,
874
+ color-mix(in srgb, var(--blue) 22%, var(--surface-2)),
875
+ color-mix(in srgb, var(--blue) 10%, var(--surface-2))
876
+ );
877
+ border-color: color-mix(in srgb, var(--blue) 45%, var(--border));
878
+ }
879
+
880
+ .btn.primary:hover {
881
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--blue) 25%, transparent), var(--shadow-soft);
882
+ }
883
+
884
+ .btn.full {
885
+ grid-column: 1 / -1;
886
+ }
887
+ </style>
888
+ </head>
889
+ <body>
890
+ <div class="cg-shell">
891
+ <section class="cg-brand-section cg-brand-section--compact" aria-label="Brand bar">
892
+ <div class="cg-brand-topbar">
893
+ <button id="lang-toggle" class="lang-btn" type="button">中文</button>
894
+ </div>
895
+ <header class="cg-brand cg-brand--details-only" aria-label="Cursor Guard">
896
+ <div class="${brandMarkClass}" aria-hidden="true">${brandInnerHtml}</div>
897
+ <div class="cg-brand-text">
898
+ <span class="cg-brand-title" id="cg-brand-title">Cursor Guard</span>
899
+ <div class="cg-brand-meta">
900
+ <span class="cg-brand-sub cg-brand-sub--project" id="cg-brand-project">-</span>
901
+ <div class="cg-brand-sub cg-brand-sub--backup" id="cg-brand-backup">
902
+ <span class="cg-brand-backup-prefix" id="cg-brand-backup-prefix" hidden>Last backup </span><span id="cg-brand-backup-age" class="backup-age">-</span>
903
+ </div>
904
+ </div>
905
+ </div>
906
+ </header>
907
+ </section>
908
+ <div class="cg-dashboard-scroll" id="cg-dashboard-scroll">
909
+ <div id="root">
910
+ <div class="empty">Waiting for data...</div>
911
+ </div>
912
+ </div>
913
+ </div>
914
+ <script>
915
+ const vscode = acquireVsCodeApi();
916
+ const root = document.getElementById('root');
917
+ const brandTitle = document.getElementById('cg-brand-title');
918
+ const brandProject = document.getElementById('cg-brand-project');
919
+ const brandBackupPrefix = document.getElementById('cg-brand-backup-prefix');
920
+ const brandBackupAge = document.getElementById('cg-brand-backup-age');
921
+ const langToggle = document.getElementById('lang-toggle');
922
+ const savedState = vscode.getState() || {};
923
+ let _locale = savedState.locale || ((navigator.language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US');
924
+ let _alertExpiresAt = 0;
925
+ let _projects = {};
926
+
927
+ const I18N = {
928
+ 'en-US': {
929
+ 'chrome.title': 'Cursor Guard',
930
+ 'chrome.switch': '\u4e2d\u6587',
931
+ 'section.status': 'Status',
932
+ 'section.actions': 'Actions',
933
+ 'state.waiting': 'Waiting for data...',
934
+ 'state.loading': 'Loading...',
935
+ 'state.empty': 'No projects detected.<br>Add .cursor-guard.json to get started.',
936
+ 'brand.noWorkspace': 'No workspace',
937
+ 'brand.addConfig': 'Add .cursor-guard.json',
938
+ 'brand.loadingBackup': 'Loading backup...',
939
+ 'brand.noGitBackup': 'No Git backup yet',
940
+ 'brand.backupPrefix': 'Last backup',
941
+ 'hero.pre.kicker': 'Pre-Warning',
942
+ 'hero.pre.title': 'Delete Risk',
943
+ 'hero.pre.subtitle': 'Review pending destructive edit',
944
+ 'hero.alert.kicker': 'Change Alert',
945
+ 'hero.alert.subtitle': 'Abnormal change velocity detected',
946
+ 'hero.protection.kicker': 'Protection',
947
+ 'hero.protection.stopped': 'Watcher Stopped',
948
+ 'hero.protection.stoppedSub': 'Start watcher to enable continuous protection',
949
+ 'hero.health.kicker': 'Health',
950
+ 'hero.health.critical': 'Critical Issue',
951
+ 'hero.health.check': 'Check diagnostics',
952
+ 'hero.protection.safe': 'Protected',
953
+ 'hero.protection.safeSub': 'Watcher running and backups healthy',
954
+ 'card.deletionRisk': 'Deletion Risk',
955
+ 'card.activeAlert': 'Active Alert',
956
+ 'card.quickStats': 'Quick Stats',
957
+ 'card.protectionScope': 'Protection Scope',
958
+ 'row.file': 'File',
959
+ 'row.risk': 'Risk',
960
+ 'row.methodsRemoved': 'Methods removed',
961
+ 'row.summary': 'Summary',
962
+ 'row.window': 'Window',
963
+ 'row.files': 'Files',
964
+ 'row.threshold': 'Threshold',
965
+ 'row.expires': 'Expires',
966
+ 'row.watcher': 'Watcher',
967
+ 'row.health': 'Health',
968
+ 'row.lastBackup': 'Last backup',
969
+ 'row.gitBackups': 'Git backups',
970
+ 'row.shadowCopies': 'Shadow copies',
971
+ 'row.diskFree': 'Disk free',
972
+ 'status.watcher.running': 'Running',
973
+ 'status.watcher.stale': 'Stale Lock',
974
+ 'status.watcher.stopped': 'Stopped',
975
+ 'status.health.healthy': 'Healthy',
976
+ 'status.health.warning': 'Warning',
977
+ 'status.health.critical': 'Critical',
978
+ 'pill.protected': '{n} protected',
979
+ 'pill.excluded': '{n} excluded',
980
+ 'pill.total': '{n} total',
981
+ 'tag.protect': 'Protect',
982
+ 'tag.ignore': 'Ignore',
983
+ 'tag.more': '+{n} more',
984
+ 'actions.openDashboard': 'Open Dashboard',
985
+ 'actions.restore': 'Restore',
986
+ 'actions.viewDetails': 'View Details',
987
+ 'actions.snapshot': 'Snapshot',
988
+ 'actions.watcherOn': 'Stop Watcher',
989
+ 'actions.watcherOff': 'Start Watcher',
990
+ 'actions.doctor': 'Doctor',
991
+ 'stats.never': 'never',
992
+ 'misc.unknown': 'Unknown',
993
+ 'misc.na': 'N/A',
994
+ 'time.secondsAgo': '{n}s ago',
995
+ 'time.minutesAgo': '{m}m {s}s ago',
996
+ 'time.hoursAgo': '{h}h {m}m ago',
997
+ 'time.daysAgo': '{d}d ago',
998
+ 'time.seconds': '{n}s',
999
+ 'time.minutes': '{m}m {s}s',
1000
+ 'alert.filesChangedFast': '{count} files changed fast'
1001
+ },
1002
+ 'zh-CN': {
1003
+ 'chrome.title': 'Cursor Guard',
1004
+ 'chrome.switch': 'EN',
1005
+ 'section.status': '\u72b6\u6001',
1006
+ 'section.actions': '\u64cd\u4f5c',
1007
+ 'state.waiting': '等待数据...',
1008
+ 'state.loading': '加载中...',
1009
+ 'state.empty': '未检测到项目。<br>添加 .cursor-guard.json 即可开始使用。',
1010
+ 'brand.noWorkspace': '无工作区',
1011
+ 'brand.addConfig': '添加 .cursor-guard.json',
1012
+ 'brand.loadingBackup': '备份信息加载中...',
1013
+ 'brand.noGitBackup': '暂无 Git 备份',
1014
+ 'brand.backupPrefix': '上次备份',
1015
+ 'hero.pre.kicker': '事先预警',
1016
+ 'hero.pre.title': '删除风险',
1017
+ 'hero.pre.subtitle': '请先检查此次破坏性编辑',
1018
+ 'hero.alert.kicker': '变更告警',
1019
+ 'hero.alert.subtitle': '检测到异常高频文件变更',
1020
+ 'hero.protection.kicker': '保护状态',
1021
+ 'hero.protection.stopped': 'Watcher 未运行',
1022
+ 'hero.protection.stoppedSub': '启动 watcher 以开启持续保护',
1023
+ 'hero.health.kicker': '健康状态',
1024
+ 'hero.health.critical': '严重问题',
1025
+ 'hero.health.check': '请检查诊断结果',
1026
+ 'hero.protection.safe': '已保护',
1027
+ 'hero.protection.safeSub': 'Watcher 正在运行,备份状态健康',
1028
+ 'card.deletionRisk': '删除风险',
1029
+ 'card.activeAlert': '活跃告警',
1030
+ 'card.quickStats': '快速概览',
1031
+ 'card.protectionScope': '保护范围',
1032
+ 'row.file': '文件',
1033
+ 'row.risk': '风险',
1034
+ 'row.methodsRemoved': '移除的方法数',
1035
+ 'row.summary': '摘要',
1036
+ 'row.window': '窗口',
1037
+ 'row.files': '文件数',
1038
+ 'row.threshold': '阈值',
1039
+ 'row.expires': '剩余时间',
1040
+ 'row.watcher': '监控',
1041
+ 'row.health': '健康',
1042
+ 'row.lastBackup': '上次备份',
1043
+ 'row.gitBackups': 'Git 备份数',
1044
+ 'row.shadowCopies': 'Shadow 备份数',
1045
+ 'row.diskFree': '剩余磁盘',
1046
+ 'status.watcher.running': '运行中',
1047
+ 'status.watcher.stale': '锁残留',
1048
+ 'status.watcher.stopped': '已停止',
1049
+ 'status.health.healthy': '健康',
1050
+ 'status.health.warning': '警告',
1051
+ 'status.health.critical': '严重',
1052
+ 'pill.protected': '{n} 个受保护',
1053
+ 'pill.excluded': '{n} 个排除',
1054
+ 'pill.total': '{n} 个总计',
1055
+ 'tag.protect': '保护',
1056
+ 'tag.ignore': '忽略',
1057
+ 'tag.more': '+{n} 个更多',
1058
+ 'actions.openDashboard': '打开看板',
1059
+ 'actions.restore': '恢复',
1060
+ 'actions.viewDetails': '查看详情',
1061
+ 'actions.snapshot': '立即快照',
1062
+ 'actions.watcherOn': '停止 Watcher',
1063
+ 'actions.watcherOff': '启动 Watcher',
1064
+ 'actions.doctor': '诊断',
1065
+ 'stats.never': '从未',
1066
+ 'misc.unknown': '未知',
1067
+ 'misc.na': 'N/A',
1068
+ 'time.secondsAgo': '{n} 秒前',
1069
+ 'time.minutesAgo': '{m} 分 {s} 秒前',
1070
+ 'time.hoursAgo': '{h} 小时 {m} 分前',
1071
+ 'time.daysAgo': '{d} 天前',
1072
+ 'time.seconds': '{n} 秒',
1073
+ 'time.minutes': '{m} 分 {s} 秒',
1074
+ 'alert.filesChangedFast': '{count} 个文件快速变更'
1075
+ }
1076
+ };
1077
+
1078
+ function t(key, params) {
1079
+ const dict = I18N[_locale] || I18N['en-US'];
1080
+ let value = dict[key] || I18N['en-US'][key] || key;
1081
+ for (const [name, replacement] of Object.entries(params || {})) {
1082
+ value = value.replaceAll('{' + name + '}', String(replacement));
1083
+ }
1084
+ return value;
1085
+ }
1086
+
1087
+ function setLocale(locale, opts) {
1088
+ _locale = locale === 'zh-CN' ? 'zh-CN' : 'en-US';
1089
+ document.documentElement.lang = _locale === 'zh-CN' ? 'zh-CN' : 'en';
1090
+ vscode.setState({ locale: _locale });
1091
+ if (!opts || opts.syncHost !== false) {
1092
+ vscode.postMessage({ cmd: 'setLocale', locale: _locale });
1093
+ }
1094
+ updateChrome();
1095
+ if (!opts || opts.render !== false) {
1096
+ render(_projects);
1097
+ }
1098
+ }
1099
+
1100
+ function toggleLocale() {
1101
+ setLocale(_locale === 'zh-CN' ? 'en-US' : 'zh-CN');
1102
+ }
1103
+
1104
+ function updateChrome() {
1105
+ document.documentElement.lang = _locale === 'zh-CN' ? 'zh-CN' : 'en';
1106
+ brandTitle.textContent = t('chrome.title');
1107
+ langToggle.textContent = t('chrome.switch');
1108
+ updateBrandBar(_projects);
1109
+ }
1110
+
1111
+ function escAttr(value) {
1112
+ return String(value)
1113
+ .replace(/&/g, '&amp;')
1114
+ .replace(/"/g, '&quot;');
1115
+ }
1116
+
1117
+ function sectionStorageKey(projectId, suffix) {
1118
+ return String(projectId || 'default').replace(/[^a-zA-Z0-9_-]/g, '_') + ':' + suffix;
1119
+ }
1120
+
1121
+ function wrapSection(projectId, suffix, title, innerHtml, extraClass) {
1122
+ extraClass = extraClass || '';
1123
+ const sk = sectionStorageKey(projectId, suffix);
1124
+ const pid = 'cg-sec-' + sk.replace(/[^a-zA-Z0-9_-]/g, '_');
1125
+ const cls = 'cg-section-fold cg-main-fold cg-main-fold--open' + (extraClass ? ' ' + extraClass : '');
1126
+ return (
1127
+ '<div class="' + cls + '" data-section-key="' + escAttr(sk) + '">' +
1128
+ '<button type="button" class="cg-main-fold-head cg-section-fold-head" aria-expanded="true" aria-controls="' + escAttr(pid) + '">' +
1129
+ '<span class="cg-main-fold-title">' + esc(title) + '</span>' +
1130
+ '<span class="cg-main-fold-chevron" aria-hidden="true">&#9662;</span></button>' +
1131
+ '<div class="cg-main-fold-body cg-section-fold-body" id="' + escAttr(pid) + '">' + innerHtml + '</div></div>'
1132
+ );
1133
+ }
1134
+
1135
+ function bindSectionFolds(container) {
1136
+ const PREFIX = 'cg-section-fold-v1:';
1137
+ container.querySelectorAll('.cg-section-fold[data-section-key]').forEach(section => {
1138
+ const key = section.getAttribute('data-section-key');
1139
+ const btn = section.querySelector('.cg-section-fold-head');
1140
+ if (!key || !btn) return;
1141
+ if (sessionStorage.getItem(PREFIX + key) === '1') {
1142
+ section.classList.add('cg-main-fold--collapsed');
1143
+ section.classList.remove('cg-main-fold--open');
1144
+ btn.setAttribute('aria-expanded', 'false');
1145
+ }
1146
+ btn.addEventListener('click', () => {
1147
+ const collapsed = section.classList.toggle('cg-main-fold--collapsed');
1148
+ section.classList.toggle('cg-main-fold--open', !collapsed);
1149
+ btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
1150
+ sessionStorage.setItem(PREFIX + key, collapsed ? '1' : '0');
1151
+ });
1152
+ });
1153
+ }
1154
+
1155
+ function formatRelativeAge(ms) {
1156
+ const sec = Math.floor((Date.now() - ms) / 1000);
1157
+ if (sec < 60) return t('time.secondsAgo', { n: sec });
1158
+ if (sec < 3600) return t('time.minutesAgo', { m: Math.floor(sec / 60), s: sec % 60 });
1159
+ if (sec < 86400) return t('time.hoursAgo', { h: Math.floor(sec / 3600), m: Math.floor((sec % 3600) / 60) });
1160
+ return t('time.daysAgo', { d: Math.floor(sec / 86400) });
1161
+ }
1162
+
1163
+ function formatCountdown(seconds) {
1164
+ if (seconds > 60) return t('time.minutes', { m: Math.floor(seconds / 60), s: seconds % 60 });
1165
+ return t('time.seconds', { n: seconds });
1166
+ }
1167
+
1168
+ function displayCount(value) {
1169
+ return value == null ? '?' : String(value);
1170
+ }
1171
+
1172
+ function pickPrimaryProject(projects) {
1173
+ const ids = Object.keys(projects || {});
1174
+ for (let i = 0; i < ids.length; i++) {
1175
+ const id = ids[i];
1176
+ if (projects[id] && projects[id].dashboard) return { id, project: projects[id], ids };
1177
+ }
1178
+ if (ids.length) return { id: ids[0], project: projects[ids[0]], ids };
1179
+ return null;
1180
+ }
1181
+
1182
+ function updateBrandBar(projects) {
1183
+ const primary = pickPrimaryProject(projects || {});
1184
+
1185
+ if (!primary) {
1186
+ brandProject.textContent = t('brand.noWorkspace');
1187
+ brandProject.removeAttribute('title');
1188
+ brandBackupPrefix.hidden = true;
1189
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
1190
+ brandBackupAge.removeAttribute('data-backup-ts');
1191
+ brandBackupAge.textContent = t('brand.addConfig');
1192
+ return;
1193
+ }
1194
+
1195
+ const project = primary.project || {};
1196
+ const dashboard = project.dashboard;
1197
+ let name = project.name || primary.id;
1198
+ if (primary.ids.length > 1) {
1199
+ name += ' +' + (primary.ids.length - 1);
1200
+ }
1201
+ brandProject.textContent = name;
1202
+ brandProject.title = name;
1203
+
1204
+ if (!dashboard) {
1205
+ brandBackupPrefix.hidden = true;
1206
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
1207
+ brandBackupAge.removeAttribute('data-backup-ts');
1208
+ brandBackupAge.textContent = t('brand.loadingBackup');
1209
+ return;
1210
+ }
1211
+
1212
+ const gitTs = dashboard.lastBackup?.git?.timestamp;
1213
+ if (gitTs) {
1214
+ const ts = new Date(gitTs).getTime();
1215
+ brandBackupPrefix.hidden = false;
1216
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
1217
+ brandBackupAge.dataset.backupTs = String(ts);
1218
+ brandBackupAge.textContent = formatRelativeAge(ts);
1219
+ } else {
1220
+ brandBackupPrefix.hidden = true;
1221
+ brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
1222
+ brandBackupAge.removeAttribute('data-backup-ts');
1223
+ brandBackupAge.textContent = t('brand.noGitBackup');
1224
+ }
1225
+ }
1226
+
1227
+ window.addEventListener('message', event => {
1228
+ if (event.data.type === 'locale') {
1229
+ setLocale(event.data.locale, { syncHost: false });
1230
+ return;
1231
+ }
1232
+ if (event.data.type === 'update') render(event.data.data);
1233
+ });
1234
+
1235
+ langToggle.addEventListener('click', toggleLocale);
1236
+ updateChrome();
1237
+ root.innerHTML = '<div class="empty">' + t('state.waiting') + '</div>';
1238
+ vscode.postMessage({ cmd: 'ready' });
1239
+
1240
+ setInterval(() => {
1241
+ if (_alertExpiresAt) {
1242
+ const el = document.querySelector('.alert-countdown');
1243
+ if (el) {
1244
+ const remain = Math.max(0, Math.ceil((_alertExpiresAt - Date.now()) / 1000));
1245
+ if (remain <= 0) {
1246
+ el.textContent = formatCountdown(0);
1247
+ _alertExpiresAt = 0;
1248
+ } else {
1249
+ el.textContent = formatCountdown(remain);
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ document.querySelectorAll('.backup-age[data-backup-ts]').forEach(ageEl => {
1255
+ const ts = parseInt(ageEl.dataset.backupTs, 10);
1256
+ if (!ts) return;
1257
+ ageEl.textContent = formatRelativeAge(ts);
1258
+ });
1259
+ }, 1000);
1260
+
1261
+ function render(projects) {
1262
+ _projects = projects || {};
1263
+ const ids = Object.keys(_projects);
1264
+ if (ids.length === 0) {
1265
+ root.innerHTML = '<div class="empty">' + t('state.empty') + '</div>';
1266
+ updateBrandBar(_projects);
1267
+ return;
1268
+ }
1269
+
1270
+ let html = '';
1271
+ for (const id of ids) {
1272
+ const project = _projects[id];
1273
+ const dashboard = project.dashboard;
1274
+ if (!dashboard) {
1275
+ html += '<div class="empty">' + esc(t('state.loading')) + '</div>';
1276
+ continue;
1277
+ }
1278
+ html += renderProject(dashboard, id);
1279
+ }
1280
+ html += renderActions(_projects, ids[0]);
1281
+ root.innerHTML = html;
1282
+ updateBrandBar(_projects);
1283
+ bindSectionFolds(root);
1284
+
1285
+ const alertCard = root.querySelector('.alert-card[data-expires]');
1286
+ _alertExpiresAt = alertCard ? parseInt(alertCard.dataset.expires, 10) || 0 : 0;
1287
+
1288
+ root.querySelectorAll('[data-cmd]').forEach(btn => {
1289
+ btn.addEventListener('click', () => {
1290
+ vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd });
1291
+ });
1292
+ });
1293
+ }
1294
+
1295
+ function renderProject(dashboard, projectId) {
1296
+ const watcherRunning = dashboard.watcher?.running;
1297
+ const latestPreWarning = dashboard.preWarnings?.active ? dashboard.preWarnings.latest : null;
1298
+ const preWarning = latestPreWarning?.mode === 'dashboard' ? latestPreWarning : null;
1299
+ const alert = dashboard.alerts?.active ? dashboard.alerts.latest : null;
1300
+ const health = dashboard.health?.status || 'unknown';
1301
+ const critical = health === 'critical';
1302
+ let html = '';
1303
+
1304
+ let heroHtml = '';
1305
+ if (preWarning) {
1306
+ heroHtml = hero('risk', t('hero.pre.kicker'), t('hero.pre.title'), preWarning.summary || t('hero.pre.subtitle'));
1307
+ } else if (alert) {
1308
+ heroHtml = hero('alert', t('hero.alert.kicker'), t('alert.filesChangedFast', { count: displayCount(alert.fileCount) }), t('hero.alert.subtitle'));
1309
+ } else if (!watcherRunning) {
1310
+ heroHtml = hero('stopped', t('hero.protection.kicker'), t('hero.protection.stopped'), t('hero.protection.stoppedSub'));
1311
+ } else if (critical) {
1312
+ heroHtml = hero('critical', t('hero.health.kicker'), t('hero.health.critical'), dashboard.health.issues?.[0] || t('hero.health.check'));
1313
+ } else {
1314
+ heroHtml = hero('protected', t('hero.protection.kicker'), t('hero.protection.safe'), t('hero.protection.safeSub'), { live: true });
1315
+ }
1316
+ html += wrapSection(projectId, 'status', t('section.status'), heroHtml, '');
1317
+
1318
+ if (preWarning) {
1319
+ let inner = '<div class="card risk-card">';
1320
+ inner += '<div class="card-title">' + esc(t('card.deletionRisk')) + '</div>';
1321
+ inner += row(t('row.file'), esc(preWarning.file || 'Unknown'), 'orange');
1322
+ inner += row(t('row.risk'), esc(String(preWarning.riskPercent || '?')) + '%', 'orange');
1323
+ if (preWarning.removedMethodCount) {
1324
+ inner += row(t('row.methodsRemoved'), esc(String(preWarning.removedMethodCount)), 'red');
1325
+ }
1326
+ inner += row(t('row.summary'), esc(preWarning.summary || t('hero.pre.subtitle')), 'orange');
1327
+ inner += '<div class="actions">';
1328
+ inner += '<button class="btn" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.openDashboard')) + '</button>';
1329
+ inner += '<button class="btn" data-cmd="cursorGuard.quickRestore">' + esc(t('actions.restore')) + '</button>';
1330
+ inner += '</div>';
1331
+ inner += '</div>';
1332
+ html += wrapSection(projectId, 'pre-warning', t('card.deletionRisk'), inner, '');
1333
+ }
1334
+
1335
+ if (alert) {
1336
+ const expiresTs = alert.expiresAt ? new Date(alert.expiresAt).getTime() : 0;
1337
+ const remain = expiresTs ? Math.max(0, Math.ceil((expiresTs - Date.now()) / 1000)) : 0;
1338
+ const display = formatCountdown(remain);
1339
+ let inner = '<div class="card alert-card" data-expires="' + expiresTs + '">';
1340
+ inner += '<div class="card-title">' + esc(t('card.activeAlert')) + '</div>';
1341
+ inner += row(t('row.window'), (alert.windowSeconds || '?') + 's', 'red');
1342
+ inner += row(t('row.files'), String(alert.fileCount || '?'), 'red');
1343
+ inner += row(t('row.threshold'), String(alert.threshold || '?'), 'yellow');
1344
+ inner += row(t('row.expires'), '<span class="alert-countdown">' + esc(display) + '</span>', 'yellow', true);
1345
+ inner += '<div class="actions">';
1346
+ inner += '<button class="btn" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.viewDetails')) + '</button>';
1347
+ inner += '</div>';
1348
+ inner += '</div>';
1349
+ html += wrapSection(projectId, 'alert', t('card.activeAlert'), inner, '');
1350
+ }
1351
+
1352
+ const gitCount = dashboard.counts?.git?.commits || 0;
1353
+ const shadowCount = dashboard.counts?.shadow?.snapshots || 0;
1354
+ const lastGitTs = dashboard.lastBackup?.git?.timestamp || '';
1355
+ const lastGit = dashboard.lastBackup?.git?.relativeTime || t('stats.never');
1356
+ const freeGB = dashboard.disk?.freeGB;
1357
+ const freeDisplay = typeof freeGB === 'number' ? freeGB.toFixed(1) + ' GB' : 'N/A';
1358
+ const diskWarn = dashboard.disk?.warning;
1359
+ const watcherInfo = watcherStateInfo(dashboard);
1360
+ const healthInfo = healthStateInfo(dashboard);
1361
+
1362
+ let statsInner = '<div class="card">';
1363
+ statsInner += '<div class="card-title">' + esc(t('card.quickStats')) + '</div>';
1364
+ statsInner += row(t('row.watcher'), watcherInfo.label, watcherInfo.tone);
1365
+ statsInner += row(t('row.health'), healthInfo.label, healthInfo.tone);
1366
+ if (lastGitTs) {
1367
+ statsInner += '<div class="row"><span class="row-name">' + esc(t('row.lastBackup')) + '</span><span class="row-value green backup-age" data-backup-ts="' + new Date(lastGitTs).getTime() + '">' + esc(formatRelativeAge(new Date(lastGitTs).getTime())) + '</span></div>';
1368
+ } else {
1369
+ statsInner += row(t('row.lastBackup'), lastGit, 'green');
1370
+ }
1371
+ statsInner += row(t('row.gitBackups'), String(gitCount), 'blue');
1372
+ if (shadowCount > 0) statsInner += row(t('row.shadowCopies'), String(shadowCount), 'blue');
1373
+ statsInner += row(t('row.diskFree'), freeDisplay, diskWarn ? 'yellow' : 'green');
1374
+ statsInner += '</div>';
1375
+ html += wrapSection(projectId, 'quick-stats', t('card.quickStats'), statsInner, '');
1376
+
1377
+ const scope = dashboard.protectionScope || {};
1378
+ const protect = scope.protect || [];
1379
+ const ignore = scope.ignore || [];
1380
+
1381
+ let scopeInner = '<div class="card">';
1382
+ scopeInner += '<div class="card-title">' + esc(t('card.protectionScope')) + '</div>';
1383
+ scopeInner += '<div class="pill-wrap">';
1384
+ scopeInner += '<span class="pill green">' + esc(t('pill.protected', { n: String(scope.fileCount || 0) })) + '</span>';
1385
+ if ((scope.excludedCount || 0) > 0) {
1386
+ scopeInner += '<span class="pill ignore">' + esc(t('pill.excluded', { n: String(scope.excludedCount || 0) })) + '</span>';
1387
+ }
1388
+ scopeInner += '<span class="pill dim">' + esc(t('pill.total', { n: String(scope.totalFiles || 0) })) + '</span>';
1389
+ scopeInner += '</div>';
1390
+
1391
+ if (protect.length > 0) {
1392
+ scopeInner += renderTags(t('tag.protect'), protect, 'green', 'tag-group--protect');
1393
+ }
1394
+ if (ignore.length > 0) {
1395
+ scopeInner += renderTags(t('tag.ignore'), ignore, 'ignore', 'tag-group--ignore');
1396
+ }
1397
+ scopeInner += '</div>';
1398
+ html += wrapSection(projectId, 'scope', t('card.protectionScope'), scopeInner, '');
1399
+
1400
+ return html;
1401
+ }
1402
+
1403
+ function renderTags(label, values, tone, groupClass) {
1404
+ const gc = groupClass ? ' ' + groupClass : '';
1405
+ let html = '<div class="tag-group' + gc + '">';
1406
+ html += '<div class="tag-label">' + esc(label) + ' (' + values.length + ')</div>';
1407
+ html += '<div class="tag-list">';
1408
+ const shown = values.slice(0, 6);
1409
+ for (const value of shown) {
1410
+ html += '<span class="tag ' + tone + '">' + esc(value) + '</span>';
1411
+ }
1412
+ if (values.length > 6) {
1413
+ html += '<span class="tag dim">' + esc(t('tag.more', { n: values.length - 6 })) + '</span>';
1414
+ }
1415
+ html += '</div></div>';
1416
+ return html;
1417
+ }
1418
+
1419
+ function watcherStateInfo(dashboard) {
1420
+ const watcher = dashboard?.watcher || {};
1421
+ if (watcher.running) return { label: t('status.watcher.running'), tone: 'green' };
1422
+ if (watcher.stale) return { label: t('status.watcher.stale'), tone: 'yellow' };
1423
+ return { label: t('status.watcher.stopped'), tone: 'red' };
1424
+ }
1425
+
1426
+ function healthStateInfo(dashboard) {
1427
+ const health = dashboard?.health?.status || 'warning';
1428
+ if (health === 'critical') return { label: t('status.health.critical'), tone: 'red' };
1429
+ if (health === 'healthy') return { label: t('status.health.healthy'), tone: 'green' };
1430
+ return { label: t('status.health.warning'), tone: 'yellow' };
1431
+ }
1432
+
1433
+ function renderActions(projects, primaryProjectId) {
1434
+ const primary = pickPrimaryProject(projects || {});
1435
+ const dashboard = primary?.project?.dashboard || null;
1436
+ const watcherRunning = dashboard?.watcher?.running;
1437
+ const pid = primaryProjectId || primary?.id || 'default';
1438
+
1439
+ let inner = '<div class="cg-actions-wrap"><div class="actions">';
1440
+ inner += '<button class="btn primary" data-cmd="cursorGuard.snapshotNow">' + esc(t('actions.snapshot')) + '</button>';
1441
+ inner += '<button class="btn" data-cmd="cursorGuard.quickRestore">' + esc(t('actions.restore')) + '</button>';
1442
+ inner += watcherRunning
1443
+ ? '<button class="btn" data-cmd="cursorGuard.stopWatcher">' + esc(t('actions.watcherOn')) + '</button>'
1444
+ : '<button class="btn" data-cmd="cursorGuard.startWatcher">' + esc(t('actions.watcherOff')) + '</button>';
1445
+ inner += '<button class="btn" data-cmd="cursorGuard.doctor">' + esc(t('actions.doctor')) + '</button>';
1446
+ inner += '<button class="btn primary full" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.openDashboard')) + '</button>';
1447
+ inner += '</div></div>';
1448
+ return wrapSection(pid, 'actions', t('section.actions'), inner, '');
1449
+ }
1450
+
1451
+ function hero(tone, kicker, title, subtitle, opts) {
1452
+ const pulse = opts && opts.live ? '<span class="cg-pulse-dot" title="Watcher running"></span>' : '';
1453
+ let html = '<div class="hero ' + tone + '">';
1454
+ html += '<div class="hero-top">';
1455
+ html += '<div class="hero-kicker">' + esc(kicker) + '</div>';
1456
+ html += pulse;
1457
+ html += '</div>';
1458
+ html += '<div class="hero-title">' + esc(title) + '</div>';
1459
+ html += '<div class="hero-sub">' + esc(subtitle) + '</div>';
1460
+ html += '</div>';
1461
+ return html;
1462
+ }
1463
+
1464
+ function row(name, value, tone, rawValue) {
1465
+ return '<div class="row"><span class="row-name">' + esc(name) + '</span><span class="row-value ' + tone + '">' + (rawValue ? value : esc(String(value))) + '</span></div>';
1466
+ }
1467
+
1468
+ function esc(value) {
1469
+ return String(value)
1470
+ .replace(/&/g, '&amp;')
1471
+ .replace(/</g, '&lt;')
1472
+ .replace(/>/g, '&gt;')
1473
+ .replace(/"/g, '&quot;');
1474
+ }
1475
+ </script>
1476
+ </body>
1477
+ </html>`;
1478
+ }
1479
+
1480
+ module.exports = { SidebarDashboardProvider };
1481
+
1482
+
1483
+
1484
+