cursor-guard 4.9.8 → 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.
- package/README.md +10 -1
- package/README.zh-CN.md +10 -1
- package/ROADMAP.md +11 -4
- package/docs/RELEASE.md +196 -0
- package/package.json +2 -1
- package/references/dashboard/public/app.js +79 -79
- package/references/dashboard/public/style.css +264 -159
- package/references/lib/core/core.test.js +139 -101
- package/references/lib/core/snapshot.js +8 -4
- package/references/mcp/server.js +73 -72
- package/references/vscode-extension/dist/{cursor-guard-ide-4.9.8.vsix → cursor-guard-ide-4.9.9.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +79 -79
- package/references/vscode-extension/dist/dashboard/public/style.css +264 -159
- package/references/vscode-extension/dist/extension.js +9 -2
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/core/snapshot.js +8 -4
- package/references/vscode-extension/dist/lib/dashboard-manager.js +110 -103
- package/references/vscode-extension/dist/lib/sidebar-webview.js +447 -156
- package/references/vscode-extension/dist/mcp/server.js +9 -6
- package/references/vscode-extension/dist/package.json +1 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +11 -4
- package/references/vscode-extension/extension.js +9 -2
- package/references/vscode-extension/lib/dashboard-manager.js +110 -103
- package/references/vscode-extension/lib/sidebar-webview.js +447 -156
- package/references/vscode-extension/package.json +1 -1
|
@@ -57,7 +57,9 @@ class SidebarDashboardProvider {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
_push(data) {
|
|
60
|
-
if (!this._view
|
|
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...".
|
|
61
63
|
|
|
62
64
|
const payload = {};
|
|
63
65
|
for (const [id, project] of data) {
|
|
@@ -107,8 +109,10 @@ function _getHtml(brandInnerHtml) {
|
|
|
107
109
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
|
|
108
110
|
--shadow-soft: 0 4px 14px rgba(0, 0, 0, 0.12);
|
|
109
111
|
--accent: var(--blue);
|
|
110
|
-
--glow-green: color-mix(in srgb, var(--green)
|
|
111
|
-
|
|
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);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
* { box-sizing: border-box; }
|
|
@@ -123,11 +127,198 @@ body {
|
|
|
123
127
|
|
|
124
128
|
.cg-shell {
|
|
125
129
|
position: relative;
|
|
126
|
-
padding:
|
|
130
|
+
padding: 8px 8px 14px;
|
|
127
131
|
min-height: 100%;
|
|
128
132
|
background:
|
|
129
|
-
radial-gradient(
|
|
130
|
-
radial-gradient(
|
|
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;
|
|
131
322
|
}
|
|
132
323
|
|
|
133
324
|
.cg-brand {
|
|
@@ -348,6 +539,8 @@ body {
|
|
|
348
539
|
.card-head { transition: none; }
|
|
349
540
|
.btn { transition: none; }
|
|
350
541
|
.lang-btn { transition: none; }
|
|
542
|
+
.cg-main-fold-chevron { transition: none; }
|
|
543
|
+
.cg-section-fold .cg-main-fold-chevron { transition: none; }
|
|
351
544
|
}
|
|
352
545
|
|
|
353
546
|
.hero.risk {
|
|
@@ -553,20 +746,48 @@ body {
|
|
|
553
746
|
border: 1px solid transparent;
|
|
554
747
|
}
|
|
555
748
|
|
|
556
|
-
.pill.green {
|
|
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
|
+
}
|
|
557
759
|
.pill.red { background: rgba(242, 159, 159, 0.12); color: var(--red); }
|
|
558
760
|
.pill.orange { background: rgba(244, 179, 110, 0.12); color: var(--orange); }
|
|
559
|
-
.pill.dim { background: rgba(154, 164, 189, 0.
|
|
560
|
-
|
|
561
|
-
.tag-group { margin-top:
|
|
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
|
+
}
|
|
562
781
|
.tag-label {
|
|
563
|
-
margin-bottom:
|
|
782
|
+
margin-bottom: 6px;
|
|
564
783
|
font-size: 10px;
|
|
565
784
|
font-weight: 700;
|
|
566
|
-
letter-spacing: 0.
|
|
785
|
+
letter-spacing: 0.08em;
|
|
567
786
|
text-transform: uppercase;
|
|
568
787
|
color: var(--muted);
|
|
569
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)); }
|
|
570
791
|
|
|
571
792
|
.tag-list {
|
|
572
793
|
display: flex;
|
|
@@ -586,9 +807,16 @@ body {
|
|
|
586
807
|
}
|
|
587
808
|
|
|
588
809
|
.tag.green {
|
|
589
|
-
color: var(--green);
|
|
590
|
-
border-color:
|
|
591
|
-
background:
|
|
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));
|
|
592
820
|
}
|
|
593
821
|
|
|
594
822
|
.tag.red {
|
|
@@ -599,6 +827,8 @@ body {
|
|
|
599
827
|
|
|
600
828
|
.tag.dim {
|
|
601
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);
|
|
602
832
|
}
|
|
603
833
|
|
|
604
834
|
.cg-actions-wrap {
|
|
@@ -658,23 +888,27 @@ body {
|
|
|
658
888
|
</head>
|
|
659
889
|
<body>
|
|
660
890
|
<div class="cg-shell">
|
|
661
|
-
<
|
|
662
|
-
<div class="
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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>
|
|
669
904
|
</div>
|
|
670
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>
|
|
671
911
|
</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
912
|
</div>
|
|
679
913
|
</div>
|
|
680
914
|
<script>
|
|
@@ -694,6 +928,8 @@ const I18N = {
|
|
|
694
928
|
'en-US': {
|
|
695
929
|
'chrome.title': 'Cursor Guard',
|
|
696
930
|
'chrome.switch': '\u4e2d\u6587',
|
|
931
|
+
'section.status': 'Status',
|
|
932
|
+
'section.actions': 'Actions',
|
|
697
933
|
'state.waiting': 'Waiting for data...',
|
|
698
934
|
'state.loading': 'Loading...',
|
|
699
935
|
'state.empty': 'No projects detected.<br>Add .cursor-guard.json to get started.',
|
|
@@ -766,74 +1002,76 @@ const I18N = {
|
|
|
766
1002
|
'zh-CN': {
|
|
767
1003
|
'chrome.title': 'Cursor Guard',
|
|
768
1004
|
'chrome.switch': 'EN',
|
|
769
|
-
'
|
|
770
|
-
'
|
|
771
|
-
'state.
|
|
772
|
-
'
|
|
773
|
-
'
|
|
774
|
-
'
|
|
775
|
-
'
|
|
776
|
-
'
|
|
777
|
-
'
|
|
778
|
-
'
|
|
779
|
-
'hero.
|
|
780
|
-
'hero.
|
|
781
|
-
'hero.
|
|
782
|
-
'hero.
|
|
783
|
-
'hero.
|
|
784
|
-
'hero.protection.
|
|
785
|
-
'
|
|
786
|
-
'
|
|
787
|
-
'
|
|
788
|
-
'
|
|
789
|
-
'
|
|
790
|
-
'
|
|
791
|
-
'
|
|
792
|
-
'
|
|
793
|
-
'
|
|
794
|
-
'
|
|
795
|
-
'
|
|
796
|
-
'row.
|
|
797
|
-
'row.
|
|
798
|
-
'row.
|
|
799
|
-
'row.
|
|
800
|
-
'row.
|
|
801
|
-
'row.
|
|
802
|
-
'row.
|
|
803
|
-
'
|
|
804
|
-
'
|
|
805
|
-
'
|
|
806
|
-
'
|
|
807
|
-
'
|
|
808
|
-
'
|
|
809
|
-
'
|
|
810
|
-
'
|
|
811
|
-
'
|
|
812
|
-
'
|
|
813
|
-
'
|
|
814
|
-
'
|
|
815
|
-
'
|
|
816
|
-
'
|
|
817
|
-
'
|
|
818
|
-
'
|
|
819
|
-
'
|
|
820
|
-
'
|
|
821
|
-
'
|
|
822
|
-
'
|
|
823
|
-
'
|
|
824
|
-
'
|
|
825
|
-
'
|
|
826
|
-
'
|
|
827
|
-
'
|
|
828
|
-
'
|
|
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': '未知',
|
|
829
1067
|
'misc.na': 'N/A',
|
|
830
|
-
'time.secondsAgo': '{n}
|
|
831
|
-
'time.minutesAgo': '{m}
|
|
832
|
-
'time.hoursAgo': '{h}
|
|
833
|
-
'time.daysAgo': '{d}
|
|
834
|
-
'time.seconds': '{n}
|
|
835
|
-
'time.minutes': '{m}
|
|
836
|
-
'alert.filesChangedFast': '{count}
|
|
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} 个文件快速变更'
|
|
837
1075
|
}
|
|
838
1076
|
};
|
|
839
1077
|
|
|
@@ -870,6 +1108,50 @@ function updateChrome() {
|
|
|
870
1108
|
updateBrandBar(_projects);
|
|
871
1109
|
}
|
|
872
1110
|
|
|
1111
|
+
function escAttr(value) {
|
|
1112
|
+
return String(value)
|
|
1113
|
+
.replace(/&/g, '&')
|
|
1114
|
+
.replace(/"/g, '"');
|
|
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">▾</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
|
+
|
|
873
1155
|
function formatRelativeAge(ms) {
|
|
874
1156
|
const sec = Math.floor((Date.now() - ms) / 1000);
|
|
875
1157
|
if (sec < 60) return t('time.secondsAgo', { n: sec });
|
|
@@ -993,11 +1275,12 @@ function render(projects) {
|
|
|
993
1275
|
html += '<div class="empty">' + esc(t('state.loading')) + '</div>';
|
|
994
1276
|
continue;
|
|
995
1277
|
}
|
|
996
|
-
html += renderProject(dashboard);
|
|
1278
|
+
html += renderProject(dashboard, id);
|
|
997
1279
|
}
|
|
998
|
-
html += renderActions(_projects);
|
|
1280
|
+
html += renderActions(_projects, ids[0]);
|
|
999
1281
|
root.innerHTML = html;
|
|
1000
1282
|
updateBrandBar(_projects);
|
|
1283
|
+
bindSectionFolds(root);
|
|
1001
1284
|
|
|
1002
1285
|
const alertCard = root.querySelector('.alert-card[data-expires]');
|
|
1003
1286
|
_alertExpiresAt = alertCard ? parseInt(alertCard.dataset.expires, 10) || 0 : 0;
|
|
@@ -1009,7 +1292,7 @@ function render(projects) {
|
|
|
1009
1292
|
});
|
|
1010
1293
|
}
|
|
1011
1294
|
|
|
1012
|
-
function renderProject(dashboard) {
|
|
1295
|
+
function renderProject(dashboard, projectId) {
|
|
1013
1296
|
const watcherRunning = dashboard.watcher?.running;
|
|
1014
1297
|
const latestPreWarning = dashboard.preWarnings?.active ? dashboard.preWarnings.latest : null;
|
|
1015
1298
|
const preWarning = latestPreWarning?.mode === 'dashboard' ? latestPreWarning : null;
|
|
@@ -1018,48 +1301,52 @@ function renderProject(dashboard) {
|
|
|
1018
1301
|
const critical = health === 'critical';
|
|
1019
1302
|
let html = '';
|
|
1020
1303
|
|
|
1304
|
+
let heroHtml = '';
|
|
1021
1305
|
if (preWarning) {
|
|
1022
|
-
|
|
1306
|
+
heroHtml = hero('risk', t('hero.pre.kicker'), t('hero.pre.title'), preWarning.summary || t('hero.pre.subtitle'));
|
|
1023
1307
|
} else if (alert) {
|
|
1024
|
-
|
|
1308
|
+
heroHtml = hero('alert', t('hero.alert.kicker'), t('alert.filesChangedFast', { count: displayCount(alert.fileCount) }), t('hero.alert.subtitle'));
|
|
1025
1309
|
} else if (!watcherRunning) {
|
|
1026
|
-
|
|
1310
|
+
heroHtml = hero('stopped', t('hero.protection.kicker'), t('hero.protection.stopped'), t('hero.protection.stoppedSub'));
|
|
1027
1311
|
} else if (critical) {
|
|
1028
|
-
|
|
1312
|
+
heroHtml = hero('critical', t('hero.health.kicker'), t('hero.health.critical'), dashboard.health.issues?.[0] || t('hero.health.check'));
|
|
1029
1313
|
} else {
|
|
1030
|
-
|
|
1314
|
+
heroHtml = hero('protected', t('hero.protection.kicker'), t('hero.protection.safe'), t('hero.protection.safeSub'), { live: true });
|
|
1031
1315
|
}
|
|
1316
|
+
html += wrapSection(projectId, 'status', t('section.status'), heroHtml, '');
|
|
1032
1317
|
|
|
1033
1318
|
if (preWarning) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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');
|
|
1038
1323
|
if (preWarning.removedMethodCount) {
|
|
1039
|
-
|
|
1324
|
+
inner += row(t('row.methodsRemoved'), esc(String(preWarning.removedMethodCount)), 'red');
|
|
1040
1325
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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, '');
|
|
1047
1333
|
}
|
|
1048
1334
|
|
|
1049
1335
|
if (alert) {
|
|
1050
1336
|
const expiresTs = alert.expiresAt ? new Date(alert.expiresAt).getTime() : 0;
|
|
1051
1337
|
const remain = expiresTs ? Math.max(0, Math.ceil((expiresTs - Date.now()) / 1000)) : 0;
|
|
1052
1338
|
const display = formatCountdown(remain);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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, '');
|
|
1063
1350
|
}
|
|
1064
1351
|
|
|
1065
1352
|
const gitCount = dashboard.counts?.git?.commits || 0;
|
|
@@ -1072,47 +1359,50 @@ function renderProject(dashboard) {
|
|
|
1072
1359
|
const watcherInfo = watcherStateInfo(dashboard);
|
|
1073
1360
|
const healthInfo = healthStateInfo(dashboard);
|
|
1074
1361
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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);
|
|
1079
1366
|
if (lastGitTs) {
|
|
1080
|
-
|
|
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>';
|
|
1081
1368
|
} else {
|
|
1082
|
-
|
|
1369
|
+
statsInner += row(t('row.lastBackup'), lastGit, 'green');
|
|
1083
1370
|
}
|
|
1084
|
-
|
|
1085
|
-
if (shadowCount > 0)
|
|
1086
|
-
|
|
1087
|
-
|
|
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, '');
|
|
1088
1376
|
|
|
1089
1377
|
const scope = dashboard.protectionScope || {};
|
|
1090
1378
|
const protect = scope.protect || [];
|
|
1091
1379
|
const ignore = scope.ignore || [];
|
|
1092
1380
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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>';
|
|
1097
1385
|
if ((scope.excludedCount || 0) > 0) {
|
|
1098
|
-
|
|
1386
|
+
scopeInner += '<span class="pill ignore">' + esc(t('pill.excluded', { n: String(scope.excludedCount || 0) })) + '</span>';
|
|
1099
1387
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1388
|
+
scopeInner += '<span class="pill dim">' + esc(t('pill.total', { n: String(scope.totalFiles || 0) })) + '</span>';
|
|
1389
|
+
scopeInner += '</div>';
|
|
1102
1390
|
|
|
1103
1391
|
if (protect.length > 0) {
|
|
1104
|
-
|
|
1392
|
+
scopeInner += renderTags(t('tag.protect'), protect, 'green', 'tag-group--protect');
|
|
1105
1393
|
}
|
|
1106
1394
|
if (ignore.length > 0) {
|
|
1107
|
-
|
|
1395
|
+
scopeInner += renderTags(t('tag.ignore'), ignore, 'ignore', 'tag-group--ignore');
|
|
1108
1396
|
}
|
|
1109
|
-
|
|
1397
|
+
scopeInner += '</div>';
|
|
1398
|
+
html += wrapSection(projectId, 'scope', t('card.protectionScope'), scopeInner, '');
|
|
1110
1399
|
|
|
1111
1400
|
return html;
|
|
1112
1401
|
}
|
|
1113
1402
|
|
|
1114
|
-
function renderTags(label, values, tone) {
|
|
1115
|
-
|
|
1403
|
+
function renderTags(label, values, tone, groupClass) {
|
|
1404
|
+
const gc = groupClass ? ' ' + groupClass : '';
|
|
1405
|
+
let html = '<div class="tag-group' + gc + '">';
|
|
1116
1406
|
html += '<div class="tag-label">' + esc(label) + ' (' + values.length + ')</div>';
|
|
1117
1407
|
html += '<div class="tag-list">';
|
|
1118
1408
|
const shown = values.slice(0, 6);
|
|
@@ -1140,21 +1430,22 @@ function healthStateInfo(dashboard) {
|
|
|
1140
1430
|
return { label: t('status.health.warning'), tone: 'yellow' };
|
|
1141
1431
|
}
|
|
1142
1432
|
|
|
1143
|
-
function renderActions(projects) {
|
|
1433
|
+
function renderActions(projects, primaryProjectId) {
|
|
1144
1434
|
const primary = pickPrimaryProject(projects || {});
|
|
1145
1435
|
const dashboard = primary?.project?.dashboard || null;
|
|
1146
1436
|
const watcherRunning = dashboard?.watcher?.running;
|
|
1437
|
+
const pid = primaryProjectId || primary?.id || 'default';
|
|
1147
1438
|
|
|
1148
|
-
let
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
|
1152
1443
|
? '<button class="btn" data-cmd="cursorGuard.stopWatcher">' + esc(t('actions.watcherOn')) + '</button>'
|
|
1153
1444
|
: '<button class="btn" data-cmd="cursorGuard.startWatcher">' + esc(t('actions.watcherOff')) + '</button>';
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
return
|
|
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, '');
|
|
1158
1449
|
}
|
|
1159
1450
|
|
|
1160
1451
|
function hero(tone, kicker, title, subtitle, opts) {
|