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.
- package/ROADMAP.md +24 -2
- package/package.json +1 -1
- package/references/dashboard/public/app.js +16 -3
- package/references/dashboard/server.js +11 -0
- package/references/lib/core/dashboard.js +8 -1
- package/references/lib/core/doctor.js +1 -1
- package/references/lib/core/snapshot.js +3 -5
- package/references/lib/utils.js +12 -5
- package/references/vscode-extension/dist/{cursor-guard-ide-4.7.5.vsix → cursor-guard-ide-4.7.8.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +16 -3
- package/references/vscode-extension/dist/dashboard/server.js +11 -0
- package/references/vscode-extension/dist/extension.js +165 -3
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/core/dashboard.js +10 -9
- package/references/vscode-extension/dist/lib/core/doctor.js +1 -1
- package/references/vscode-extension/dist/lib/core/snapshot.js +3 -5
- package/references/vscode-extension/dist/lib/dashboard-manager.js +7 -0
- package/references/vscode-extension/dist/lib/sidebar-webview.js +272 -222
- package/references/vscode-extension/dist/lib/utils.js +12 -5
- package/references/vscode-extension/dist/lib/webview-provider.js +70 -27
- package/references/vscode-extension/dist/package.json +49 -2
- package/references/vscode-extension/dist/skill/ROADMAP.md +34 -2
- package/references/vscode-extension/extension.js +101 -3
- package/references/vscode-extension/lib/dashboard-manager.js +7 -0
- package/references/vscode-extension/lib/sidebar-webview.js +129 -5
- package/references/vscode-extension/lib/webview-provider.js +48 -30
- 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,
|
|
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:
|
|
64
|
+
--radius: 8px;
|
|
65
65
|
}
|
|
66
66
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
67
67
|
body {
|
|
68
|
-
font:
|
|
68
|
+
font: 12px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
69
69
|
color: var(--text);
|
|
70
70
|
background: transparent;
|
|
71
|
-
padding:
|
|
71
|
+
padding: 10px;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
/* ── Big status indicator ── */
|
|
75
|
+
.status-hero {
|
|
76
|
+
text-align: center;
|
|
77
|
+
padding: 14px 10px;
|
|
76
78
|
border-radius: var(--radius);
|
|
77
|
-
|
|
78
|
-
margin-bottom: 6px;
|
|
79
|
+
margin-bottom: 10px;
|
|
79
80
|
}
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
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-
|
|
89
|
-
|
|
90
|
-
|
|
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-
|
|
94
|
-
|
|
95
|
-
|
|
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-
|
|
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
|
-
.
|
|
122
|
-
.
|
|
123
|
-
.
|
|
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
|
-
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
152
|
-
.
|
|
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
|
-
|
|
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
|
-
.
|
|
167
|
-
.
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
.
|
|
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
|
-
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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(--
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
.
|
|
211
|
-
|
|
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
|
-
.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
249
|
-
html += renderProject(
|
|
282
|
+
if (!d) { html += '<div class="empty-state">Loading...</div>'; continue; }
|
|
283
|
+
html += renderProject(d);
|
|
250
284
|
}
|
|
251
|
-
html += renderActions();
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
331
|
+
// ── Alert detail card (only when active) ──
|
|
274
332
|
if (hasAlert) {
|
|
275
333
|
const a = d.alerts.latest;
|
|
276
|
-
const
|
|
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-
|
|
279
|
-
h += '<div class="
|
|
280
|
-
h += '<div class="
|
|
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
|
-
//
|
|
346
|
+
// ── Quick stats ──
|
|
285
347
|
const gitC = d.counts?.git?.commits || 0;
|
|
286
348
|
const shadowC = d.counts?.shadow?.snapshots || 0;
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
h += '
|
|
295
|
-
h += '
|
|
296
|
-
h +=
|
|
297
|
-
h +=
|
|
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
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
h += '<
|
|
333
|
-
h += '<div class="
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
h += '</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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
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
|
-
*
|
|
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
|
|
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)
|
|
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;
|