cursor-guard 4.9.1 → 4.9.8
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/README.md +130 -10
- package/README.zh-CN.md +130 -10
- package/ROADMAP.md +65 -8
- package/SKILL.md +32 -22
- package/package.json +3 -2
- package/references/config-reference.md +68 -7
- package/references/config-reference.zh-CN.md +68 -7
- package/references/cursor-guard.example.json +11 -7
- package/references/cursor-guard.schema.json +30 -7
- package/references/dashboard/public/app.js +73 -27
- package/references/dashboard/public/index.html +8 -7
- package/references/lib/auto-backup.js +40 -2
- package/references/lib/core/backups.js +46 -16
- package/references/lib/core/core.test.js +101 -22
- package/references/lib/core/dashboard.js +37 -23
- package/references/lib/core/doctor.js +19 -13
- package/references/lib/core/pre-warning.js +296 -0
- package/references/lib/core/snapshot.js +24 -2
- package/references/lib/core/status.js +15 -7
- package/references/lib/utils.js +46 -20
- package/references/mcp/mcp.test.js +60 -12
- package/references/mcp/server.js +72 -60
- package/references/quickstart.zh-CN.md +46 -21
- package/references/vscode-extension/build-vsix.js +4 -1
- package/references/vscode-extension/dist/LICENSE +65 -0
- package/references/vscode-extension/dist/{cursor-guard-ide-4.9.1.vsix → cursor-guard-ide-4.9.8.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +73 -27
- package/references/vscode-extension/dist/dashboard/public/index.html +8 -7
- package/references/vscode-extension/dist/extension.js +406 -5
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/auto-backup.js +40 -2
- package/references/vscode-extension/dist/lib/core/backups.js +46 -16
- package/references/vscode-extension/dist/lib/core/dashboard.js +37 -23
- package/references/vscode-extension/dist/lib/core/doctor.js +19 -13
- package/references/vscode-extension/dist/lib/core/pre-warning.js +296 -0
- package/references/vscode-extension/dist/lib/core/snapshot.js +24 -2
- package/references/vscode-extension/dist/lib/core/status.js +15 -7
- package/references/vscode-extension/dist/lib/dashboard-manager.js +102 -52
- package/references/vscode-extension/dist/lib/locale.js +36 -0
- package/references/vscode-extension/dist/lib/sidebar-webview.js +1027 -281
- package/references/vscode-extension/dist/lib/status-bar.js +95 -68
- package/references/vscode-extension/dist/lib/tree-view.js +174 -114
- package/references/vscode-extension/dist/lib/utils.js +46 -20
- package/references/vscode-extension/dist/mcp/server.js +395 -31
- package/references/vscode-extension/dist/media/brand-placeholder.png +0 -0
- package/references/vscode-extension/dist/package.json +1 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +65 -8
- package/references/vscode-extension/dist/skill/SKILL.md +32 -22
- package/references/vscode-extension/dist/skill/config-reference.md +68 -7
- package/references/vscode-extension/dist/skill/config-reference.zh-CN.md +68 -7
- package/references/vscode-extension/dist/skill/cursor-guard.example.json +11 -7
- package/references/vscode-extension/dist/skill/cursor-guard.schema.json +30 -7
- package/references/vscode-extension/extension.js +406 -5
- package/references/vscode-extension/lib/dashboard-manager.js +102 -52
- package/references/vscode-extension/lib/locale.js +36 -0
- package/references/vscode-extension/lib/sidebar-webview.js +1027 -281
- package/references/vscode-extension/lib/status-bar.js +95 -68
- package/references/vscode-extension/lib/tree-view.js +174 -114
- package/references/vscode-extension/media/brand-placeholder.png +0 -0
- package/references/vscode-extension/package.json +1 -1
|
@@ -1,39 +1,73 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const vscode = require('vscode');
|
|
4
|
+
const { getLocale, setLocale } = require('./locale');
|
|
4
5
|
|
|
5
6
|
class SidebarDashboardProvider {
|
|
6
|
-
constructor(poller) {
|
|
7
|
+
constructor(poller, context) {
|
|
7
8
|
this._poller = poller;
|
|
9
|
+
this._extensionUri = context?.extensionUri;
|
|
10
|
+
this._localeStorage = context?.globalState;
|
|
11
|
+
this._locale = getLocale(this._localeStorage);
|
|
8
12
|
this._view = null;
|
|
9
13
|
this._sub = poller.onChange(data => this._push(data));
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
resolveWebviewView(webviewView) {
|
|
13
17
|
this._view = webviewView;
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
const webview = webviewView.webview;
|
|
19
|
+
webview.options = { enableScripts: true };
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
}
|
|
19
43
|
if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
|
|
20
44
|
});
|
|
21
45
|
|
|
22
46
|
webviewView.onDidChangeVisibility(() => {
|
|
23
|
-
if (webviewView.visible)
|
|
47
|
+
if (webviewView.visible) {
|
|
48
|
+
this._postLocale();
|
|
49
|
+
this._push(this._poller.data);
|
|
50
|
+
}
|
|
24
51
|
});
|
|
25
52
|
}
|
|
26
53
|
|
|
54
|
+
_postLocale() {
|
|
55
|
+
if (!this._view) return;
|
|
56
|
+
this._view.webview.postMessage({ type: 'locale', locale: this._locale });
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
_push(data) {
|
|
28
60
|
if (!this._view?.visible) return;
|
|
61
|
+
|
|
29
62
|
const payload = {};
|
|
30
|
-
for (const [id,
|
|
63
|
+
for (const [id, project] of data) {
|
|
31
64
|
payload[id] = {
|
|
32
|
-
name:
|
|
33
|
-
dashboard:
|
|
34
|
-
backups: (
|
|
65
|
+
name: project.name || id,
|
|
66
|
+
dashboard: project.dashboard,
|
|
67
|
+
backups: (project.backups || []).slice(0, 5),
|
|
35
68
|
};
|
|
36
69
|
}
|
|
70
|
+
|
|
37
71
|
this._view.webview.postMessage({ type: 'update', data: payload });
|
|
38
72
|
}
|
|
39
73
|
|
|
@@ -42,218 +76,883 @@ class SidebarDashboardProvider {
|
|
|
42
76
|
}
|
|
43
77
|
}
|
|
44
78
|
|
|
45
|
-
function
|
|
79
|
+
function escHtmlAttr(value) {
|
|
80
|
+
return String(value)
|
|
81
|
+
.replace(/&/g, '&')
|
|
82
|
+
.replace(/"/g, '"');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _getHtml(brandInnerHtml) {
|
|
86
|
+
brandInnerHtml = brandInnerHtml || '';
|
|
87
|
+
const brandMarkClass =
|
|
88
|
+
'cg-brand-mark' + (brandInnerHtml ? ' cg-brand-mark--has-img' : '');
|
|
46
89
|
return `<!DOCTYPE html>
|
|
47
90
|
<html lang="en">
|
|
48
91
|
<head>
|
|
49
92
|
<meta charset="UTF-8">
|
|
50
93
|
<style>
|
|
51
94
|
:root {
|
|
52
|
-
--
|
|
53
|
-
--surface: #
|
|
54
|
-
--border:
|
|
55
|
-
--text: #
|
|
56
|
-
--
|
|
57
|
-
--green: #
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
--
|
|
64
|
-
--
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
--surface: var(--vscode-sideBar-background, #1f2430);
|
|
96
|
+
--surface-2: var(--vscode-editorWidget-background, #252a38);
|
|
97
|
+
--border: var(--vscode-widget-border, rgba(120, 130, 160, 0.22));
|
|
98
|
+
--text: var(--vscode-foreground, #e8eaf0);
|
|
99
|
+
--muted: var(--vscode-descriptionForeground, #9aa4bd);
|
|
100
|
+
--green: var(--vscode-testing-iconPassed, #89d18a);
|
|
101
|
+
--yellow: var(--vscode-editorWarning-foreground, #e4c06a);
|
|
102
|
+
--red: var(--vscode-testing-iconFailed, #f0a0a0);
|
|
103
|
+
--orange: var(--vscode-charts-orange, #f0b070);
|
|
104
|
+
--blue: var(--vscode-textLink-foreground, #8eb6ff);
|
|
105
|
+
--radius: 12px;
|
|
106
|
+
--radius-lg: 14px;
|
|
107
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
|
|
108
|
+
--shadow-soft: 0 4px 14px rgba(0, 0, 0, 0.12);
|
|
109
|
+
--accent: var(--blue);
|
|
110
|
+
--glow-green: color-mix(in srgb, var(--green) 35%, transparent);
|
|
111
|
+
--glow-blue: color-mix(in srgb, var(--blue) 28%, transparent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
* { box-sizing: border-box; }
|
|
67
115
|
body {
|
|
68
|
-
|
|
116
|
+
margin: 0;
|
|
117
|
+
padding: 0;
|
|
118
|
+
font: 13px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
69
119
|
color: var(--text);
|
|
70
120
|
background: transparent;
|
|
71
|
-
|
|
121
|
+
-webkit-font-smoothing: antialiased;
|
|
72
122
|
}
|
|
73
123
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
124
|
+
.cg-shell {
|
|
125
|
+
position: relative;
|
|
126
|
+
padding: 10px 10px 18px;
|
|
127
|
+
min-height: 100%;
|
|
128
|
+
background:
|
|
129
|
+
radial-gradient(120% 80% at 0% -20%, var(--glow-blue), transparent 55%),
|
|
130
|
+
radial-gradient(90% 60% at 100% 0%, var(--glow-green), transparent 45%);
|
|
80
131
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
|
|
133
|
+
.cg-brand {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 10px;
|
|
137
|
+
margin-bottom: 14px;
|
|
138
|
+
padding: 10px 12px;
|
|
139
|
+
border-radius: var(--radius-lg);
|
|
140
|
+
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
|
141
|
+
background: color-mix(in srgb, var(--surface-2) 75%, transparent);
|
|
142
|
+
box-shadow: var(--shadow-soft);
|
|
143
|
+
backdrop-filter: blur(8px);
|
|
84
144
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
|
|
146
|
+
.cg-brand-mark {
|
|
147
|
+
width: 36px;
|
|
148
|
+
height: 36px;
|
|
149
|
+
border-radius: 10px;
|
|
150
|
+
flex-shrink: 0;
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
background: linear-gradient(135deg, color-mix(in srgb, var(--blue) 55%, #1a1a2e), color-mix(in srgb, var(--green) 40%, #1a1a2e));
|
|
156
|
+
box-shadow:
|
|
157
|
+
0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent) inset,
|
|
158
|
+
0 4px 12px color-mix(in srgb, var(--blue) 25%, transparent);
|
|
88
159
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
160
|
+
|
|
161
|
+
.cg-brand-mark--has-img {
|
|
162
|
+
background: color-mix(in srgb, var(--surface-2) 88%, var(--text));
|
|
163
|
+
padding: 4px;
|
|
164
|
+
box-shadow:
|
|
165
|
+
0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent) inset,
|
|
166
|
+
0 2px 10px color-mix(in srgb, var(--blue) 18%, transparent);
|
|
92
167
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
168
|
+
|
|
169
|
+
.cg-brand-mark-img {
|
|
170
|
+
width: 100%;
|
|
171
|
+
height: 100%;
|
|
172
|
+
object-fit: contain;
|
|
173
|
+
display: block;
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
user-select: none;
|
|
96
176
|
}
|
|
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; }
|
|
100
177
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
margin-bottom: 10px;
|
|
178
|
+
.cg-brand-text {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 2px;
|
|
182
|
+
min-width: 0;
|
|
183
|
+
flex: 1;
|
|
108
184
|
}
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
|
|
186
|
+
.cg-brand-title {
|
|
187
|
+
font-size: 14px;
|
|
188
|
+
font-weight: 800;
|
|
189
|
+
letter-spacing: -0.03em;
|
|
190
|
+
line-height: 1.2;
|
|
191
|
+
background: linear-gradient(90deg, var(--text), color-mix(in srgb, var(--text) 72%, var(--blue)));
|
|
192
|
+
-webkit-background-clip: text;
|
|
193
|
+
background-clip: text;
|
|
194
|
+
color: transparent;
|
|
116
195
|
}
|
|
117
|
-
.alert-card .btn-sm:hover { border-color: var(--blue); color: var(--blue); }
|
|
118
196
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
197
|
+
.cg-brand-meta {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
gap: 3px;
|
|
201
|
+
min-width: 0;
|
|
202
|
+
margin-top: 1px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cg-brand-sub {
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
opacity: 0.88;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.cg-brand-sub--project {
|
|
211
|
+
font-size: 11px;
|
|
212
|
+
letter-spacing: 0.02em;
|
|
213
|
+
color: color-mix(in srgb, var(--text) 92%, var(--muted));
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
text-overflow: ellipsis;
|
|
216
|
+
white-space: nowrap;
|
|
126
217
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
218
|
+
|
|
219
|
+
.cg-brand-sub--backup {
|
|
220
|
+
font-size: 10px;
|
|
221
|
+
letter-spacing: 0.1em;
|
|
222
|
+
text-transform: uppercase;
|
|
223
|
+
color: var(--muted);
|
|
130
224
|
}
|
|
131
|
-
|
|
225
|
+
|
|
226
|
+
.cg-brand-backup-prefix {
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
color: var(--muted);
|
|
229
|
+
margin-right: 2px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#cg-brand-backup .backup-age[data-backup-ts] {
|
|
233
|
+
color: var(--green);
|
|
234
|
+
font-weight: 700;
|
|
235
|
+
letter-spacing: 0.06em;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.cg-brand-tools {
|
|
132
239
|
display: flex;
|
|
133
|
-
justify-content: space-between;
|
|
134
240
|
align-items: center;
|
|
135
|
-
|
|
241
|
+
justify-content: flex-end;
|
|
242
|
+
flex-shrink: 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.lang-btn {
|
|
246
|
+
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
|
|
247
|
+
border-radius: 999px;
|
|
248
|
+
background: color-mix(in srgb, var(--surface-2) 85%, transparent);
|
|
249
|
+
color: var(--text);
|
|
250
|
+
padding: 6px 11px;
|
|
251
|
+
font: inherit;
|
|
136
252
|
font-size: 11px;
|
|
253
|
+
font-weight: 700;
|
|
254
|
+
letter-spacing: 0.03em;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
|
137
257
|
}
|
|
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); }
|
|
143
258
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
259
|
+
.lang-btn:hover {
|
|
260
|
+
border-color: color-mix(in srgb, var(--blue) 55%, var(--border));
|
|
261
|
+
color: var(--blue);
|
|
262
|
+
box-shadow: var(--shadow-soft);
|
|
147
263
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
264
|
+
|
|
265
|
+
.lang-btn:focus-visible {
|
|
266
|
+
outline: 2px solid color-mix(in srgb, var(--blue) 70%, transparent);
|
|
267
|
+
outline-offset: 2px;
|
|
152
268
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
font-weight: 600;
|
|
269
|
+
|
|
270
|
+
.empty {
|
|
271
|
+
padding: 26px 12px;
|
|
157
272
|
text-align: center;
|
|
273
|
+
color: var(--muted);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.hero {
|
|
277
|
+
position: relative;
|
|
278
|
+
margin-bottom: 12px;
|
|
279
|
+
padding: 16px 14px;
|
|
280
|
+
border-radius: var(--radius-lg);
|
|
158
281
|
border: 1px solid var(--border);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
282
|
+
background: color-mix(in srgb, var(--vscode-editor-background, #1e1e1e) 92%, transparent);
|
|
283
|
+
box-shadow: var(--shadow);
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.hero::before {
|
|
288
|
+
content: "";
|
|
289
|
+
position: absolute;
|
|
290
|
+
left: 0;
|
|
291
|
+
top: 0;
|
|
292
|
+
right: 0;
|
|
293
|
+
height: 3px;
|
|
294
|
+
opacity: 0.85;
|
|
295
|
+
background: linear-gradient(90deg, var(--muted), var(--muted));
|
|
296
|
+
pointer-events: none;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.hero.protected::before {
|
|
300
|
+
background: linear-gradient(90deg, var(--green), var(--blue));
|
|
301
|
+
box-shadow: 0 0 16px var(--glow-green);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.hero.risk::before {
|
|
305
|
+
background: linear-gradient(90deg, var(--orange), var(--yellow));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.hero.alert::before,
|
|
309
|
+
.hero.critical::before {
|
|
310
|
+
background: linear-gradient(90deg, var(--red), var(--orange));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.hero.stopped::before {
|
|
314
|
+
background: linear-gradient(90deg, var(--yellow), var(--muted));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.hero-top {
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
justify-content: space-between;
|
|
321
|
+
gap: 8px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.hero-top .hero-kicker {
|
|
325
|
+
margin: 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.cg-pulse-dot {
|
|
329
|
+
width: 8px;
|
|
330
|
+
height: 8px;
|
|
331
|
+
border-radius: 50%;
|
|
332
|
+
background: var(--green);
|
|
333
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent);
|
|
334
|
+
animation: cg-pulse 2.2s ease-out infinite;
|
|
335
|
+
flex-shrink: 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@keyframes cg-pulse {
|
|
339
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 45%, transparent); opacity: 1; }
|
|
340
|
+
70% { box-shadow: 0 0 0 8px transparent; opacity: 0.85; }
|
|
341
|
+
100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@media (prefers-reduced-motion: reduce) {
|
|
345
|
+
.cg-pulse-dot { animation: none; box-shadow: 0 0 6px color-mix(in srgb, var(--green) 35%, transparent); }
|
|
346
|
+
.card { transition: none; }
|
|
347
|
+
.card-chevron { transition: none; }
|
|
348
|
+
.card-head { transition: none; }
|
|
349
|
+
.btn { transition: none; }
|
|
350
|
+
.lang-btn { transition: none; }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.hero.risk {
|
|
354
|
+
border-color: rgba(244, 179, 110, 0.45);
|
|
355
|
+
background: rgba(244, 179, 110, 0.12);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.hero.alert {
|
|
359
|
+
border-color: rgba(242, 159, 159, 0.45);
|
|
360
|
+
background: rgba(242, 159, 159, 0.12);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.hero.stopped {
|
|
364
|
+
border-color: rgba(245, 213, 133, 0.45);
|
|
365
|
+
background: rgba(245, 213, 133, 0.10);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.hero.critical {
|
|
369
|
+
border-color: rgba(242, 159, 159, 0.60);
|
|
370
|
+
background: rgba(242, 159, 159, 0.16);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.hero.protected {
|
|
374
|
+
border-color: rgba(154, 215, 162, 0.45);
|
|
375
|
+
background: rgba(154, 215, 162, 0.10);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.hero-kicker {
|
|
379
|
+
font-size: 10px;
|
|
380
|
+
letter-spacing: 0.1em;
|
|
381
|
+
text-transform: uppercase;
|
|
382
|
+
color: var(--muted);
|
|
383
|
+
font-weight: 600;
|
|
384
|
+
opacity: 0.92;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.hero-title {
|
|
388
|
+
margin-top: 8px;
|
|
389
|
+
font-size: 18px;
|
|
390
|
+
font-weight: 800;
|
|
391
|
+
letter-spacing: -0.03em;
|
|
392
|
+
line-height: 1.2;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.hero-sub {
|
|
396
|
+
margin-top: 6px;
|
|
397
|
+
color: var(--muted);
|
|
398
|
+
font-size: 12px;
|
|
399
|
+
line-height: 1.45;
|
|
400
|
+
opacity: 0.95;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.card {
|
|
404
|
+
position: relative;
|
|
405
|
+
margin-bottom: 12px;
|
|
406
|
+
padding: 12px 14px;
|
|
407
|
+
border-radius: var(--radius-lg);
|
|
408
|
+
border: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
|
|
409
|
+
background: linear-gradient(
|
|
410
|
+
165deg,
|
|
411
|
+
color-mix(in srgb, var(--surface-2) 100%, var(--text)) 0%,
|
|
412
|
+
color-mix(in srgb, var(--surface-2) 96%, transparent) 100%
|
|
413
|
+
);
|
|
414
|
+
box-shadow: var(--shadow);
|
|
415
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.card:hover {
|
|
419
|
+
border-color: color-mix(in srgb, var(--border) 70%, var(--blue));
|
|
420
|
+
box-shadow: var(--shadow-soft);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.card.risk-card {
|
|
424
|
+
border-color: rgba(244, 179, 110, 0.45);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.card.alert-card {
|
|
428
|
+
border-color: rgba(242, 159, 159, 0.45);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.card.cg-collapsible {
|
|
432
|
+
padding-top: 0;
|
|
433
|
+
padding-bottom: 12px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.card-head {
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
justify-content: space-between;
|
|
440
|
+
gap: 10px;
|
|
441
|
+
width: calc(100% + 28px);
|
|
442
|
+
margin: 0 -14px 0 -14px;
|
|
443
|
+
padding: 12px 14px;
|
|
444
|
+
border: none;
|
|
445
|
+
border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
|
|
446
|
+
background: transparent;
|
|
447
|
+
color: inherit;
|
|
162
448
|
cursor: pointer;
|
|
163
|
-
|
|
449
|
+
text-align: left;
|
|
450
|
+
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
451
|
+
transition: background 0.15s ease;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.card-head:hover {
|
|
455
|
+
background: color-mix(in srgb, var(--text) 5%, transparent);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.card.cg-collapsible.is-collapsed .card-head {
|
|
459
|
+
border-bottom-color: transparent;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.card-head .card-title {
|
|
463
|
+
margin: 0;
|
|
464
|
+
padding: 0;
|
|
465
|
+
border: none;
|
|
466
|
+
flex: 1;
|
|
467
|
+
min-width: 0;
|
|
164
468
|
}
|
|
165
|
-
|
|
166
|
-
.
|
|
167
|
-
|
|
168
|
-
|
|
469
|
+
|
|
470
|
+
.card-title {
|
|
471
|
+
margin-bottom: 10px;
|
|
472
|
+
font-size: 10px;
|
|
473
|
+
font-weight: 700;
|
|
474
|
+
letter-spacing: 0.1em;
|
|
475
|
+
text-transform: uppercase;
|
|
476
|
+
color: var(--muted);
|
|
477
|
+
padding-bottom: 8px;
|
|
478
|
+
border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
|
|
169
479
|
}
|
|
170
|
-
.action-btn.full { grid-column: 1 / -1; }
|
|
171
|
-
.action-btn .icon { margin-right: 3px; }
|
|
172
480
|
|
|
173
|
-
|
|
174
|
-
|
|
481
|
+
.card-chevron {
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
justify-content: center;
|
|
485
|
+
width: 22px;
|
|
486
|
+
height: 22px;
|
|
487
|
+
border-radius: 6px;
|
|
488
|
+
font-size: 11px;
|
|
489
|
+
line-height: 1;
|
|
490
|
+
color: var(--muted);
|
|
491
|
+
background: color-mix(in srgb, var(--text) 6%, transparent);
|
|
492
|
+
transition: transform 0.2s ease, background 0.15s ease;
|
|
493
|
+
flex-shrink: 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.card-head:hover .card-chevron {
|
|
497
|
+
background: color-mix(in srgb, var(--text) 10%, transparent);
|
|
498
|
+
color: var(--text);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.card.is-collapsed .card-chevron {
|
|
502
|
+
transform: rotate(-90deg);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.card-panel {
|
|
506
|
+
padding-top: 12px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.card-panel .actions {
|
|
510
|
+
margin-top: 10px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.card.is-collapsed .card-panel {
|
|
514
|
+
display: none;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.row {
|
|
518
|
+
display: flex;
|
|
519
|
+
justify-content: space-between;
|
|
520
|
+
align-items: flex-start;
|
|
521
|
+
gap: 12px;
|
|
522
|
+
padding: 5px 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.row-name {
|
|
526
|
+
color: var(--muted);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.row-value {
|
|
530
|
+
text-align: right;
|
|
531
|
+
font-weight: 600;
|
|
532
|
+
font-variant-numeric: tabular-nums;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.row-value.green { color: var(--green); }
|
|
536
|
+
.row-value.blue { color: var(--blue); }
|
|
537
|
+
.row-value.yellow { color: var(--yellow); }
|
|
538
|
+
.row-value.orange { color: var(--orange); }
|
|
539
|
+
.row-value.red { color: var(--red); }
|
|
540
|
+
|
|
541
|
+
.pill-wrap {
|
|
175
542
|
display: flex;
|
|
176
543
|
flex-wrap: wrap;
|
|
177
544
|
gap: 6px;
|
|
178
545
|
margin-bottom: 8px;
|
|
179
546
|
}
|
|
180
|
-
|
|
547
|
+
|
|
548
|
+
.pill {
|
|
549
|
+
padding: 4px 10px;
|
|
550
|
+
border-radius: 999px;
|
|
181
551
|
font-size: 11px;
|
|
182
552
|
font-weight: 600;
|
|
183
|
-
|
|
184
|
-
border-radius: 10px;
|
|
185
|
-
}
|
|
186
|
-
.scope-chip.protected {
|
|
187
|
-
background: rgba(166,227,161,0.12);
|
|
188
|
-
color: var(--green);
|
|
189
|
-
}
|
|
190
|
-
.scope-chip.excluded {
|
|
191
|
-
background: rgba(243,139,168,0.12);
|
|
192
|
-
color: var(--red);
|
|
553
|
+
border: 1px solid transparent;
|
|
193
554
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
.
|
|
199
|
-
|
|
555
|
+
|
|
556
|
+
.pill.green { background: rgba(154, 215, 162, 0.12); color: var(--green); }
|
|
557
|
+
.pill.red { background: rgba(242, 159, 159, 0.12); color: var(--red); }
|
|
558
|
+
.pill.orange { background: rgba(244, 179, 110, 0.12); color: var(--orange); }
|
|
559
|
+
.pill.dim { background: rgba(154, 164, 189, 0.12); color: var(--muted); }
|
|
560
|
+
|
|
561
|
+
.tag-group { margin-top: 8px; }
|
|
562
|
+
.tag-label {
|
|
563
|
+
margin-bottom: 4px;
|
|
200
564
|
font-size: 10px;
|
|
201
565
|
font-weight: 700;
|
|
566
|
+
letter-spacing: 0.06em;
|
|
202
567
|
text-transform: uppercase;
|
|
203
|
-
|
|
204
|
-
display: block;
|
|
205
|
-
margin-bottom: 4px;
|
|
568
|
+
color: var(--muted);
|
|
206
569
|
}
|
|
207
|
-
|
|
208
|
-
.
|
|
209
|
-
.scope-tags {
|
|
570
|
+
|
|
571
|
+
.tag-list {
|
|
210
572
|
display: flex;
|
|
211
573
|
flex-wrap: wrap;
|
|
212
574
|
gap: 4px;
|
|
213
575
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
padding: 2px 6px;
|
|
217
|
-
border-radius: 4px;
|
|
218
|
-
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
576
|
+
|
|
577
|
+
.tag {
|
|
219
578
|
max-width: 100%;
|
|
220
579
|
overflow: hidden;
|
|
221
580
|
text-overflow: ellipsis;
|
|
222
581
|
white-space: nowrap;
|
|
582
|
+
padding: 4px 8px;
|
|
583
|
+
border-radius: 8px;
|
|
584
|
+
border: 1px solid var(--border);
|
|
585
|
+
font: 10px/1.45 ui-monospace, Consolas, "Cascadia Code", monospace;
|
|
223
586
|
}
|
|
224
|
-
|
|
225
|
-
|
|
587
|
+
|
|
588
|
+
.tag.green {
|
|
226
589
|
color: var(--green);
|
|
227
|
-
border:
|
|
590
|
+
border-color: rgba(154, 215, 162, 0.3);
|
|
591
|
+
background: rgba(154, 215, 162, 0.08);
|
|
228
592
|
}
|
|
229
|
-
|
|
230
|
-
|
|
593
|
+
|
|
594
|
+
.tag.red {
|
|
231
595
|
color: var(--red);
|
|
232
|
-
border:
|
|
596
|
+
border-color: rgba(242, 159, 159, 0.3);
|
|
597
|
+
background: rgba(242, 159, 159, 0.08);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.tag.dim {
|
|
601
|
+
color: var(--muted);
|
|
233
602
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
603
|
+
|
|
604
|
+
.cg-actions-wrap {
|
|
605
|
+
margin-top: 4px;
|
|
606
|
+
padding-top: 14px;
|
|
607
|
+
border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.actions {
|
|
611
|
+
display: grid;
|
|
612
|
+
grid-template-columns: 1fr 1fr;
|
|
613
|
+
gap: 8px;
|
|
614
|
+
margin-top: 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.btn {
|
|
237
618
|
border: 1px solid var(--border);
|
|
619
|
+
border-radius: var(--radius);
|
|
620
|
+
background: color-mix(in srgb, var(--surface-2) 88%, var(--text));
|
|
621
|
+
color: var(--text);
|
|
622
|
+
padding: 9px 8px;
|
|
623
|
+
font: inherit;
|
|
624
|
+
font-weight: 600;
|
|
625
|
+
font-size: 12px;
|
|
626
|
+
cursor: pointer;
|
|
627
|
+
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.btn:hover {
|
|
631
|
+
border-color: color-mix(in srgb, var(--blue) 55%, var(--border));
|
|
632
|
+
color: var(--blue);
|
|
633
|
+
box-shadow: var(--shadow-soft);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.btn:focus-visible {
|
|
637
|
+
outline: 2px solid color-mix(in srgb, var(--blue) 70%, transparent);
|
|
638
|
+
outline-offset: 2px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.btn.primary {
|
|
642
|
+
background: linear-gradient(
|
|
643
|
+
165deg,
|
|
644
|
+
color-mix(in srgb, var(--blue) 22%, var(--surface-2)),
|
|
645
|
+
color-mix(in srgb, var(--blue) 10%, var(--surface-2))
|
|
646
|
+
);
|
|
647
|
+
border-color: color-mix(in srgb, var(--blue) 45%, var(--border));
|
|
238
648
|
}
|
|
239
649
|
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
650
|
+
.btn.primary:hover {
|
|
651
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--blue) 25%, transparent), var(--shadow-soft);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.btn.full {
|
|
655
|
+
grid-column: 1 / -1;
|
|
243
656
|
}
|
|
244
657
|
</style>
|
|
245
658
|
</head>
|
|
246
659
|
<body>
|
|
247
|
-
<div
|
|
248
|
-
<
|
|
660
|
+
<div class="cg-shell">
|
|
661
|
+
<header class="cg-brand" aria-label="Cursor Guard">
|
|
662
|
+
<div class="${brandMarkClass}" aria-hidden="true">${brandInnerHtml}</div>
|
|
663
|
+
<div class="cg-brand-text">
|
|
664
|
+
<span class="cg-brand-title" id="cg-brand-title">Cursor Guard</span>
|
|
665
|
+
<div class="cg-brand-meta">
|
|
666
|
+
<span class="cg-brand-sub cg-brand-sub--project" id="cg-brand-project">-</span>
|
|
667
|
+
<div class="cg-brand-sub cg-brand-sub--backup" id="cg-brand-backup">
|
|
668
|
+
<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>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="cg-brand-tools">
|
|
673
|
+
<button id="lang-toggle" class="lang-btn" type="button">中文</button>
|
|
674
|
+
</div>
|
|
675
|
+
</header>
|
|
676
|
+
<div id="root">
|
|
677
|
+
<div class="empty">Waiting for data...</div>
|
|
678
|
+
</div>
|
|
249
679
|
</div>
|
|
250
680
|
<script>
|
|
251
681
|
const vscode = acquireVsCodeApi();
|
|
682
|
+
const root = document.getElementById('root');
|
|
683
|
+
const brandTitle = document.getElementById('cg-brand-title');
|
|
684
|
+
const brandProject = document.getElementById('cg-brand-project');
|
|
685
|
+
const brandBackupPrefix = document.getElementById('cg-brand-backup-prefix');
|
|
686
|
+
const brandBackupAge = document.getElementById('cg-brand-backup-age');
|
|
687
|
+
const langToggle = document.getElementById('lang-toggle');
|
|
688
|
+
const savedState = vscode.getState() || {};
|
|
689
|
+
let _locale = savedState.locale || ((navigator.language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US');
|
|
252
690
|
let _alertExpiresAt = 0;
|
|
691
|
+
let _projects = {};
|
|
692
|
+
|
|
693
|
+
const I18N = {
|
|
694
|
+
'en-US': {
|
|
695
|
+
'chrome.title': 'Cursor Guard',
|
|
696
|
+
'chrome.switch': '\u4e2d\u6587',
|
|
697
|
+
'state.waiting': 'Waiting for data...',
|
|
698
|
+
'state.loading': 'Loading...',
|
|
699
|
+
'state.empty': 'No projects detected.<br>Add .cursor-guard.json to get started.',
|
|
700
|
+
'brand.noWorkspace': 'No workspace',
|
|
701
|
+
'brand.addConfig': 'Add .cursor-guard.json',
|
|
702
|
+
'brand.loadingBackup': 'Loading backup...',
|
|
703
|
+
'brand.noGitBackup': 'No Git backup yet',
|
|
704
|
+
'brand.backupPrefix': 'Last backup',
|
|
705
|
+
'hero.pre.kicker': 'Pre-Warning',
|
|
706
|
+
'hero.pre.title': 'Delete Risk',
|
|
707
|
+
'hero.pre.subtitle': 'Review pending destructive edit',
|
|
708
|
+
'hero.alert.kicker': 'Change Alert',
|
|
709
|
+
'hero.alert.subtitle': 'Abnormal change velocity detected',
|
|
710
|
+
'hero.protection.kicker': 'Protection',
|
|
711
|
+
'hero.protection.stopped': 'Watcher Stopped',
|
|
712
|
+
'hero.protection.stoppedSub': 'Start watcher to enable continuous protection',
|
|
713
|
+
'hero.health.kicker': 'Health',
|
|
714
|
+
'hero.health.critical': 'Critical Issue',
|
|
715
|
+
'hero.health.check': 'Check diagnostics',
|
|
716
|
+
'hero.protection.safe': 'Protected',
|
|
717
|
+
'hero.protection.safeSub': 'Watcher running and backups healthy',
|
|
718
|
+
'card.deletionRisk': 'Deletion Risk',
|
|
719
|
+
'card.activeAlert': 'Active Alert',
|
|
720
|
+
'card.quickStats': 'Quick Stats',
|
|
721
|
+
'card.protectionScope': 'Protection Scope',
|
|
722
|
+
'row.file': 'File',
|
|
723
|
+
'row.risk': 'Risk',
|
|
724
|
+
'row.methodsRemoved': 'Methods removed',
|
|
725
|
+
'row.summary': 'Summary',
|
|
726
|
+
'row.window': 'Window',
|
|
727
|
+
'row.files': 'Files',
|
|
728
|
+
'row.threshold': 'Threshold',
|
|
729
|
+
'row.expires': 'Expires',
|
|
730
|
+
'row.watcher': 'Watcher',
|
|
731
|
+
'row.health': 'Health',
|
|
732
|
+
'row.lastBackup': 'Last backup',
|
|
733
|
+
'row.gitBackups': 'Git backups',
|
|
734
|
+
'row.shadowCopies': 'Shadow copies',
|
|
735
|
+
'row.diskFree': 'Disk free',
|
|
736
|
+
'status.watcher.running': 'Running',
|
|
737
|
+
'status.watcher.stale': 'Stale Lock',
|
|
738
|
+
'status.watcher.stopped': 'Stopped',
|
|
739
|
+
'status.health.healthy': 'Healthy',
|
|
740
|
+
'status.health.warning': 'Warning',
|
|
741
|
+
'status.health.critical': 'Critical',
|
|
742
|
+
'pill.protected': '{n} protected',
|
|
743
|
+
'pill.excluded': '{n} excluded',
|
|
744
|
+
'pill.total': '{n} total',
|
|
745
|
+
'tag.protect': 'Protect',
|
|
746
|
+
'tag.ignore': 'Ignore',
|
|
747
|
+
'tag.more': '+{n} more',
|
|
748
|
+
'actions.openDashboard': 'Open Dashboard',
|
|
749
|
+
'actions.restore': 'Restore',
|
|
750
|
+
'actions.viewDetails': 'View Details',
|
|
751
|
+
'actions.snapshot': 'Snapshot',
|
|
752
|
+
'actions.watcherOn': 'Stop Watcher',
|
|
753
|
+
'actions.watcherOff': 'Start Watcher',
|
|
754
|
+
'actions.doctor': 'Doctor',
|
|
755
|
+
'stats.never': 'never',
|
|
756
|
+
'misc.unknown': 'Unknown',
|
|
757
|
+
'misc.na': 'N/A',
|
|
758
|
+
'time.secondsAgo': '{n}s ago',
|
|
759
|
+
'time.minutesAgo': '{m}m {s}s ago',
|
|
760
|
+
'time.hoursAgo': '{h}h {m}m ago',
|
|
761
|
+
'time.daysAgo': '{d}d ago',
|
|
762
|
+
'time.seconds': '{n}s',
|
|
763
|
+
'time.minutes': '{m}m {s}s',
|
|
764
|
+
'alert.filesChangedFast': '{count} files changed fast'
|
|
765
|
+
},
|
|
766
|
+
'zh-CN': {
|
|
767
|
+
'chrome.title': 'Cursor Guard',
|
|
768
|
+
'chrome.switch': 'EN',
|
|
769
|
+
'state.waiting': '绛夊緟鏁版嵁涓?..',
|
|
770
|
+
'state.loading': '鍔犺浇涓?..',
|
|
771
|
+
'state.empty': '鏈娴嬪埌椤圭洰銆?br>娣诲姞 .cursor-guard.json 鍚庡嵆鍙惎鐢ㄣ€?,
|
|
772
|
+
'hero.pre.kicker': '浜嬪厛棰勮',
|
|
773
|
+
'hero.pre.title': '鍒犻櫎椋庨櫓',
|
|
774
|
+
'hero.pre.subtitle': '璇峰厛妫€鏌ヨ繖娆$牬鍧忔€х紪杈?,
|
|
775
|
+
'hero.alert.kicker': '鍙樻洿鍛婅',
|
|
776
|
+
'hero.alert.subtitle': '妫€娴嬪埌寮傚父楂橀鏂囦欢鍙樻洿',
|
|
777
|
+
'hero.protection.kicker': '淇濇姢鐘舵€?,
|
|
778
|
+
'hero.protection.stopped': 'Watcher 鏈繍琛?,
|
|
779
|
+
'hero.protection.stoppedSub': '鍚姩 watcher 浠ュ紑鍚寔缁繚鎶?,
|
|
780
|
+
'hero.health.kicker': '鍋ュ悍鐘舵€?,
|
|
781
|
+
'hero.health.critical': '涓ラ噸闂',
|
|
782
|
+
'hero.health.check': '璇锋鏌ヨ瘖鏂粨鏋?,
|
|
783
|
+
'hero.protection.safe': '淇濇姢涓?,
|
|
784
|
+
'hero.protection.safeSub': 'Watcher 姝e湪杩愯锛屽浠界姸鎬佸仴搴?,
|
|
785
|
+
'card.deletionRisk': '鍒犻櫎椋庨櫓',
|
|
786
|
+
'card.activeAlert': '娲昏穬鍛婅',
|
|
787
|
+
'card.quickStats': '蹇€熸瑙?,
|
|
788
|
+
'card.protectionScope': '淇濇姢鑼冨洿',
|
|
789
|
+
'row.file': '鏂囦欢',
|
|
790
|
+
'row.risk': '椋庨櫓',
|
|
791
|
+
'row.methodsRemoved': '绉婚櫎鐨勬柟娉曟暟',
|
|
792
|
+
'row.summary': '鎽樿',
|
|
793
|
+
'row.window': '绐楀彛',
|
|
794
|
+
'row.files': '鏂囦欢鏁?,
|
|
795
|
+
'row.threshold': '闃堝€?,
|
|
796
|
+
'row.expires': '鍓╀綑鏃堕棿',
|
|
797
|
+
'row.watcher': '\u76d1\u63a7',
|
|
798
|
+
'row.health': '\u5065\u5eb7',
|
|
799
|
+
'row.lastBackup': '涓婃澶囦唤',
|
|
800
|
+
'row.gitBackups': 'Git 澶囦唤鏁?,
|
|
801
|
+
'row.shadowCopies': 'Shadow 澶囦唤鏁?,
|
|
802
|
+
'row.diskFree': '鍓╀綑纾佺洏',
|
|
803
|
+
'status.watcher.running': '\u8fd0\u884c\u4e2d',
|
|
804
|
+
'status.watcher.stale': '\u9501\u6b8b\u7559',
|
|
805
|
+
'status.watcher.stopped': '\u5df2\u505c\u6b62',
|
|
806
|
+
'status.health.healthy': '\u5065\u5eb7',
|
|
807
|
+
'status.health.warning': '\u8b66\u544a',
|
|
808
|
+
'status.health.critical': '\u4e25\u91cd',
|
|
809
|
+
'pill.protected': '{n} 涓彈淇濇姢',
|
|
810
|
+
'pill.excluded': '{n} 涓帓闄?,
|
|
811
|
+
'pill.total': '{n} 涓€昏',
|
|
812
|
+
'tag.protect': '淇濇姢',
|
|
813
|
+
'tag.ignore': '蹇界暐',
|
|
814
|
+
'tag.more': '+{n} 涓洿澶?,
|
|
815
|
+
'actions.openDashboard': '鎵撳紑鐪嬫澘',
|
|
816
|
+
'actions.restore': '鎭㈠',
|
|
817
|
+
'actions.viewDetails': '鏌ョ湅璇︽儏',
|
|
818
|
+
'actions.snapshot': '绔嬪嵆蹇収',
|
|
819
|
+
'actions.watcherOn': '\u505c\u6b62 Watcher',
|
|
820
|
+
'actions.watcherOff': '\u542f\u52a8 Watcher',
|
|
821
|
+
'actions.doctor': '璇婃柇',
|
|
822
|
+
'brand.noWorkspace': '\u65e0\u5de5\u4f5c\u533a',
|
|
823
|
+
'brand.addConfig': '\u6dfb\u52a0 .cursor-guard.json',
|
|
824
|
+
'brand.loadingBackup': '\u5907\u4efd\u4fe1\u606f\u52a0\u8f7d\u4e2d...',
|
|
825
|
+
'brand.noGitBackup': '\u6682\u65e0 Git \u5907\u4efd',
|
|
826
|
+
'brand.backupPrefix': '\u4e0a\u6b21\u5907\u4efd',
|
|
827
|
+
'stats.never': '\u4ece\u672a',
|
|
828
|
+
'misc.unknown': '\u672a\u77e5',
|
|
829
|
+
'misc.na': 'N/A',
|
|
830
|
+
'time.secondsAgo': '{n} 绉掑墠',
|
|
831
|
+
'time.minutesAgo': '{m} 鍒?{s} 绉掑墠',
|
|
832
|
+
'time.hoursAgo': '{h} 灏忔椂 {m} 鍒嗗墠',
|
|
833
|
+
'time.daysAgo': '{d} 澶╁墠',
|
|
834
|
+
'time.seconds': '{n} 绉?,
|
|
835
|
+
'time.minutes': '{m} 鍒?{s} 绉?,
|
|
836
|
+
'alert.filesChangedFast': '{count} 涓枃浠跺揩閫熷彉鏇?
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
function t(key, params) {
|
|
841
|
+
const dict = I18N[_locale] || I18N['en-US'];
|
|
842
|
+
let value = dict[key] || I18N['en-US'][key] || key;
|
|
843
|
+
for (const [name, replacement] of Object.entries(params || {})) {
|
|
844
|
+
value = value.replaceAll('{' + name + '}', String(replacement));
|
|
845
|
+
}
|
|
846
|
+
return value;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function setLocale(locale, opts) {
|
|
850
|
+
_locale = locale === 'zh-CN' ? 'zh-CN' : 'en-US';
|
|
851
|
+
document.documentElement.lang = _locale === 'zh-CN' ? 'zh-CN' : 'en';
|
|
852
|
+
vscode.setState({ locale: _locale });
|
|
853
|
+
if (!opts || opts.syncHost !== false) {
|
|
854
|
+
vscode.postMessage({ cmd: 'setLocale', locale: _locale });
|
|
855
|
+
}
|
|
856
|
+
updateChrome();
|
|
857
|
+
if (!opts || opts.render !== false) {
|
|
858
|
+
render(_projects);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function toggleLocale() {
|
|
863
|
+
setLocale(_locale === 'zh-CN' ? 'en-US' : 'zh-CN');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function updateChrome() {
|
|
867
|
+
document.documentElement.lang = _locale === 'zh-CN' ? 'zh-CN' : 'en';
|
|
868
|
+
brandTitle.textContent = t('chrome.title');
|
|
869
|
+
langToggle.textContent = t('chrome.switch');
|
|
870
|
+
updateBrandBar(_projects);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function formatRelativeAge(ms) {
|
|
874
|
+
const sec = Math.floor((Date.now() - ms) / 1000);
|
|
875
|
+
if (sec < 60) return t('time.secondsAgo', { n: sec });
|
|
876
|
+
if (sec < 3600) return t('time.minutesAgo', { m: Math.floor(sec / 60), s: sec % 60 });
|
|
877
|
+
if (sec < 86400) return t('time.hoursAgo', { h: Math.floor(sec / 3600), m: Math.floor((sec % 3600) / 60) });
|
|
878
|
+
return t('time.daysAgo', { d: Math.floor(sec / 86400) });
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function formatCountdown(seconds) {
|
|
882
|
+
if (seconds > 60) return t('time.minutes', { m: Math.floor(seconds / 60), s: seconds % 60 });
|
|
883
|
+
return t('time.seconds', { n: seconds });
|
|
884
|
+
}
|
|
253
885
|
|
|
254
|
-
|
|
255
|
-
|
|
886
|
+
function displayCount(value) {
|
|
887
|
+
return value == null ? '?' : String(value);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function pickPrimaryProject(projects) {
|
|
891
|
+
const ids = Object.keys(projects || {});
|
|
892
|
+
for (let i = 0; i < ids.length; i++) {
|
|
893
|
+
const id = ids[i];
|
|
894
|
+
if (projects[id] && projects[id].dashboard) return { id, project: projects[id], ids };
|
|
895
|
+
}
|
|
896
|
+
if (ids.length) return { id: ids[0], project: projects[ids[0]], ids };
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function updateBrandBar(projects) {
|
|
901
|
+
const primary = pickPrimaryProject(projects || {});
|
|
902
|
+
|
|
903
|
+
if (!primary) {
|
|
904
|
+
brandProject.textContent = t('brand.noWorkspace');
|
|
905
|
+
brandProject.removeAttribute('title');
|
|
906
|
+
brandBackupPrefix.hidden = true;
|
|
907
|
+
brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
|
|
908
|
+
brandBackupAge.removeAttribute('data-backup-ts');
|
|
909
|
+
brandBackupAge.textContent = t('brand.addConfig');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const project = primary.project || {};
|
|
914
|
+
const dashboard = project.dashboard;
|
|
915
|
+
let name = project.name || primary.id;
|
|
916
|
+
if (primary.ids.length > 1) {
|
|
917
|
+
name += ' +' + (primary.ids.length - 1);
|
|
918
|
+
}
|
|
919
|
+
brandProject.textContent = name;
|
|
920
|
+
brandProject.title = name;
|
|
921
|
+
|
|
922
|
+
if (!dashboard) {
|
|
923
|
+
brandBackupPrefix.hidden = true;
|
|
924
|
+
brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
|
|
925
|
+
brandBackupAge.removeAttribute('data-backup-ts');
|
|
926
|
+
brandBackupAge.textContent = t('brand.loadingBackup');
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const gitTs = dashboard.lastBackup?.git?.timestamp;
|
|
931
|
+
if (gitTs) {
|
|
932
|
+
const ts = new Date(gitTs).getTime();
|
|
933
|
+
brandBackupPrefix.hidden = false;
|
|
934
|
+
brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
|
|
935
|
+
brandBackupAge.dataset.backupTs = String(ts);
|
|
936
|
+
brandBackupAge.textContent = formatRelativeAge(ts);
|
|
937
|
+
} else {
|
|
938
|
+
brandBackupPrefix.hidden = true;
|
|
939
|
+
brandBackupPrefix.textContent = t('brand.backupPrefix') + ' ';
|
|
940
|
+
brandBackupAge.removeAttribute('data-backup-ts');
|
|
941
|
+
brandBackupAge.textContent = t('brand.noGitBackup');
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
window.addEventListener('message', event => {
|
|
946
|
+
if (event.data.type === 'locale') {
|
|
947
|
+
setLocale(event.data.locale, { syncHost: false });
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
if (event.data.type === 'update') render(event.data.data);
|
|
256
951
|
});
|
|
952
|
+
|
|
953
|
+
langToggle.addEventListener('click', toggleLocale);
|
|
954
|
+
updateChrome();
|
|
955
|
+
root.innerHTML = '<div class="empty">' + t('state.waiting') + '</div>';
|
|
257
956
|
vscode.postMessage({ cmd: 'ready' });
|
|
258
957
|
|
|
259
958
|
setInterval(() => {
|
|
@@ -261,187 +960,234 @@ setInterval(() => {
|
|
|
261
960
|
const el = document.querySelector('.alert-countdown');
|
|
262
961
|
if (el) {
|
|
263
962
|
const remain = Math.max(0, Math.ceil((_alertExpiresAt - Date.now()) / 1000));
|
|
264
|
-
if (remain <= 0) {
|
|
265
|
-
|
|
963
|
+
if (remain <= 0) {
|
|
964
|
+
el.textContent = formatCountdown(0);
|
|
965
|
+
_alertExpiresAt = 0;
|
|
966
|
+
} else {
|
|
967
|
+
el.textContent = formatCountdown(remain);
|
|
968
|
+
}
|
|
266
969
|
}
|
|
267
970
|
}
|
|
268
|
-
|
|
269
|
-
|
|
971
|
+
|
|
972
|
+
document.querySelectorAll('.backup-age[data-backup-ts]').forEach(ageEl => {
|
|
270
973
|
const ts = parseInt(ageEl.dataset.backupTs, 10);
|
|
271
|
-
if (ts)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
else if (sec < 3600) ageEl.textContent = Math.floor(sec / 60) + 'm ' + (sec % 60) + 's ago';
|
|
275
|
-
else if (sec < 86400) ageEl.textContent = Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm ago';
|
|
276
|
-
else ageEl.textContent = Math.floor(sec / 86400) + 'd ago';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
974
|
+
if (!ts) return;
|
|
975
|
+
ageEl.textContent = formatRelativeAge(ts);
|
|
976
|
+
});
|
|
279
977
|
}, 1000);
|
|
280
978
|
|
|
281
979
|
function render(projects) {
|
|
282
|
-
|
|
980
|
+
_projects = projects || {};
|
|
981
|
+
const ids = Object.keys(_projects);
|
|
283
982
|
if (ids.length === 0) {
|
|
284
|
-
root.innerHTML = '<div class="empty
|
|
983
|
+
root.innerHTML = '<div class="empty">' + t('state.empty') + '</div>';
|
|
984
|
+
updateBrandBar(_projects);
|
|
285
985
|
return;
|
|
286
986
|
}
|
|
987
|
+
|
|
287
988
|
let html = '';
|
|
288
989
|
for (const id of ids) {
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
if (!
|
|
292
|
-
|
|
990
|
+
const project = _projects[id];
|
|
991
|
+
const dashboard = project.dashboard;
|
|
992
|
+
if (!dashboard) {
|
|
993
|
+
html += '<div class="empty">' + esc(t('state.loading')) + '</div>';
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
html += renderProject(dashboard);
|
|
293
997
|
}
|
|
294
|
-
html += renderActions(
|
|
998
|
+
html += renderActions(_projects);
|
|
295
999
|
root.innerHTML = html;
|
|
1000
|
+
updateBrandBar(_projects);
|
|
296
1001
|
|
|
297
1002
|
const alertCard = root.querySelector('.alert-card[data-expires]');
|
|
298
1003
|
_alertExpiresAt = alertCard ? parseInt(alertCard.dataset.expires, 10) || 0 : 0;
|
|
299
1004
|
|
|
300
1005
|
root.querySelectorAll('[data-cmd]').forEach(btn => {
|
|
301
|
-
btn.addEventListener('click', () =>
|
|
1006
|
+
btn.addEventListener('click', () => {
|
|
1007
|
+
vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd });
|
|
1008
|
+
});
|
|
302
1009
|
});
|
|
303
1010
|
}
|
|
304
1011
|
|
|
305
|
-
function renderProject(
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
} else if (
|
|
321
|
-
|
|
322
|
-
h += '<span class="status-icon">🟡</span>';
|
|
323
|
-
h += '<span class="status-text">Watcher Stopped</span>';
|
|
324
|
-
h += '<span class="status-sub">Start watcher to enable protection</span>';
|
|
325
|
-
h += '</div>';
|
|
326
|
-
} else if (isCritical) {
|
|
327
|
-
h += '<div class="status-hero critical">';
|
|
328
|
-
h += '<span class="status-icon">🔴</span>';
|
|
329
|
-
h += '<span class="status-text">Critical Issue</span>';
|
|
330
|
-
h += '<span class="status-sub">' + esc(d.health.issues?.[0] || 'Check diagnostics') + '</span>';
|
|
331
|
-
h += '</div>';
|
|
1012
|
+
function renderProject(dashboard) {
|
|
1013
|
+
const watcherRunning = dashboard.watcher?.running;
|
|
1014
|
+
const latestPreWarning = dashboard.preWarnings?.active ? dashboard.preWarnings.latest : null;
|
|
1015
|
+
const preWarning = latestPreWarning?.mode === 'dashboard' ? latestPreWarning : null;
|
|
1016
|
+
const alert = dashboard.alerts?.active ? dashboard.alerts.latest : null;
|
|
1017
|
+
const health = dashboard.health?.status || 'unknown';
|
|
1018
|
+
const critical = health === 'critical';
|
|
1019
|
+
let html = '';
|
|
1020
|
+
|
|
1021
|
+
if (preWarning) {
|
|
1022
|
+
html += hero('risk', t('hero.pre.kicker'), t('hero.pre.title'), preWarning.summary || t('hero.pre.subtitle'));
|
|
1023
|
+
} else if (alert) {
|
|
1024
|
+
html += hero('alert', t('hero.alert.kicker'), t('alert.filesChangedFast', { count: displayCount(alert.fileCount) }), t('hero.alert.subtitle'));
|
|
1025
|
+
} else if (!watcherRunning) {
|
|
1026
|
+
html += hero('stopped', t('hero.protection.kicker'), t('hero.protection.stopped'), t('hero.protection.stoppedSub'));
|
|
1027
|
+
} else if (critical) {
|
|
1028
|
+
html += hero('critical', t('hero.health.kicker'), t('hero.health.critical'), dashboard.health.issues?.[0] || t('hero.health.check'));
|
|
332
1029
|
} else {
|
|
333
|
-
|
|
334
|
-
h += '<span class="status-icon">🟢</span>';
|
|
335
|
-
h += '<span class="status-text">Protected</span>';
|
|
336
|
-
h += '<span class="status-sub">Watcher running · All systems OK</span>';
|
|
337
|
-
h += '</div>';
|
|
1030
|
+
html += hero('protected', t('hero.protection.kicker'), t('hero.protection.safe'), t('hero.protection.safeSub'), { live: true });
|
|
338
1031
|
}
|
|
339
1032
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
1033
|
+
if (preWarning) {
|
|
1034
|
+
html += '<div class="card risk-card">';
|
|
1035
|
+
html += '<div class="card-title">' + esc(t('card.deletionRisk')) + '</div>';
|
|
1036
|
+
html += row(t('row.file'), esc(preWarning.file || 'Unknown'), 'orange');
|
|
1037
|
+
html += row(t('row.risk'), esc(String(preWarning.riskPercent || '?')) + '%', 'orange');
|
|
1038
|
+
if (preWarning.removedMethodCount) {
|
|
1039
|
+
html += row(t('row.methodsRemoved'), esc(String(preWarning.removedMethodCount)), 'red');
|
|
1040
|
+
}
|
|
1041
|
+
html += row(t('row.summary'), esc(preWarning.summary || t('hero.pre.subtitle')), 'orange');
|
|
1042
|
+
html += '<div class="actions">';
|
|
1043
|
+
html += '<button class="btn" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.openDashboard')) + '</button>';
|
|
1044
|
+
html += '<button class="btn" data-cmd="cursorGuard.quickRestore">' + esc(t('actions.restore')) + '</button>';
|
|
1045
|
+
html += '</div>';
|
|
1046
|
+
html += '</div>';
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (alert) {
|
|
1050
|
+
const expiresTs = alert.expiresAt ? new Date(alert.expiresAt).getTime() : 0;
|
|
344
1051
|
const remain = expiresTs ? Math.max(0, Math.ceil((expiresTs - Date.now()) / 1000)) : 0;
|
|
345
|
-
const display =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1052
|
+
const display = formatCountdown(remain);
|
|
1053
|
+
html += '<div class="card alert-card" data-expires="' + expiresTs + '">';
|
|
1054
|
+
html += '<div class="card-title">' + esc(t('card.activeAlert')) + '</div>';
|
|
1055
|
+
html += row(t('row.window'), (alert.windowSeconds || '?') + 's', 'red');
|
|
1056
|
+
html += row(t('row.files'), String(alert.fileCount || '?'), 'red');
|
|
1057
|
+
html += row(t('row.threshold'), String(alert.threshold || '?'), 'yellow');
|
|
1058
|
+
html += row(t('row.expires'), '<span class="alert-countdown">' + esc(display) + '</span>', 'yellow', true);
|
|
1059
|
+
html += '<div class="actions">';
|
|
1060
|
+
html += '<button class="btn" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.viewDetails')) + '</button>';
|
|
1061
|
+
html += '</div>';
|
|
1062
|
+
html += '</div>';
|
|
353
1063
|
}
|
|
354
1064
|
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const freeGB = d.disk?.freeGB;
|
|
1065
|
+
const gitCount = dashboard.counts?.git?.commits || 0;
|
|
1066
|
+
const shadowCount = dashboard.counts?.shadow?.snapshots || 0;
|
|
1067
|
+
const lastGitTs = dashboard.lastBackup?.git?.timestamp || '';
|
|
1068
|
+
const lastGit = dashboard.lastBackup?.git?.relativeTime || t('stats.never');
|
|
1069
|
+
const freeGB = dashboard.disk?.freeGB;
|
|
361
1070
|
const freeDisplay = typeof freeGB === 'number' ? freeGB.toFixed(1) + ' GB' : 'N/A';
|
|
362
|
-
const diskWarn =
|
|
1071
|
+
const diskWarn = dashboard.disk?.warning;
|
|
1072
|
+
const watcherInfo = watcherStateInfo(dashboard);
|
|
1073
|
+
const healthInfo = healthStateInfo(dashboard);
|
|
363
1074
|
|
|
364
|
-
|
|
365
|
-
|
|
1075
|
+
html += '<div class="card">';
|
|
1076
|
+
html += '<div class="card-title">' + esc(t('card.quickStats')) + '</div>';
|
|
1077
|
+
html += row(t('row.watcher'), watcherInfo.label, watcherInfo.tone);
|
|
1078
|
+
html += row(t('row.health'), healthInfo.label, healthInfo.tone);
|
|
366
1079
|
if (lastGitTs) {
|
|
367
|
-
|
|
1080
|
+
html += '<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>';
|
|
368
1081
|
} else {
|
|
369
|
-
|
|
1082
|
+
html += row(t('row.lastBackup'), lastGit, 'green');
|
|
1083
|
+
}
|
|
1084
|
+
html += row(t('row.gitBackups'), String(gitCount), 'blue');
|
|
1085
|
+
if (shadowCount > 0) html += row(t('row.shadowCopies'), String(shadowCount), 'blue');
|
|
1086
|
+
html += row(t('row.diskFree'), freeDisplay, diskWarn ? 'yellow' : 'green');
|
|
1087
|
+
html += '</div>';
|
|
1088
|
+
|
|
1089
|
+
const scope = dashboard.protectionScope || {};
|
|
1090
|
+
const protect = scope.protect || [];
|
|
1091
|
+
const ignore = scope.ignore || [];
|
|
1092
|
+
|
|
1093
|
+
html += '<div class="card">';
|
|
1094
|
+
html += '<div class="card-title">' + esc(t('card.protectionScope')) + '</div>';
|
|
1095
|
+
html += '<div class="pill-wrap">';
|
|
1096
|
+
html += '<span class="pill green">' + esc(t('pill.protected', { n: String(scope.fileCount || 0) })) + '</span>';
|
|
1097
|
+
if ((scope.excludedCount || 0) > 0) {
|
|
1098
|
+
html += '<span class="pill red">' + esc(t('pill.excluded', { n: String(scope.excludedCount || 0) })) + '</span>';
|
|
370
1099
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// ── Protection Scope ──
|
|
377
|
-
const scope = d.protectionScope || {};
|
|
378
|
-
const pCount = scope.fileCount || 0;
|
|
379
|
-
const exCount = scope.excludedCount || 0;
|
|
380
|
-
const totalF = scope.totalFiles || 0;
|
|
381
|
-
const protectPats = scope.protect || [];
|
|
382
|
-
const ignorePats = scope.ignore || [];
|
|
383
|
-
|
|
384
|
-
h += '<div class="stats-card">';
|
|
385
|
-
h += '<div class="label-sm">Protection Scope</div>';
|
|
386
|
-
h += '<div class="scope-summary">';
|
|
387
|
-
h += '<span class="scope-chip protected">\u{1f6e1}\ufe0f ' + pCount + ' protected</span>';
|
|
388
|
-
if (exCount > 0) h += '<span class="scope-chip excluded">\u{1f6ab} ' + exCount + ' excluded</span>';
|
|
389
|
-
h += '<span class="scope-chip total">' + totalF + ' total</span>';
|
|
390
|
-
h += '</div>';
|
|
391
|
-
|
|
392
|
-
if (protectPats.length > 0) {
|
|
393
|
-
h += '<div class="scope-block">';
|
|
394
|
-
h += '<span class="scope-label green">Protect (' + protectPats.length + ')</span>';
|
|
395
|
-
h += '<div class="scope-tags">';
|
|
396
|
-
const showP = protectPats.slice(0, 6);
|
|
397
|
-
for (const p of showP) h += '<span class="scope-tag green">' + esc(p) + '</span>';
|
|
398
|
-
if (protectPats.length > 6) h += '<span class="scope-tag dim">+' + (protectPats.length - 6) + ' more</span>';
|
|
399
|
-
h += '</div></div>';
|
|
1100
|
+
html += '<span class="pill dim">' + esc(t('pill.total', { n: String(scope.totalFiles || 0) })) + '</span>';
|
|
1101
|
+
html += '</div>';
|
|
1102
|
+
|
|
1103
|
+
if (protect.length > 0) {
|
|
1104
|
+
html += renderTags(t('tag.protect'), protect, 'green');
|
|
400
1105
|
}
|
|
1106
|
+
if (ignore.length > 0) {
|
|
1107
|
+
html += renderTags(t('tag.ignore'), ignore, 'red');
|
|
1108
|
+
}
|
|
1109
|
+
html += '</div>';
|
|
401
1110
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1111
|
+
return html;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function renderTags(label, values, tone) {
|
|
1115
|
+
let html = '<div class="tag-group">';
|
|
1116
|
+
html += '<div class="tag-label">' + esc(label) + ' (' + values.length + ')</div>';
|
|
1117
|
+
html += '<div class="tag-list">';
|
|
1118
|
+
const shown = values.slice(0, 6);
|
|
1119
|
+
for (const value of shown) {
|
|
1120
|
+
html += '<span class="tag ' + tone + '">' + esc(value) + '</span>';
|
|
1121
|
+
}
|
|
1122
|
+
if (values.length > 6) {
|
|
1123
|
+
html += '<span class="tag dim">' + esc(t('tag.more', { n: values.length - 6 })) + '</span>';
|
|
410
1124
|
}
|
|
1125
|
+
html += '</div></div>';
|
|
1126
|
+
return html;
|
|
1127
|
+
}
|
|
411
1128
|
|
|
412
|
-
|
|
1129
|
+
function watcherStateInfo(dashboard) {
|
|
1130
|
+
const watcher = dashboard?.watcher || {};
|
|
1131
|
+
if (watcher.running) return { label: t('status.watcher.running'), tone: 'green' };
|
|
1132
|
+
if (watcher.stale) return { label: t('status.watcher.stale'), tone: 'yellow' };
|
|
1133
|
+
return { label: t('status.watcher.stopped'), tone: 'red' };
|
|
1134
|
+
}
|
|
413
1135
|
|
|
414
|
-
|
|
1136
|
+
function healthStateInfo(dashboard) {
|
|
1137
|
+
const health = dashboard?.health?.status || 'warning';
|
|
1138
|
+
if (health === 'critical') return { label: t('status.health.critical'), tone: 'red' };
|
|
1139
|
+
if (health === 'healthy') return { label: t('status.health.healthy'), tone: 'green' };
|
|
1140
|
+
return { label: t('status.health.warning'), tone: 'yellow' };
|
|
415
1141
|
}
|
|
416
1142
|
|
|
417
1143
|
function renderActions(projects) {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
const
|
|
1144
|
+
const primary = pickPrimaryProject(projects || {});
|
|
1145
|
+
const dashboard = primary?.project?.dashboard || null;
|
|
1146
|
+
const watcherRunning = dashboard?.watcher?.running;
|
|
421
1147
|
|
|
422
|
-
let
|
|
423
|
-
|
|
424
|
-
|
|
1148
|
+
let html = '<div class="cg-actions-wrap"><div class="actions">';
|
|
1149
|
+
html += '<button class="btn primary" data-cmd="cursorGuard.snapshotNow">' + esc(t('actions.snapshot')) + '</button>';
|
|
1150
|
+
html += '<button class="btn" data-cmd="cursorGuard.quickRestore">' + esc(t('actions.restore')) + '</button>';
|
|
1151
|
+
html += watcherRunning
|
|
1152
|
+
? '<button class="btn" data-cmd="cursorGuard.stopWatcher">' + esc(t('actions.watcherOn')) + '</button>'
|
|
1153
|
+
: '<button class="btn" data-cmd="cursorGuard.startWatcher">' + esc(t('actions.watcherOff')) + '</button>';
|
|
1154
|
+
html += '<button class="btn" data-cmd="cursorGuard.doctor">' + esc(t('actions.doctor')) + '</button>';
|
|
1155
|
+
html += '<button class="btn primary full" data-cmd="cursorGuard.openDashboard">' + esc(t('actions.openDashboard')) + '</button>';
|
|
1156
|
+
html += '</div></div>';
|
|
1157
|
+
return html;
|
|
1158
|
+
}
|
|
425
1159
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1160
|
+
function hero(tone, kicker, title, subtitle, opts) {
|
|
1161
|
+
const pulse = opts && opts.live ? '<span class="cg-pulse-dot" title="Watcher running"></span>' : '';
|
|
1162
|
+
let html = '<div class="hero ' + tone + '">';
|
|
1163
|
+
html += '<div class="hero-top">';
|
|
1164
|
+
html += '<div class="hero-kicker">' + esc(kicker) + '</div>';
|
|
1165
|
+
html += pulse;
|
|
1166
|
+
html += '</div>';
|
|
1167
|
+
html += '<div class="hero-title">' + esc(title) + '</div>';
|
|
1168
|
+
html += '<div class="hero-sub">' + esc(subtitle) + '</div>';
|
|
1169
|
+
html += '</div>';
|
|
1170
|
+
return html;
|
|
1171
|
+
}
|
|
432
1172
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return h;
|
|
1173
|
+
function row(name, value, tone, rawValue) {
|
|
1174
|
+
return '<div class="row"><span class="row-name">' + esc(name) + '</span><span class="row-value ' + tone + '">' + (rawValue ? value : esc(String(value))) + '</span></div>';
|
|
436
1175
|
}
|
|
437
1176
|
|
|
438
|
-
function
|
|
439
|
-
return
|
|
1177
|
+
function esc(value) {
|
|
1178
|
+
return String(value)
|
|
1179
|
+
.replace(/&/g, '&')
|
|
1180
|
+
.replace(/</g, '<')
|
|
1181
|
+
.replace(/>/g, '>')
|
|
1182
|
+
.replace(/"/g, '"');
|
|
440
1183
|
}
|
|
441
|
-
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
442
1184
|
</script>
|
|
443
1185
|
</body>
|
|
444
1186
|
</html>`;
|
|
445
1187
|
}
|
|
446
1188
|
|
|
447
1189
|
module.exports = { SidebarDashboardProvider };
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
|