cursor-guard 4.9.12 → 4.9.15
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 +697 -697
- package/README.zh-CN.md +696 -696
- package/ROADMAP.md +1775 -1758
- package/SKILL.md +631 -629
- package/docs/RELEASE.md +197 -196
- package/docs/SNAPSHOT-BOOKMARK.md +47 -0
- package/package.json +2 -1
- package/references/dashboard/public/app.js +2079 -2050
- package/references/dashboard/public/style.css +1660 -1628
- package/references/lib/core/backups.js +509 -507
- package/references/lib/core/core.test.js +39 -1
- package/references/lib/core/snapshot.js +441 -416
- package/references/mcp/mcp.test.js +381 -362
- package/references/mcp/server.js +404 -347
- package/references/vscode-extension/{cursor-guard-ide-4.9.12.vsix → dist/cursor-guard-ide-4.9.15.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +2079 -2050
- package/references/vscode-extension/dist/dashboard/public/style.css +1660 -1628
- package/references/vscode-extension/dist/extension.js +780 -704
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/auto-setup.js +201 -192
- package/references/vscode-extension/dist/lib/core/backups.js +509 -507
- package/references/vscode-extension/dist/lib/core/snapshot.js +441 -416
- package/references/vscode-extension/dist/mcp/server.js +78 -12
- package/references/vscode-extension/dist/package.json +7 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +1775 -1758
- package/references/vscode-extension/dist/skill/SKILL.md +631 -629
- package/references/vscode-extension/extension.js +780 -704
- package/references/vscode-extension/lib/auto-setup.js +201 -192
- package/references/vscode-extension/package.json +7 -1
- package/references/vscode-extension/dist/cursor-guard-ide-4.9.12.vsix +0 -0
|
@@ -1,2050 +1,2079 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* ═══════════════════════════════════════════════════════════════
|
|
4
|
-
Cursor Guard Dashboard — Frontend
|
|
5
|
-
═══════════════════════════════════════════════════════════════ */
|
|
6
|
-
|
|
7
|
-
/* ── I18n Dictionary ──────────────────────────────────────── */
|
|
8
|
-
|
|
9
|
-
const I18N = {
|
|
10
|
-
'en-US': {
|
|
11
|
-
'app.title': 'Cursor Guard Dashboard',
|
|
12
|
-
'topbar.refresh': 'Refresh',
|
|
13
|
-
'topbar.lastRefresh':'Last refresh',
|
|
14
|
-
'state.loading': 'Loading…',
|
|
15
|
-
'state.retry': 'Retry',
|
|
16
|
-
|
|
17
|
-
'overview.title': 'Overview',
|
|
18
|
-
'backups.title': 'Backups & Recovery',
|
|
19
|
-
'protection.title': 'Protection Scope',
|
|
20
|
-
'diagnostics.title':'Diagnostics',
|
|
21
|
-
|
|
22
|
-
'health.title': 'Health',
|
|
23
|
-
'health.healthy': 'Healthy',
|
|
24
|
-
'health.warning': 'Warning',
|
|
25
|
-
'health.critical': 'Critical',
|
|
26
|
-
'health.unknown': 'Unknown',
|
|
27
|
-
|
|
28
|
-
'gitBackup.title': 'Latest Git Backup',
|
|
29
|
-
'gitBackup.none': 'No Git backup yet',
|
|
30
|
-
'shadowBackup.title':'Latest Shadow Snapshot',
|
|
31
|
-
'shadowBackup.none':'No Shadow snapshot yet',
|
|
32
|
-
|
|
33
|
-
'watcher.title': 'Watcher',
|
|
34
|
-
'watcher.running': 'Running',
|
|
35
|
-
'watcher.stopped': 'Stopped',
|
|
36
|
-
'watcher.stale': 'Stale',
|
|
37
|
-
'watcher.pid': 'PID',
|
|
38
|
-
'watcher.since': 'Since',
|
|
39
|
-
|
|
40
|
-
'alert.title': 'Alerts',
|
|
41
|
-
'alert.none': 'No active alerts',
|
|
42
|
-
'alert.active': 'Active Alert',
|
|
43
|
-
'alert.triggered': 'Triggered',
|
|
44
|
-
'alert.expires': 'Expires in',
|
|
45
|
-
'alert.detail': '{count} files in {window}s (threshold: {threshold})',
|
|
46
|
-
'alert.expired': 'Expired',
|
|
47
|
-
'alert.history': 'Recent Alert History',
|
|
48
|
-
'alert.noHistory': 'No alert history',
|
|
49
|
-
'alert.historyCount':'History ({n})',
|
|
50
|
-
'alert.showFiles': 'Show file details',
|
|
51
|
-
'alert.hideFiles': 'Hide file details',
|
|
52
|
-
'alert.col.file': 'File',
|
|
53
|
-
'alert.col.action': 'Action',
|
|
54
|
-
'alert.col.changes':'Changes',
|
|
55
|
-
'alert.col.detail': 'Detail',
|
|
56
|
-
'alert.col.breakdown': 'Breakdown',
|
|
57
|
-
'alert.col.files': 'Files',
|
|
58
|
-
'alert.action.modified': 'Modified',
|
|
59
|
-
'alert.action.added': 'Added',
|
|
60
|
-
'alert.action.deleted': 'Deleted',
|
|
61
|
-
'alert.action.renamed': 'Renamed',
|
|
62
|
-
'alert.breakdown': '{added} added, {modified} modified, {deleted} deleted',
|
|
63
|
-
'alert.suggestion': 'Check recent changes and consider creating a manual snapshot',
|
|
64
|
-
'alert.dismiss': 'Dismiss alert',
|
|
65
|
-
'alert.dismissBusy': 'Dismissing…',
|
|
66
|
-
'alert.viewFiles': 'View file details ({n} files)',
|
|
67
|
-
'preWarning.title': 'Pre-Warning',
|
|
68
|
-
'preWarning.none': 'No destructive edit risk detected',
|
|
69
|
-
'preWarning.active': 'Delete Risk',
|
|
70
|
-
'preWarning.file': 'File',
|
|
71
|
-
'preWarning.risk': 'Risk',
|
|
72
|
-
'preWarning.methods': 'Methods removed',
|
|
73
|
-
'preWarning.suggestion': 'Review this deletion before applying or restore from the latest snapshot',
|
|
74
|
-
'modal.alertFiles': 'Alert File Details',
|
|
75
|
-
'modal.col.restore': 'Restore',
|
|
76
|
-
'modal.copyRestore': 'Copy cmd',
|
|
77
|
-
'modal.restorePreDelete':'Restore pre-delete',
|
|
78
|
-
'modal.copied': 'Copied!',
|
|
79
|
-
|
|
80
|
-
'backups.gitCommits': 'Git Commits',
|
|
81
|
-
'backups.shadowSnapshots': 'Shadow Snapshots',
|
|
82
|
-
'backups.preRestore': 'Pre-Restore Snapshots',
|
|
83
|
-
'backups.diskUsage': 'Disk Usage',
|
|
84
|
-
'backups.gitDisk': 'Git',
|
|
85
|
-
'backups.shadowDisk': 'Shadow',
|
|
86
|
-
'backups.restorePoints': 'Restore Points',
|
|
87
|
-
'backups.filterAll': 'All',
|
|
88
|
-
'backups.noBackups': 'No restore points found',
|
|
89
|
-
'backups.col.time': 'Time',
|
|
90
|
-
'backups.col.type': 'Type',
|
|
91
|
-
'backups.col.meta': 'Scope / baseline',
|
|
92
|
-
'backups.col.ref': 'Ref / Hash',
|
|
93
|
-
'backups.meta.legacyHint': 'Commit predates Guard scope/baseline trailers',
|
|
94
|
-
|
|
95
|
-
'backups.
|
|
96
|
-
|
|
97
|
-
'backups.scope.
|
|
98
|
-
|
|
99
|
-
'backups.
|
|
100
|
-
|
|
101
|
-
'backups.baseline.
|
|
102
|
-
'backups.baseline.
|
|
103
|
-
'backups.baseline.
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
'type.git-
|
|
108
|
-
'type.
|
|
109
|
-
'type.
|
|
110
|
-
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
'protection.
|
|
114
|
-
'protection.
|
|
115
|
-
'protection.
|
|
116
|
-
'protection.
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
|
|
120
|
-
'diagnostics.
|
|
121
|
-
'diagnostics.
|
|
122
|
-
'diagnostics.
|
|
123
|
-
'diagnostics.
|
|
124
|
-
'diagnostics.
|
|
125
|
-
|
|
126
|
-
'
|
|
127
|
-
|
|
128
|
-
'drawer.
|
|
129
|
-
'drawer.
|
|
130
|
-
'drawer.
|
|
131
|
-
'drawer.
|
|
132
|
-
'drawer.
|
|
133
|
-
'drawer.
|
|
134
|
-
'drawer.
|
|
135
|
-
'drawer.field.
|
|
136
|
-
'drawer.field.
|
|
137
|
-
'drawer.field.
|
|
138
|
-
'drawer.field.
|
|
139
|
-
'drawer.field.
|
|
140
|
-
'drawer.field.
|
|
141
|
-
'drawer.field.
|
|
142
|
-
'drawer.field.
|
|
143
|
-
'drawer.field.
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'summary
|
|
153
|
-
'
|
|
154
|
-
'summary.
|
|
155
|
-
'summary.
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
'drawer.field.
|
|
162
|
-
'drawer.
|
|
163
|
-
'drawer.
|
|
164
|
-
|
|
165
|
-
'
|
|
166
|
-
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'
|
|
175
|
-
|
|
176
|
-
'upgrade.
|
|
177
|
-
'upgrade.
|
|
178
|
-
'upgrade.
|
|
179
|
-
|
|
180
|
-
'
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
|
|
195
|
-
'issue.
|
|
196
|
-
'issue.
|
|
197
|
-
'issue.
|
|
198
|
-
'issue.
|
|
199
|
-
'issue.
|
|
200
|
-
|
|
201
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
|
|
206
|
-
'check.
|
|
207
|
-
'check.
|
|
208
|
-
'check.
|
|
209
|
-
'check.
|
|
210
|
-
'check.
|
|
211
|
-
'check.
|
|
212
|
-
'check.
|
|
213
|
-
'check.
|
|
214
|
-
'check.
|
|
215
|
-
'check.
|
|
216
|
-
'check.
|
|
217
|
-
'check.
|
|
218
|
-
'check.
|
|
219
|
-
'check.
|
|
220
|
-
|
|
221
|
-
'
|
|
222
|
-
'
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
|
|
226
|
-
'detail.
|
|
227
|
-
'detail.
|
|
228
|
-
'detail.
|
|
229
|
-
'detail.
|
|
230
|
-
'detail.
|
|
231
|
-
'detail.
|
|
232
|
-
'detail.
|
|
233
|
-
'detail.
|
|
234
|
-
'detail.
|
|
235
|
-
'detail.
|
|
236
|
-
'detail.
|
|
237
|
-
'detail.
|
|
238
|
-
'detail.
|
|
239
|
-
'detail.
|
|
240
|
-
'detail.
|
|
241
|
-
'detail.
|
|
242
|
-
'detail.
|
|
243
|
-
'detail.
|
|
244
|
-
'detail.
|
|
245
|
-
'detail.
|
|
246
|
-
'detail.
|
|
247
|
-
'detail.
|
|
248
|
-
'detail.
|
|
249
|
-
'detail.
|
|
250
|
-
'detail.
|
|
251
|
-
'detail.
|
|
252
|
-
'detail.
|
|
253
|
-
'detail.
|
|
254
|
-
'detail.
|
|
255
|
-
'detail.
|
|
256
|
-
'detail.
|
|
257
|
-
'detail.
|
|
258
|
-
'detail.
|
|
259
|
-
'detail.
|
|
260
|
-
'detail.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
'
|
|
265
|
-
'
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
'
|
|
271
|
-
'
|
|
272
|
-
'
|
|
273
|
-
'
|
|
274
|
-
|
|
275
|
-
'
|
|
276
|
-
'
|
|
277
|
-
'
|
|
278
|
-
'
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
'
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
|
|
286
|
-
'
|
|
287
|
-
'
|
|
288
|
-
'
|
|
289
|
-
'
|
|
290
|
-
|
|
291
|
-
'watcher.
|
|
292
|
-
|
|
293
|
-
'
|
|
294
|
-
'
|
|
295
|
-
'
|
|
296
|
-
'
|
|
297
|
-
|
|
298
|
-
'alert.
|
|
299
|
-
'alert.
|
|
300
|
-
'alert.
|
|
301
|
-
'alert.
|
|
302
|
-
'alert.
|
|
303
|
-
'alert.
|
|
304
|
-
'alert.
|
|
305
|
-
'alert.
|
|
306
|
-
'alert.
|
|
307
|
-
'alert.
|
|
308
|
-
'alert.
|
|
309
|
-
'alert.
|
|
310
|
-
'alert.col.
|
|
311
|
-
'alert.action
|
|
312
|
-
'alert.
|
|
313
|
-
'alert.
|
|
314
|
-
'alert.
|
|
315
|
-
'alert.
|
|
316
|
-
'alert.
|
|
317
|
-
'alert.
|
|
318
|
-
'alert.
|
|
319
|
-
'alert.
|
|
320
|
-
'
|
|
321
|
-
'
|
|
322
|
-
'
|
|
323
|
-
'
|
|
324
|
-
'
|
|
325
|
-
'preWarning.
|
|
326
|
-
'preWarning.
|
|
327
|
-
'
|
|
328
|
-
'
|
|
329
|
-
'
|
|
330
|
-
'
|
|
331
|
-
'
|
|
332
|
-
|
|
333
|
-
'
|
|
334
|
-
'
|
|
335
|
-
'
|
|
336
|
-
'
|
|
337
|
-
|
|
338
|
-
'backups.
|
|
339
|
-
'backups.
|
|
340
|
-
'backups.
|
|
341
|
-
'backups.
|
|
342
|
-
'backups.
|
|
343
|
-
'backups.
|
|
344
|
-
'backups.
|
|
345
|
-
'backups.
|
|
346
|
-
'backups.
|
|
347
|
-
|
|
348
|
-
'backups.
|
|
349
|
-
'backups.
|
|
350
|
-
'backups.
|
|
351
|
-
|
|
352
|
-
'backups.
|
|
353
|
-
'backups.
|
|
354
|
-
|
|
355
|
-
'backups.
|
|
356
|
-
'backups.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
'
|
|
360
|
-
'
|
|
361
|
-
'
|
|
362
|
-
'
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
'
|
|
366
|
-
'
|
|
367
|
-
'
|
|
368
|
-
'
|
|
369
|
-
'
|
|
370
|
-
|
|
371
|
-
'
|
|
372
|
-
'
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
'
|
|
376
|
-
'
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
'
|
|
380
|
-
'
|
|
381
|
-
'
|
|
382
|
-
'
|
|
383
|
-
'
|
|
384
|
-
'
|
|
385
|
-
|
|
386
|
-
'drawer.
|
|
387
|
-
'drawer.
|
|
388
|
-
'drawer.
|
|
389
|
-
'drawer.
|
|
390
|
-
'drawer.
|
|
391
|
-
'drawer.
|
|
392
|
-
'drawer.
|
|
393
|
-
'drawer.field.
|
|
394
|
-
'drawer.field.
|
|
395
|
-
'drawer.field.
|
|
396
|
-
'drawer.field.
|
|
397
|
-
'
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
'
|
|
405
|
-
'
|
|
406
|
-
'
|
|
407
|
-
'
|
|
408
|
-
'
|
|
409
|
-
'
|
|
410
|
-
'
|
|
411
|
-
'
|
|
412
|
-
'
|
|
413
|
-
'
|
|
414
|
-
'
|
|
415
|
-
'
|
|
416
|
-
'
|
|
417
|
-
|
|
418
|
-
'
|
|
419
|
-
|
|
420
|
-
'
|
|
421
|
-
'
|
|
422
|
-
'
|
|
423
|
-
|
|
424
|
-
'
|
|
425
|
-
'
|
|
426
|
-
'
|
|
427
|
-
|
|
428
|
-
'
|
|
429
|
-
|
|
430
|
-
'
|
|
431
|
-
'
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
-
'
|
|
442
|
-
|
|
443
|
-
'
|
|
444
|
-
'
|
|
445
|
-
'
|
|
446
|
-
|
|
447
|
-
'
|
|
448
|
-
'
|
|
449
|
-
'
|
|
450
|
-
'
|
|
451
|
-
'
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
'
|
|
455
|
-
'
|
|
456
|
-
'
|
|
457
|
-
'
|
|
458
|
-
'
|
|
459
|
-
'
|
|
460
|
-
'
|
|
461
|
-
'
|
|
462
|
-
'
|
|
463
|
-
|
|
464
|
-
'check.
|
|
465
|
-
'check.
|
|
466
|
-
'check.Config
|
|
467
|
-
'check.
|
|
468
|
-
'check.
|
|
469
|
-
'check.
|
|
470
|
-
'check.
|
|
471
|
-
'check.
|
|
472
|
-
'check.
|
|
473
|
-
|
|
474
|
-
'
|
|
475
|
-
'
|
|
476
|
-
'
|
|
477
|
-
'
|
|
478
|
-
'
|
|
479
|
-
'
|
|
480
|
-
'
|
|
481
|
-
'
|
|
482
|
-
'
|
|
483
|
-
|
|
484
|
-
'detail.
|
|
485
|
-
'detail.
|
|
486
|
-
'detail.
|
|
487
|
-
'detail.
|
|
488
|
-
'detail.
|
|
489
|
-
'detail.
|
|
490
|
-
'detail.
|
|
491
|
-
'detail.
|
|
492
|
-
'detail.
|
|
493
|
-
'detail.
|
|
494
|
-
'detail.
|
|
495
|
-
'detail.
|
|
496
|
-
'detail.
|
|
497
|
-
'detail.
|
|
498
|
-
'detail.
|
|
499
|
-
'detail.
|
|
500
|
-
'detail.
|
|
501
|
-
'detail.
|
|
502
|
-
'detail.
|
|
503
|
-
'detail.
|
|
504
|
-
'detail.
|
|
505
|
-
'detail.
|
|
506
|
-
'detail.
|
|
507
|
-
'detail.
|
|
508
|
-
'detail.
|
|
509
|
-
'detail.
|
|
510
|
-
'detail.
|
|
511
|
-
'detail.
|
|
512
|
-
'detail.
|
|
513
|
-
'detail.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
{ re: /^
|
|
648
|
-
{ re: /^
|
|
649
|
-
{ re: /^
|
|
650
|
-
{ re: /^
|
|
651
|
-
{ re: /^
|
|
652
|
-
{ re:
|
|
653
|
-
{ re: /^
|
|
654
|
-
{ re: /^
|
|
655
|
-
{ re:
|
|
656
|
-
{ re:
|
|
657
|
-
{ re:
|
|
658
|
-
{ re: /^
|
|
659
|
-
{ re: /^
|
|
660
|
-
{ re: /^(
|
|
661
|
-
{ re: /^
|
|
662
|
-
{ re: /^(
|
|
663
|
-
{ re: /^
|
|
664
|
-
{ re: /^(.+?)
|
|
665
|
-
{ re:
|
|
666
|
-
{ re:
|
|
667
|
-
{ re:
|
|
668
|
-
{ re: /^
|
|
669
|
-
{ re: /^
|
|
670
|
-
{ re: /^
|
|
671
|
-
{ re: /^
|
|
672
|
-
{ re: /^
|
|
673
|
-
{ re: /^
|
|
674
|
-
{ re: /^
|
|
675
|
-
{ re: /^
|
|
676
|
-
]
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
async function
|
|
767
|
-
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
<
|
|
1121
|
-
<
|
|
1122
|
-
<
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
body.
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
<div class="alert-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
.
|
|
1259
|
-
|
|
1260
|
-
.
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
function
|
|
1265
|
-
if (!
|
|
1266
|
-
return
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
if (!
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
if (
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
: ''
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
<
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
</
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
<
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
}).
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
{
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
container.
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
if (
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
//
|
|
1931
|
-
|
|
1932
|
-
const
|
|
1933
|
-
if (
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
});
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
banner.
|
|
1996
|
-
banner.
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
await
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
4
|
+
Cursor Guard Dashboard — Frontend
|
|
5
|
+
═══════════════════════════════════════════════════════════════ */
|
|
6
|
+
|
|
7
|
+
/* ── I18n Dictionary ──────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
const I18N = {
|
|
10
|
+
'en-US': {
|
|
11
|
+
'app.title': 'Cursor Guard Dashboard',
|
|
12
|
+
'topbar.refresh': 'Refresh',
|
|
13
|
+
'topbar.lastRefresh':'Last refresh',
|
|
14
|
+
'state.loading': 'Loading…',
|
|
15
|
+
'state.retry': 'Retry',
|
|
16
|
+
|
|
17
|
+
'overview.title': 'Overview',
|
|
18
|
+
'backups.title': 'Backups & Recovery',
|
|
19
|
+
'protection.title': 'Protection Scope',
|
|
20
|
+
'diagnostics.title':'Diagnostics',
|
|
21
|
+
|
|
22
|
+
'health.title': 'Health',
|
|
23
|
+
'health.healthy': 'Healthy',
|
|
24
|
+
'health.warning': 'Warning',
|
|
25
|
+
'health.critical': 'Critical',
|
|
26
|
+
'health.unknown': 'Unknown',
|
|
27
|
+
|
|
28
|
+
'gitBackup.title': 'Latest Git Backup',
|
|
29
|
+
'gitBackup.none': 'No Git backup yet',
|
|
30
|
+
'shadowBackup.title':'Latest Shadow Snapshot',
|
|
31
|
+
'shadowBackup.none':'No Shadow snapshot yet',
|
|
32
|
+
|
|
33
|
+
'watcher.title': 'Watcher',
|
|
34
|
+
'watcher.running': 'Running',
|
|
35
|
+
'watcher.stopped': 'Stopped',
|
|
36
|
+
'watcher.stale': 'Stale',
|
|
37
|
+
'watcher.pid': 'PID',
|
|
38
|
+
'watcher.since': 'Since',
|
|
39
|
+
|
|
40
|
+
'alert.title': 'Alerts',
|
|
41
|
+
'alert.none': 'No active alerts',
|
|
42
|
+
'alert.active': 'Active Alert',
|
|
43
|
+
'alert.triggered': 'Triggered',
|
|
44
|
+
'alert.expires': 'Expires in',
|
|
45
|
+
'alert.detail': '{count} files in {window}s (threshold: {threshold})',
|
|
46
|
+
'alert.expired': 'Expired',
|
|
47
|
+
'alert.history': 'Recent Alert History',
|
|
48
|
+
'alert.noHistory': 'No alert history',
|
|
49
|
+
'alert.historyCount':'History ({n})',
|
|
50
|
+
'alert.showFiles': 'Show file details',
|
|
51
|
+
'alert.hideFiles': 'Hide file details',
|
|
52
|
+
'alert.col.file': 'File',
|
|
53
|
+
'alert.col.action': 'Action',
|
|
54
|
+
'alert.col.changes':'Changes',
|
|
55
|
+
'alert.col.detail': 'Detail',
|
|
56
|
+
'alert.col.breakdown': 'Breakdown',
|
|
57
|
+
'alert.col.files': 'Files',
|
|
58
|
+
'alert.action.modified': 'Modified',
|
|
59
|
+
'alert.action.added': 'Added',
|
|
60
|
+
'alert.action.deleted': 'Deleted',
|
|
61
|
+
'alert.action.renamed': 'Renamed',
|
|
62
|
+
'alert.breakdown': '{added} added, {modified} modified, {deleted} deleted',
|
|
63
|
+
'alert.suggestion': 'Check recent changes and consider creating a manual snapshot',
|
|
64
|
+
'alert.dismiss': 'Dismiss alert',
|
|
65
|
+
'alert.dismissBusy': 'Dismissing…',
|
|
66
|
+
'alert.viewFiles': 'View file details ({n} files)',
|
|
67
|
+
'preWarning.title': 'Pre-Warning',
|
|
68
|
+
'preWarning.none': 'No destructive edit risk detected',
|
|
69
|
+
'preWarning.active': 'Delete Risk',
|
|
70
|
+
'preWarning.file': 'File',
|
|
71
|
+
'preWarning.risk': 'Risk',
|
|
72
|
+
'preWarning.methods': 'Methods removed',
|
|
73
|
+
'preWarning.suggestion': 'Review this deletion before applying or restore from the latest snapshot',
|
|
74
|
+
'modal.alertFiles': 'Alert File Details',
|
|
75
|
+
'modal.col.restore': 'Restore',
|
|
76
|
+
'modal.copyRestore': 'Copy cmd',
|
|
77
|
+
'modal.restorePreDelete':'Restore pre-delete',
|
|
78
|
+
'modal.copied': 'Copied!',
|
|
79
|
+
|
|
80
|
+
'backups.gitCommits': 'Git Commits',
|
|
81
|
+
'backups.shadowSnapshots': 'Shadow Snapshots',
|
|
82
|
+
'backups.preRestore': 'Pre-Restore Snapshots',
|
|
83
|
+
'backups.diskUsage': 'Disk Usage',
|
|
84
|
+
'backups.gitDisk': 'Git',
|
|
85
|
+
'backups.shadowDisk': 'Shadow',
|
|
86
|
+
'backups.restorePoints': 'Restore Points',
|
|
87
|
+
'backups.filterAll': 'All',
|
|
88
|
+
'backups.noBackups': 'No restore points found',
|
|
89
|
+
'backups.col.time': 'Time',
|
|
90
|
+
'backups.col.type': 'Type',
|
|
91
|
+
'backups.col.meta': 'Scope / baseline',
|
|
92
|
+
'backups.col.ref': 'Ref / Hash',
|
|
93
|
+
'backups.meta.legacyHint': 'Commit predates Guard scope/baseline trailers',
|
|
94
|
+
'backups.bookmarkBadge': 'Bookmark (no tree change)',
|
|
95
|
+
'backups.bookmarkTitle': 'Same tree as previous Guard baseline; commit still records intent and timestamp on the timeline.',
|
|
96
|
+
|
|
97
|
+
'backups.scope.full': 'Full workspace',
|
|
98
|
+
'backups.scope.narrow': 'Protect patterns only',
|
|
99
|
+
'backups.scope.unknown': 'Unknown',
|
|
100
|
+
|
|
101
|
+
'backups.baseline.autoBackup': 'Δ vs last auto-backup tip',
|
|
102
|
+
'backups.baseline.snapshot': 'Δ vs last manual snapshot tip',
|
|
103
|
+
'backups.baseline.initial': 'First Guard commit (no parent)',
|
|
104
|
+
'backups.baseline.other': 'Δ vs custom ref',
|
|
105
|
+
'backups.baseline.unknown': 'Unknown baseline',
|
|
106
|
+
|
|
107
|
+
'type.git-auto-backup': 'Git Auto-Backup',
|
|
108
|
+
'type.git-pre-restore': 'Git Pre-Restore',
|
|
109
|
+
'type.git-snapshot': 'Git Snapshot',
|
|
110
|
+
'type.shadow': 'Shadow Snapshot',
|
|
111
|
+
'type.shadow-pre-restore': 'Shadow Pre-Restore',
|
|
112
|
+
|
|
113
|
+
'protection.protect': 'Protected Patterns',
|
|
114
|
+
'protection.ignore': 'Ignored Patterns',
|
|
115
|
+
'protection.fileCount': '{n} files in protection scope',
|
|
116
|
+
'protection.note': 'These patterns define which files are protected. This is not the full directory listing.',
|
|
117
|
+
'protection.allFiles': 'All files (no protect patterns configured)',
|
|
118
|
+
'protection.noIgnore': 'None',
|
|
119
|
+
|
|
120
|
+
'diagnostics.pass': 'Pass',
|
|
121
|
+
'diagnostics.warn': 'Warn',
|
|
122
|
+
'diagnostics.fail': 'Fail',
|
|
123
|
+
'diagnostics.hint': 'Click for details →',
|
|
124
|
+
'diagnostics.PASS': 'PASS',
|
|
125
|
+
'diagnostics.WARN': 'WARN',
|
|
126
|
+
'diagnostics.FAIL': 'FAIL',
|
|
127
|
+
|
|
128
|
+
'drawer.restorePoint': 'Restore Point Details',
|
|
129
|
+
'drawer.doctorTitle': 'Diagnostic Details',
|
|
130
|
+
'drawer.close': 'Close',
|
|
131
|
+
'drawer.preview': 'Preview JSON',
|
|
132
|
+
'drawer.copyRef': 'Copy Ref',
|
|
133
|
+
'drawer.copyJson': 'Copy JSON',
|
|
134
|
+
'drawer.copied': 'Copied!',
|
|
135
|
+
'drawer.field.time': 'Time',
|
|
136
|
+
'drawer.field.type': 'Type',
|
|
137
|
+
'drawer.field.ref': 'Ref',
|
|
138
|
+
'drawer.field.hash': 'Commit Hash',
|
|
139
|
+
'drawer.field.path': 'Path',
|
|
140
|
+
'drawer.field.message': 'Message',
|
|
141
|
+
'drawer.field.filesChanged': 'Files Changed',
|
|
142
|
+
'drawer.field.summary': 'Change Summary',
|
|
143
|
+
'drawer.field.trigger': 'Trigger',
|
|
144
|
+
'drawer.field.guardScope': 'Snapshot scope',
|
|
145
|
+
'drawer.field.guardDiffBase': 'Diff baseline',
|
|
146
|
+
'drawer.field.bookmark': 'Bookmark snapshot',
|
|
147
|
+
'drawer.field.guardEvent': 'MCP / audit event',
|
|
148
|
+
'trigger.auto': 'Auto (scheduled)',
|
|
149
|
+
'trigger.manual': 'Manual (agent)',
|
|
150
|
+
'trigger.pre-restore': 'Pre-Restore',
|
|
151
|
+
'trigger.mcp-event': 'MCP audit event',
|
|
152
|
+
'backups.col.summary': 'Changes',
|
|
153
|
+
'backups.search': 'Search files…',
|
|
154
|
+
'summary.modified': 'Modified',
|
|
155
|
+
'summary.added': 'Added',
|
|
156
|
+
'summary.deleted': 'Deleted',
|
|
157
|
+
'summary.renamed': 'Renamed',
|
|
158
|
+
'summary.files': 'files',
|
|
159
|
+
'summary.linesHint': 'Total lines added / deleted in this backup (from Summary)',
|
|
160
|
+
'summary.andMore': 'and {n} more…',
|
|
161
|
+
'drawer.field.intent': 'Intent',
|
|
162
|
+
'drawer.field.agent': 'Agent',
|
|
163
|
+
'drawer.field.session': 'Session',
|
|
164
|
+
'drawer.field.from': 'From (current)',
|
|
165
|
+
'drawer.field.restoreTo':'Restore to',
|
|
166
|
+
'drawer.field.restoreFile':'Restored file',
|
|
167
|
+
'drawer.restoreCmd': 'Copy Restore Command',
|
|
168
|
+
'drawer.restoreCmdFile':'Copy File Restore Command',
|
|
169
|
+
|
|
170
|
+
'watcher.lastScan': 'Last scan',
|
|
171
|
+
|
|
172
|
+
'error.fetchFailed': 'Failed to fetch data',
|
|
173
|
+
'error.sectionFailed': 'This section failed to load',
|
|
174
|
+
'empty.noData': 'No data available',
|
|
175
|
+
|
|
176
|
+
'upgrade.banner': 'New version {installed} available (current server: {server}). Please restart the Dashboard service to load the latest features.',
|
|
177
|
+
'upgrade.dismiss': 'Dismiss',
|
|
178
|
+
'upgrade.restartNow': 'Restart Now',
|
|
179
|
+
'upgrade.restart': 'Manual restart',
|
|
180
|
+
'upgrade.hint': 'Stop the current process (Ctrl+C), then run: cursor-guard-backup --path <dir> --dashboard',
|
|
181
|
+
'upgrade.restarting': 'Restarting...',
|
|
182
|
+
'upgrade.waiting': 'Waiting for server...',
|
|
183
|
+
'upgrade.failed': 'Restart failed, try manually',
|
|
184
|
+
|
|
185
|
+
'strategy.git': 'Git',
|
|
186
|
+
'strategy.shadow': 'Shadow',
|
|
187
|
+
'strategy.both': 'Both',
|
|
188
|
+
|
|
189
|
+
'time.justNow': 'just now',
|
|
190
|
+
'time.secondsAgo': '{n}s ago',
|
|
191
|
+
'time.minutesAgo': '{n}m ago',
|
|
192
|
+
'time.hoursAgo': '{n}h ago',
|
|
193
|
+
'time.daysAgo': '{n}d ago',
|
|
194
|
+
|
|
195
|
+
'issue.watcher_not_running': 'Auto-backup watcher is not running',
|
|
196
|
+
'issue.watcher_stale': 'Watcher has a stale lock file (process not running)',
|
|
197
|
+
'issue.strategy_no_git': 'Strategy requires Git but directory is not a git repo',
|
|
198
|
+
'issue.no_auto_backup_ref': 'No auto-backup ref found — watcher may not have run yet',
|
|
199
|
+
'issue.disk_critically_low': 'Disk space critically low ({gb} GB free)',
|
|
200
|
+
'issue.disk_low': 'Disk space low ({gb} GB free)',
|
|
201
|
+
'issue.git_backup_stale': 'Last git backup is stale ({rel})',
|
|
202
|
+
'issue.active_alert': 'Active alert: {type} — {count} files in {window}s',
|
|
203
|
+
'issue.pre_warning_active': 'Pre-warning active: {summary}',
|
|
204
|
+
'issue.alert_high_velocity': 'High volume of file changes detected. Consider reviewing recent modifications and creating a manual snapshot.',
|
|
205
|
+
|
|
206
|
+
'check.Git installed': 'Git installed',
|
|
207
|
+
'check.Git repository': 'Git repository',
|
|
208
|
+
'check.Config file': 'Config file',
|
|
209
|
+
'check.Strategy compatibility': 'Strategy compatibility',
|
|
210
|
+
'check.Backup ref': 'Backup ref',
|
|
211
|
+
'check.Guard refs': 'Guard refs',
|
|
212
|
+
'check.Shadow copies': 'Shadow copies',
|
|
213
|
+
'check.Backup dir ignored': 'Backup dir ignored',
|
|
214
|
+
'check.Config: backup_strategy': 'Config: backup_strategy',
|
|
215
|
+
'check.Config: pre_restore_backup': 'Config: pre_restore_backup',
|
|
216
|
+
'check.Config: interval': 'Config: interval',
|
|
217
|
+
'check.Config: retention.mode': 'Config: retention.mode',
|
|
218
|
+
'check.Config: git_retention.mode': 'Config: git_retention.mode',
|
|
219
|
+
'check.Protect patterns': 'Protect patterns',
|
|
220
|
+
'check.Disk space': 'Disk space',
|
|
221
|
+
'check.Lock file': 'Lock file',
|
|
222
|
+
'check.Node.js': 'Node.js',
|
|
223
|
+
'check.MCP server': 'MCP server',
|
|
224
|
+
'check.MCP version': 'MCP version',
|
|
225
|
+
|
|
226
|
+
'detail.git_version': 'version {v}',
|
|
227
|
+
'detail.git_not_found': 'git not found in PATH; only shadow strategy available',
|
|
228
|
+
'detail.worktree': 'worktree detected (git-dir: {dir})',
|
|
229
|
+
'detail.standard_repo': 'standard repo',
|
|
230
|
+
'detail.not_git_repo': 'not a Git repo; git/both strategies won\'t work',
|
|
231
|
+
'detail.config_valid': '.cursor-guard.json found and valid JSON',
|
|
232
|
+
'detail.config_parse_error': 'JSON parse error: {err}',
|
|
233
|
+
'detail.config_missing': 'no .cursor-guard.json found; using defaults (protect everything)',
|
|
234
|
+
'detail.strategy_no_git': 'backup_strategy=\'{s}\' but directory is not a Git repo',
|
|
235
|
+
'detail.strategy_ok': 'backup_strategy=\'{s}\' and Git repo exists',
|
|
236
|
+
'detail.strategy_shadow': 'backup_strategy=\'shadow\' — no Git required',
|
|
237
|
+
'detail.strategy_unknown': 'unknown backup_strategy=\'{s}\' (must be git/shadow/both)',
|
|
238
|
+
'detail.ref_exists': 'refs/guard/auto-backup exists ({n} commits)',
|
|
239
|
+
'detail.ref_legacy': 'legacy refs/heads/cursor-guard/auto-backup found ({n} commits) — run auto-backup once to migrate',
|
|
240
|
+
'detail.ref_not_created': 'refs/guard/auto-backup not created yet (will be created on first backup)',
|
|
241
|
+
'detail.guard_refs_found': '{n} ref(s) found ({pre} pre-restore snapshots)',
|
|
242
|
+
'detail.guard_refs_none': 'no guard refs yet (created on first snapshot or restore)',
|
|
243
|
+
'detail.shadow_stats': '{n} snapshot(s), {mb} MB total',
|
|
244
|
+
'detail.shadow_not_found': '.cursor-guard-backup/ not found (will be created on first shadow backup)',
|
|
245
|
+
'detail.gitignore_ok': '.cursor-guard-backup/ is git-ignored',
|
|
246
|
+
'detail.gitignore_missing': '.cursor-guard-backup/ may NOT be git-ignored — backup changes could trigger commits',
|
|
247
|
+
'detail.invalid_value': 'invalid value \'{v}\'',
|
|
248
|
+
'detail.pre_restore_never': 'set to \'never\' — restores won\'t auto-preserve current version',
|
|
249
|
+
'detail.interval_low': '{n}s is below minimum (5s), will be clamped',
|
|
250
|
+
'detail.protect_count': '{matched} / {total} files matched by protect patterns',
|
|
251
|
+
'detail.disk_critical': '{gb} GB free — critically low',
|
|
252
|
+
'detail.disk_free': '{gb} GB free',
|
|
253
|
+
'detail.disk_unknown': 'could not determine free space',
|
|
254
|
+
'detail.lock_running': 'watcher running (pid={pid}, since {since})',
|
|
255
|
+
'detail.lock_stale': 'stale lock file (pid={pid} is dead) — safe to delete or run doctor_fix',
|
|
256
|
+
'detail.lock_exists': 'lock file exists — another instance may be running. {info}',
|
|
257
|
+
'detail.lock_none': 'no lock file (no running instance)',
|
|
258
|
+
'detail.node_ok': '{v}',
|
|
259
|
+
'detail.node_old': '{v} — recommended >=18',
|
|
260
|
+
'detail.mcp_ok': 'server.js found, SDK {v}',
|
|
261
|
+
'detail.mcp_no_sdk': 'server.js found but @modelcontextprotocol/sdk not installed — run: cd <skill-dir>; npm install',
|
|
262
|
+
'detail.mcp_no_server': 'SDK installed ({v}) but server.js not found at expected path',
|
|
263
|
+
'detail.mcp_not_configured': 'MCP not configured (optional — cursor-guard works without it)',
|
|
264
|
+
'detail.mcp_version_mismatch': 'running v{mem} but disk has v{disk} — restart Cursor to load the new version',
|
|
265
|
+
'detail.mcp_version_ok': 'v{v}',
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
'zh-CN': {
|
|
269
|
+
'app.title': 'Cursor Guard 仪表盘',
|
|
270
|
+
'topbar.refresh': '刷新',
|
|
271
|
+
'topbar.lastRefresh':'上次刷新',
|
|
272
|
+
'state.loading': '加载中…',
|
|
273
|
+
'state.retry': '重试',
|
|
274
|
+
|
|
275
|
+
'overview.title': '总览',
|
|
276
|
+
'backups.title': '备份与恢复',
|
|
277
|
+
'protection.title': '保护范围',
|
|
278
|
+
'diagnostics.title':'诊断',
|
|
279
|
+
|
|
280
|
+
'health.title': '健康状态',
|
|
281
|
+
'health.healthy': '健康',
|
|
282
|
+
'health.warning': '警告',
|
|
283
|
+
'health.critical': '严重',
|
|
284
|
+
'health.unknown': '未知',
|
|
285
|
+
|
|
286
|
+
'gitBackup.title': '最近 Git 备份',
|
|
287
|
+
'gitBackup.none': '暂无 Git 备份',
|
|
288
|
+
'shadowBackup.title':'最近影子快照',
|
|
289
|
+
'shadowBackup.none':'暂无影子快照',
|
|
290
|
+
|
|
291
|
+
'watcher.title': '守护进程',
|
|
292
|
+
'watcher.running': '运行中',
|
|
293
|
+
'watcher.stopped': '已停止',
|
|
294
|
+
'watcher.stale': '已过期',
|
|
295
|
+
'watcher.pid': 'PID',
|
|
296
|
+
'watcher.since': '启动时间',
|
|
297
|
+
|
|
298
|
+
'alert.title': '告警',
|
|
299
|
+
'alert.none': '无活跃告警',
|
|
300
|
+
'alert.active': '活跃告警',
|
|
301
|
+
'alert.triggered': '触发时间',
|
|
302
|
+
'alert.expires': '剩余有效',
|
|
303
|
+
'alert.detail': '{count} 个文件在 {window} 秒内变更(阈值:{threshold})',
|
|
304
|
+
'alert.expired': '已过期',
|
|
305
|
+
'alert.history': '近期告警历史',
|
|
306
|
+
'alert.noHistory': '暂无告警记录',
|
|
307
|
+
'alert.historyCount':'历史({n} 条)',
|
|
308
|
+
'alert.showFiles': '展开文件详情',
|
|
309
|
+
'alert.hideFiles': '收起文件详情',
|
|
310
|
+
'alert.col.file': '文件',
|
|
311
|
+
'alert.col.action': '操作',
|
|
312
|
+
'alert.col.changes':'变化量',
|
|
313
|
+
'alert.col.detail': '详情',
|
|
314
|
+
'alert.col.breakdown': '文件类型',
|
|
315
|
+
'alert.col.files': '文件列表',
|
|
316
|
+
'alert.action.modified': '修改',
|
|
317
|
+
'alert.action.added': '新增',
|
|
318
|
+
'alert.action.deleted': '删除',
|
|
319
|
+
'alert.action.renamed': '重命名',
|
|
320
|
+
'alert.breakdown': '新增 {added} · 修改 {modified} · 删除 {deleted}',
|
|
321
|
+
'alert.suggestion': '建议检查近期变更,并考虑手动创建快照',
|
|
322
|
+
'alert.dismiss': '忽略此告警',
|
|
323
|
+
'alert.dismissBusy': '正在忽略…',
|
|
324
|
+
'alert.viewFiles': '查看文件详情({n} 个文件)',
|
|
325
|
+
'preWarning.title': '事先预警',
|
|
326
|
+
'preWarning.none': '未检测到破坏性编辑风险',
|
|
327
|
+
'preWarning.active': '删除风险',
|
|
328
|
+
'preWarning.file': '文件',
|
|
329
|
+
'preWarning.risk': '风险',
|
|
330
|
+
'preWarning.methods': '移除的方法数',
|
|
331
|
+
'preWarning.suggestion': '建议在应用前检查这次删除,或直接从最新快照恢复',
|
|
332
|
+
'modal.alertFiles': '告警文件详情',
|
|
333
|
+
'modal.col.restore': '恢复',
|
|
334
|
+
'modal.copyRestore': '复制命令',
|
|
335
|
+
'modal.restorePreDelete':'恢复删除前',
|
|
336
|
+
'modal.copied': '已复制!',
|
|
337
|
+
|
|
338
|
+
'backups.gitCommits': 'Git 提交数',
|
|
339
|
+
'backups.shadowSnapshots': '影子快照',
|
|
340
|
+
'backups.preRestore': '恢复前快照',
|
|
341
|
+
'backups.diskUsage': '磁盘占用',
|
|
342
|
+
'backups.gitDisk': 'Git',
|
|
343
|
+
'backups.shadowDisk': 'Shadow',
|
|
344
|
+
'backups.restorePoints': '恢复点',
|
|
345
|
+
'backups.filterAll': '全部',
|
|
346
|
+
'backups.noBackups': '暂无恢复点',
|
|
347
|
+
'backups.col.time': '时间',
|
|
348
|
+
'backups.col.type': '类型',
|
|
349
|
+
'backups.col.meta': '范围 / 基线',
|
|
350
|
+
'backups.col.ref': '引用 / Hash',
|
|
351
|
+
'backups.meta.legacyHint': '该提交早于 Guard 范围/基线 trailer',
|
|
352
|
+
'backups.bookmarkBadge': '书签(无文件变更)',
|
|
353
|
+
'backups.bookmarkTitle': '与上一 Guard 基线树相同;仍生成提交,用于记录意图与时间。',
|
|
354
|
+
|
|
355
|
+
'backups.scope.full': '全工作区',
|
|
356
|
+
'backups.scope.narrow': '仅 protect 规则内',
|
|
357
|
+
'backups.scope.unknown': '未知',
|
|
358
|
+
|
|
359
|
+
'backups.baseline.autoBackup': '相对上次自动备份 tip',
|
|
360
|
+
'backups.baseline.snapshot': '相对上次手动快照 tip',
|
|
361
|
+
'backups.baseline.initial': '首条 Guard 提交(无父提交)',
|
|
362
|
+
'backups.baseline.other': '相对自定义引用',
|
|
363
|
+
'backups.baseline.unknown': '基线未知',
|
|
364
|
+
|
|
365
|
+
'type.git-auto-backup': 'Git 自动备份',
|
|
366
|
+
'type.git-pre-restore': 'Git 恢复前快照',
|
|
367
|
+
'type.git-snapshot': 'Git 快照',
|
|
368
|
+
'type.shadow': '影子快照',
|
|
369
|
+
'type.shadow-pre-restore': '影子恢复前快照',
|
|
370
|
+
|
|
371
|
+
'protection.protect': '保护规则',
|
|
372
|
+
'protection.ignore': '忽略规则',
|
|
373
|
+
'protection.fileCount': '{n} 个文件在保护范围内',
|
|
374
|
+
'protection.note': '以下是当前会进入保护范围的文件规则,不等于当前目录全部文件。',
|
|
375
|
+
'protection.allFiles': '全部文件(未配置 protect 规则)',
|
|
376
|
+
'protection.noIgnore': '无',
|
|
377
|
+
|
|
378
|
+
'diagnostics.pass': '通过',
|
|
379
|
+
'diagnostics.warn': '警告',
|
|
380
|
+
'diagnostics.fail': '失败',
|
|
381
|
+
'diagnostics.hint': '点击查看详情 →',
|
|
382
|
+
'diagnostics.PASS': '通过',
|
|
383
|
+
'diagnostics.WARN': '警告',
|
|
384
|
+
'diagnostics.FAIL': '失败',
|
|
385
|
+
|
|
386
|
+
'drawer.restorePoint': '恢复点详情',
|
|
387
|
+
'drawer.doctorTitle': '诊断详情',
|
|
388
|
+
'drawer.close': '关闭',
|
|
389
|
+
'drawer.preview': '预览 JSON',
|
|
390
|
+
'drawer.copyRef': '复制引用',
|
|
391
|
+
'drawer.copyJson': '复制 JSON',
|
|
392
|
+
'drawer.copied': '已复制!',
|
|
393
|
+
'drawer.field.time': '时间',
|
|
394
|
+
'drawer.field.type': '类型',
|
|
395
|
+
'drawer.field.ref': '引用',
|
|
396
|
+
'drawer.field.hash': '提交 Hash',
|
|
397
|
+
'drawer.field.path': '路径',
|
|
398
|
+
'drawer.field.message': '消息',
|
|
399
|
+
'drawer.field.filesChanged': '变更文件数',
|
|
400
|
+
'drawer.field.summary': '变更摘要',
|
|
401
|
+
'drawer.field.trigger': '触发方式',
|
|
402
|
+
'drawer.field.guardScope': '快照范围',
|
|
403
|
+
'drawer.field.guardDiffBase': '差异基线',
|
|
404
|
+
'drawer.field.bookmark': '书签快照',
|
|
405
|
+
'drawer.field.guardEvent': 'MCP / 审计事件',
|
|
406
|
+
'trigger.auto': '自动(定时)',
|
|
407
|
+
'trigger.manual': '手动(Agent)',
|
|
408
|
+
'trigger.pre-restore': '恢复前快照',
|
|
409
|
+
'trigger.mcp-event': 'MCP 审计事件',
|
|
410
|
+
'backups.col.summary': '变更',
|
|
411
|
+
'backups.search': '搜索文件…',
|
|
412
|
+
'summary.modified': '修改',
|
|
413
|
+
'summary.added': '新增',
|
|
414
|
+
'summary.deleted': '删除',
|
|
415
|
+
'summary.renamed': '重命名',
|
|
416
|
+
'summary.files': '个文件',
|
|
417
|
+
'summary.linesHint': '本条备份新增/删除行数合计(来自 Summary)',
|
|
418
|
+
'summary.andMore': '等 {n} 个文件…',
|
|
419
|
+
'drawer.field.intent': '操作意图',
|
|
420
|
+
'drawer.field.agent': 'AI 模型',
|
|
421
|
+
'drawer.field.session': '会话 ID',
|
|
422
|
+
'drawer.field.from': '恢复前版本',
|
|
423
|
+
'drawer.field.restoreTo':'恢复目标',
|
|
424
|
+
'drawer.field.restoreFile':'恢复文件',
|
|
425
|
+
'drawer.restoreCmd': '复制恢复命令',
|
|
426
|
+
'drawer.restoreCmdFile':'复制文件恢复命令',
|
|
427
|
+
|
|
428
|
+
'watcher.lastScan': '最后扫描',
|
|
429
|
+
|
|
430
|
+
'error.fetchFailed': '数据拉取失败',
|
|
431
|
+
'error.sectionFailed': '此区块加载失败',
|
|
432
|
+
'empty.noData': '暂无数据',
|
|
433
|
+
|
|
434
|
+
'upgrade.banner': '检测到新版本 {installed}(当前服务: {server}),请重启 Dashboard 服务以加载最新功能',
|
|
435
|
+
'upgrade.dismiss': '关闭',
|
|
436
|
+
'upgrade.restartNow': '一键重启',
|
|
437
|
+
'upgrade.restart': '手动重启',
|
|
438
|
+
'upgrade.hint': '停止当前进程 (Ctrl+C),然后运行: cursor-guard-backup --path <目录> --dashboard',
|
|
439
|
+
'upgrade.restarting': '正在重启...',
|
|
440
|
+
'upgrade.waiting': '等待服务就绪...',
|
|
441
|
+
'upgrade.failed': '重启失败,请手动重启',
|
|
442
|
+
|
|
443
|
+
'strategy.git': 'Git',
|
|
444
|
+
'strategy.shadow': '影子',
|
|
445
|
+
'strategy.both': '双重',
|
|
446
|
+
|
|
447
|
+
'time.justNow': '刚刚',
|
|
448
|
+
'time.secondsAgo': '{n} 秒前',
|
|
449
|
+
'time.minutesAgo': '{n} 分钟前',
|
|
450
|
+
'time.hoursAgo': '{n} 小时前',
|
|
451
|
+
'time.daysAgo': '{n} 天前',
|
|
452
|
+
|
|
453
|
+
'issue.watcher_not_running': '自动备份守护进程未运行',
|
|
454
|
+
'issue.watcher_stale': '守护进程锁文件已过期(进程未运行)',
|
|
455
|
+
'issue.strategy_no_git': '策略需要 Git 但目录不是 Git 仓库',
|
|
456
|
+
'issue.no_auto_backup_ref': '未找到自动备份引用——守护进程可能尚未运行',
|
|
457
|
+
'issue.disk_critically_low': '磁盘空间严重不足({gb} GB 可用)',
|
|
458
|
+
'issue.disk_low': '磁盘空间不足({gb} GB 可用)',
|
|
459
|
+
'issue.git_backup_stale': '最近 Git 备份已过时({rel})',
|
|
460
|
+
'issue.active_alert': '活跃告警:{type}——{count} 个文件在 {window} 秒内变更',
|
|
461
|
+
'issue.pre_warning_active': '事先预警生效:{summary}',
|
|
462
|
+
'issue.alert_high_velocity': '检测到大量文件变更,建议检查最近修改并手动创建快照。',
|
|
463
|
+
|
|
464
|
+
'check.Git installed': 'Git 安装状态',
|
|
465
|
+
'check.Git repository': 'Git 仓库',
|
|
466
|
+
'check.Config file': '配置文件',
|
|
467
|
+
'check.Strategy compatibility': '策略兼容性',
|
|
468
|
+
'check.Backup ref': '备份引用',
|
|
469
|
+
'check.Guard refs': 'Guard 引用',
|
|
470
|
+
'check.Shadow copies': '影子拷贝',
|
|
471
|
+
'check.Backup dir ignored': '备份目录忽略',
|
|
472
|
+
'check.Config: backup_strategy': '配置:备份策略',
|
|
473
|
+
'check.Config: pre_restore_backup': '配置:恢复前备份',
|
|
474
|
+
'check.Config: interval': '配置:备份间隔',
|
|
475
|
+
'check.Config: retention.mode': '配置:留存模式',
|
|
476
|
+
'check.Config: git_retention.mode': '配置:Git 留存模式',
|
|
477
|
+
'check.Protect patterns': '保护规则匹配',
|
|
478
|
+
'check.Disk space': '磁盘空间',
|
|
479
|
+
'check.Lock file': '锁文件',
|
|
480
|
+
'check.Node.js': 'Node.js',
|
|
481
|
+
'check.MCP server': 'MCP 服务器',
|
|
482
|
+
'check.MCP version': 'MCP 版本',
|
|
483
|
+
|
|
484
|
+
'detail.git_version': '版本 {v}',
|
|
485
|
+
'detail.git_not_found': 'PATH 中未找到 git;仅可使用 shadow 策略',
|
|
486
|
+
'detail.worktree': '检测到工作树(git-dir:{dir})',
|
|
487
|
+
'detail.standard_repo': '标准仓库',
|
|
488
|
+
'detail.not_git_repo': '非 Git 仓库;git/both 策略不可用',
|
|
489
|
+
'detail.config_valid': '.cursor-guard.json 已找到且 JSON 格式有效',
|
|
490
|
+
'detail.config_parse_error': 'JSON 解析错误:{err}',
|
|
491
|
+
'detail.config_missing': '未找到 .cursor-guard.json;使用默认设置(保护全部文件)',
|
|
492
|
+
'detail.strategy_no_git': 'backup_strategy=\'{s}\' 但目录不是 Git 仓库',
|
|
493
|
+
'detail.strategy_ok': 'backup_strategy=\'{s}\' 且 Git 仓库存在',
|
|
494
|
+
'detail.strategy_shadow': 'backup_strategy=\'shadow\'——不需要 Git',
|
|
495
|
+
'detail.strategy_unknown': '未知 backup_strategy=\'{s}\'(须为 git/shadow/both)',
|
|
496
|
+
'detail.ref_exists': 'refs/guard/auto-backup 存在({n} 个提交)',
|
|
497
|
+
'detail.ref_legacy': '发现旧版 refs/heads/cursor-guard/auto-backup({n} 个提交)——运行一次自动备份即可迁移',
|
|
498
|
+
'detail.ref_not_created': 'refs/guard/auto-backup 尚未创建(首次备份时自动创建)',
|
|
499
|
+
'detail.guard_refs_found': '{n} 个引用({pre} 个恢复前快照)',
|
|
500
|
+
'detail.guard_refs_none': '尚无 guard 引用(首次快照或恢复时创建)',
|
|
501
|
+
'detail.shadow_stats': '{n} 个快照,共 {mb} MB',
|
|
502
|
+
'detail.shadow_not_found': '.cursor-guard-backup/ 未找到(首次影子备份时自动创建)',
|
|
503
|
+
'detail.gitignore_ok': '.cursor-guard-backup/ 已被 git 忽略',
|
|
504
|
+
'detail.gitignore_missing': '.cursor-guard-backup/ 可能未被 git 忽略——备份变更可能触发提交',
|
|
505
|
+
'detail.invalid_value': '无效值 \'{v}\'',
|
|
506
|
+
'detail.pre_restore_never': '设为 \'never\'——恢复时不会自动保留当前版本',
|
|
507
|
+
'detail.interval_low': '{n} 秒低于最小值(5 秒),将被限制',
|
|
508
|
+
'detail.protect_count': '{matched} / {total} 个文件匹配保护规则',
|
|
509
|
+
'detail.disk_critical': '{gb} GB 可用——严重不足',
|
|
510
|
+
'detail.disk_free': '{gb} GB 可用',
|
|
511
|
+
'detail.disk_unknown': '无法检测可用空间',
|
|
512
|
+
'detail.lock_running': '守护进程运行中(pid={pid},启动于 {since})',
|
|
513
|
+
'detail.lock_stale': '残留锁文件(pid={pid} 已终止)——可安全删除或运行 doctor_fix',
|
|
514
|
+
'detail.lock_exists': '锁文件存在——可能有其他实例正在运行。{info}',
|
|
515
|
+
'detail.lock_none': '无锁文件(无运行中的实例)',
|
|
516
|
+
'detail.node_ok': '{v}',
|
|
517
|
+
'detail.node_old': '{v}——建议 >=18',
|
|
518
|
+
'detail.mcp_ok': 'server.js 已找到,SDK {v}',
|
|
519
|
+
'detail.mcp_no_sdk': 'server.js 已找到但 @modelcontextprotocol/sdk 未安装——请运行:cd <skill-dir>; npm install',
|
|
520
|
+
'detail.mcp_no_server': 'SDK 已安装({v})但 server.js 未在预期路径找到',
|
|
521
|
+
'detail.mcp_not_configured': 'MCP 未配置(可选——cursor-guard 无需 MCP 也能工作)',
|
|
522
|
+
'detail.mcp_version_mismatch': '运行中 v{mem},磁盘为 v{disk}——请重启 Cursor 加载新版本',
|
|
523
|
+
'detail.mcp_version_ok': 'v{v}',
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/* ── State ────────────────────────────────────────────────── */
|
|
528
|
+
|
|
529
|
+
const state = {
|
|
530
|
+
locale: 'en-US',
|
|
531
|
+
projects: [],
|
|
532
|
+
currentProjectId: null,
|
|
533
|
+
pageData: null,
|
|
534
|
+
filteredBackups: [],
|
|
535
|
+
backupFilter: 'all',
|
|
536
|
+
fileSearch: '',
|
|
537
|
+
refreshTimer: null,
|
|
538
|
+
tickTimer: null,
|
|
539
|
+
lastRefreshAt: null,
|
|
540
|
+
drawerOpen: null,
|
|
541
|
+
alertHistory: loadAlertHistory(),
|
|
542
|
+
alertExpiresAt: null,
|
|
543
|
+
/** Server-Sent Events for push refresh (see /api/events); closed on project switch */
|
|
544
|
+
eventSource: null,
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
/** Rare fallback poll if SSE misses (OS watcher gaps); primary updates come from EventSource. */
|
|
548
|
+
const FALLBACK_POLL_MS_VISIBLE = 180000;
|
|
549
|
+
const FALLBACK_POLL_MS_HIDDEN = 600000;
|
|
550
|
+
const ALERT_HISTORY_KEY = 'cursorGuard_alertHistory';
|
|
551
|
+
|
|
552
|
+
function loadAlertHistory() {
|
|
553
|
+
try {
|
|
554
|
+
const raw = localStorage.getItem(ALERT_HISTORY_KEY);
|
|
555
|
+
return raw ? JSON.parse(raw) : [];
|
|
556
|
+
} catch { return []; }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function saveAlertHistory() {
|
|
560
|
+
try { localStorage.setItem(ALERT_HISTORY_KEY, JSON.stringify(state.alertHistory)); } catch { /* quota */ }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ── DOM helpers ──────────────────────────────────────────── */
|
|
564
|
+
|
|
565
|
+
const $ = (s) => document.querySelector(s);
|
|
566
|
+
const $$ = (s) => document.querySelectorAll(s);
|
|
567
|
+
const show = (el) => el && el.classList.remove('hidden');
|
|
568
|
+
const hide = (el) => el && el.classList.add('hidden');
|
|
569
|
+
|
|
570
|
+
function esc(s) {
|
|
571
|
+
if (s == null) return '';
|
|
572
|
+
return String(s)
|
|
573
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
574
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* ── I18n helpers ─────────────────────────────────────────── */
|
|
578
|
+
|
|
579
|
+
function t(key, params) {
|
|
580
|
+
const dict = I18N[state.locale] || I18N['en-US'];
|
|
581
|
+
let text = dict[key] || I18N['en-US'][key] || key;
|
|
582
|
+
if (params) {
|
|
583
|
+
for (const [k, v] of Object.entries(params)) {
|
|
584
|
+
text = text.replaceAll(`{${k}}`, String(v));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return text;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function detectLocale() {
|
|
591
|
+
const saved = localStorage.getItem('cg-locale');
|
|
592
|
+
if (saved && I18N[saved]) return saved;
|
|
593
|
+
const nav = navigator.language || '';
|
|
594
|
+
return nav.startsWith('zh') ? 'zh-CN' : 'en-US';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function setLocale(loc) {
|
|
598
|
+
state.locale = loc;
|
|
599
|
+
localStorage.setItem('cg-locale', loc);
|
|
600
|
+
document.documentElement.lang = loc === 'zh-CN' ? 'zh-CN' : 'en';
|
|
601
|
+
document.title = t('app.title');
|
|
602
|
+
const refreshBtn = $('#refresh-btn');
|
|
603
|
+
if (refreshBtn) refreshBtn.title = t('topbar.refresh');
|
|
604
|
+
updateStaticI18n();
|
|
605
|
+
if (state.pageData) renderAll();
|
|
606
|
+
updateRefreshDisplay();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function updateStaticI18n() {
|
|
610
|
+
$$('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* ── Backend string translation ────────────────────────────── */
|
|
614
|
+
|
|
615
|
+
const ISSUE_PATTERNS = [
|
|
616
|
+
{ re: /^Auto-backup watcher is not running$/, key: 'issue.watcher_not_running' },
|
|
617
|
+
{ re: /^Watcher has a stale lock file/, key: 'issue.watcher_stale' },
|
|
618
|
+
{ re: /^Strategy requires Git but directory is not a git repo$/, key: 'issue.strategy_no_git' },
|
|
619
|
+
{ re: /^No auto-backup ref found/, key: 'issue.no_auto_backup_ref' },
|
|
620
|
+
{ re: /^Disk space critically low \((.+?) GB free\)$/, key: 'issue.disk_critically_low', extract: ['gb'] },
|
|
621
|
+
{ re: /^Disk space low \((.+?) GB free\)$/, key: 'issue.disk_low', extract: ['gb'] },
|
|
622
|
+
{ re: /^Last git backup is stale \((.+?)\)$/, key: 'issue.git_backup_stale', extract: ['rel'] },
|
|
623
|
+
{ re: /^Active alert: (.+?) — (\d+) files in (\d+)s$/, key: 'issue.active_alert', extract: ['type', 'count', 'window'] },
|
|
624
|
+
{ re: /^Pre-warning active: (.+)$/, key: 'issue.pre_warning_active', extract: ['summary'] },
|
|
625
|
+
{ re: /^High volume of file changes/, key: 'issue.alert_high_velocity' },
|
|
626
|
+
];
|
|
627
|
+
|
|
628
|
+
function translateIssue(text) {
|
|
629
|
+
for (const p of ISSUE_PATTERNS) {
|
|
630
|
+
const m = text.match(p.re);
|
|
631
|
+
if (m) {
|
|
632
|
+
const params = {};
|
|
633
|
+
if (p.extract) p.extract.forEach((k, i) => { params[k] = m[i + 1]; });
|
|
634
|
+
return t(p.key, params);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return text;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function translateCheckName(name) {
|
|
641
|
+
const key = 'check.' + name;
|
|
642
|
+
const translated = t(key);
|
|
643
|
+
return translated !== key ? translated : name;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const DETAIL_PATTERNS = [
|
|
647
|
+
{ re: /^version (.+)$/, key: 'detail.git_version', extract: ['v'] },
|
|
648
|
+
{ re: /^git not found in PATH/, key: 'detail.git_not_found' },
|
|
649
|
+
{ re: /^worktree detected \(git-dir: (.+)\)$/, key: 'detail.worktree', extract: ['dir'] },
|
|
650
|
+
{ re: /^standard repo$/, key: 'detail.standard_repo' },
|
|
651
|
+
{ re: /^not a Git repo/, key: 'detail.not_git_repo' },
|
|
652
|
+
{ re: /^\.cursor-guard\.json found and valid JSON$/, key: 'detail.config_valid' },
|
|
653
|
+
{ re: /^JSON parse error: (.+)$/, key: 'detail.config_parse_error', extract: ['err'] },
|
|
654
|
+
{ re: /^no \.cursor-guard\.json found/, key: 'detail.config_missing' },
|
|
655
|
+
{ re: /^backup_strategy='(.+?)' but directory is not a Git repo$/, key: 'detail.strategy_no_git', extract: ['s'] },
|
|
656
|
+
{ re: /^backup_strategy='(.+?)' and Git repo exists$/, key: 'detail.strategy_ok', extract: ['s'] },
|
|
657
|
+
{ re: /^backup_strategy='shadow'/, key: 'detail.strategy_shadow' },
|
|
658
|
+
{ re: /^unknown backup_strategy='(.+?)'/, key: 'detail.strategy_unknown', extract: ['s'] },
|
|
659
|
+
{ re: /^refs\/guard\/auto-backup exists \((.+?) commits?\)$/, key: 'detail.ref_exists', extract: ['n'] },
|
|
660
|
+
{ re: /^legacy refs\/heads\/cursor-guard\/auto-backup found \((.+?) commits?\)/,key: 'detail.ref_legacy', extract: ['n'] },
|
|
661
|
+
{ re: /^refs\/guard\/auto-backup not created yet/, key: 'detail.ref_not_created' },
|
|
662
|
+
{ re: /^(\d+) ref\(s\) found \((\d+) pre-restore snapshots?\)$/, key: 'detail.guard_refs_found', extract: ['n', 'pre'] },
|
|
663
|
+
{ re: /^no guard refs yet/, key: 'detail.guard_refs_none' },
|
|
664
|
+
{ re: /^(\d+) snapshot\(s\), (.+?) MB total$/, key: 'detail.shadow_stats', extract: ['n', 'mb'] },
|
|
665
|
+
{ re: /^\.cursor-guard-backup\/ not found/, key: 'detail.shadow_not_found' },
|
|
666
|
+
{ re: /^\.cursor-guard-backup\/ is git-ignored$/, key: 'detail.gitignore_ok' },
|
|
667
|
+
{ re: /^\.cursor-guard-backup\/ may NOT be git-ignored/, key: 'detail.gitignore_missing' },
|
|
668
|
+
{ re: /^invalid value '(.+)'$/, key: 'detail.invalid_value', extract: ['v'] },
|
|
669
|
+
{ re: /^set to 'never'/, key: 'detail.pre_restore_never' },
|
|
670
|
+
{ re: /^(\d+)s is below minimum/, key: 'detail.interval_low', extract: ['n'] },
|
|
671
|
+
{ re: /^(\d+) \/ (\d+) files matched by protect patterns$/, key: 'detail.protect_count', extract: ['matched', 'total'] },
|
|
672
|
+
{ re: /^(.+?) GB free — critically low$/, key: 'detail.disk_critical', extract: ['gb'] },
|
|
673
|
+
{ re: /^could not determine free space$/, key: 'detail.disk_unknown' },
|
|
674
|
+
{ re: /^(.+?) GB free$/, key: 'detail.disk_free', extract: ['gb'] },
|
|
675
|
+
{ re: /^watcher running \(pid=(\d+), since (.+)\)$/, key: 'detail.lock_running', extract: ['pid', 'since'] },
|
|
676
|
+
{ re: /^stale lock file \(pid=(\d+) is dead\)/, key: 'detail.lock_stale', extract: ['pid'] },
|
|
677
|
+
{ re: /^lock file exists — another instance may be running\. ?(.*)$/,key: 'detail.lock_exists', extract: ['info'] },
|
|
678
|
+
{ re: /^no lock file/, key: 'detail.lock_none' },
|
|
679
|
+
{ re: /^(v\d+\.\d+\.\d+\S*) — recommended >=18$/, key: 'detail.node_old', extract: ['v'] },
|
|
680
|
+
{ re: /^server\.js found, SDK (.+)$/, key: 'detail.mcp_ok', extract: ['v'] },
|
|
681
|
+
{ re: /^server\.js found but @modelcontextprotocol/, key: 'detail.mcp_no_sdk' },
|
|
682
|
+
{ re: /^SDK installed \((.+?)\) but server\.js/, key: 'detail.mcp_no_server', extract: ['v'] },
|
|
683
|
+
{ re: /^MCP not configured/, key: 'detail.mcp_not_configured' },
|
|
684
|
+
{ re: /^running v(.+?) but disk has v(.+?) —/, key: 'detail.mcp_version_mismatch', extract: ['mem', 'disk'] },
|
|
685
|
+
{ re: /^v(\d+\.\d+\.\d+\S*)$/, key: 'detail.mcp_version_ok', extract: ['v'] },
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
function translateDetail(text) {
|
|
689
|
+
if (!text) return text;
|
|
690
|
+
for (const p of DETAIL_PATTERNS) {
|
|
691
|
+
const m = text.match(p.re);
|
|
692
|
+
if (m) {
|
|
693
|
+
const params = {};
|
|
694
|
+
if (p.extract) p.extract.forEach((k, i) => { params[k] = m[i + 1]; });
|
|
695
|
+
return t(p.key, params);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return text;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* ── Time helpers ─────────────────────────────────────────── */
|
|
702
|
+
|
|
703
|
+
function parseShadowTs(ts) {
|
|
704
|
+
if (!ts) return null;
|
|
705
|
+
const m = String(ts).match(/^(?:pre-restore-)?(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);
|
|
706
|
+
if (!m) return null;
|
|
707
|
+
return new Date(`${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function toDate(ts) {
|
|
711
|
+
if (!ts) return null;
|
|
712
|
+
let d = new Date(ts);
|
|
713
|
+
if (!isNaN(d.getTime())) return d;
|
|
714
|
+
d = parseShadowTs(ts);
|
|
715
|
+
return d && !isNaN(d.getTime()) ? d : null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function formatTime(ts) {
|
|
719
|
+
const d = toDate(ts);
|
|
720
|
+
if (!d) return ts || '-';
|
|
721
|
+
return new Intl.DateTimeFormat(state.locale, {
|
|
722
|
+
month: 'short', day: 'numeric',
|
|
723
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
724
|
+
}).format(d);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function relativeTime(ts) {
|
|
728
|
+
const d = toDate(ts);
|
|
729
|
+
if (!d) return '';
|
|
730
|
+
const ms = Date.now() - d.getTime();
|
|
731
|
+
if (ms < 0) return t('time.justNow');
|
|
732
|
+
const sec = Math.floor(ms / 1000);
|
|
733
|
+
if (sec < 60) return t('time.secondsAgo', { n: sec });
|
|
734
|
+
const min = Math.floor(sec / 60);
|
|
735
|
+
if (min < 60) return t('time.minutesAgo', { n: min });
|
|
736
|
+
const hr = Math.floor(min / 60);
|
|
737
|
+
if (hr < 24) return t('time.hoursAgo', { n: hr });
|
|
738
|
+
return t('time.daysAgo', { n: Math.floor(hr / 24) });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/* ── Data fetching ────────────────────────────────────────── */
|
|
742
|
+
|
|
743
|
+
let _fetchFailCount = 0;
|
|
744
|
+
async function fetchJson(url) {
|
|
745
|
+
const base = window.__GUARD_BASE_URL__ || '';
|
|
746
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
747
|
+
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
|
|
748
|
+
try {
|
|
749
|
+
const r = await fetch(base + url + tokenParam);
|
|
750
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
751
|
+
_fetchFailCount = 0;
|
|
752
|
+
return r.json();
|
|
753
|
+
} catch (err) {
|
|
754
|
+
_fetchFailCount++;
|
|
755
|
+
if (window.__IN_VSCODE__ && _fetchFailCount >= 3) {
|
|
756
|
+
try {
|
|
757
|
+
const api = window.__vscodeApi || (window.__vscodeApi = acquireVsCodeApi());
|
|
758
|
+
api.postMessage({ type: 'fetchError', detail: err.message });
|
|
759
|
+
} catch { /* ignore */ }
|
|
760
|
+
}
|
|
761
|
+
throw err;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** POST JSON API (same token query as GET). */
|
|
766
|
+
async function postJson(url) {
|
|
767
|
+
const base = window.__GUARD_BASE_URL__ || '';
|
|
768
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
769
|
+
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${encodeURIComponent(window.__GUARD_TOKEN__)}` : '';
|
|
770
|
+
const r = await fetch(base + url + tokenParam, { method: 'POST' });
|
|
771
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
772
|
+
_fetchFailCount = 0;
|
|
773
|
+
return r.json();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function dismissActiveAlert() {
|
|
777
|
+
if (!state.currentProjectId) return;
|
|
778
|
+
const btn = document.querySelector('[data-dismiss-active-alert]');
|
|
779
|
+
if (btn) {
|
|
780
|
+
btn.disabled = true;
|
|
781
|
+
btn.textContent = t('alert.dismissBusy');
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
await postJson(`/api/dismiss-alert?id=${encodeURIComponent(state.currentProjectId)}`);
|
|
785
|
+
await loadPageData();
|
|
786
|
+
renderAll();
|
|
787
|
+
} catch (e) {
|
|
788
|
+
showGlobalError(e.message || String(e));
|
|
789
|
+
} finally {
|
|
790
|
+
if (btn) {
|
|
791
|
+
btn.disabled = false;
|
|
792
|
+
btn.textContent = t('alert.dismiss');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function loadProjects() {
|
|
798
|
+
state.projects = await fetchJson('/api/projects');
|
|
799
|
+
if (state.projects.length > 0 && !state.currentProjectId) {
|
|
800
|
+
state.currentProjectId = state.projects[0].id;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function loadPageData(opts = {}) {
|
|
805
|
+
if (!state.currentProjectId) return;
|
|
806
|
+
const id = state.currentProjectId;
|
|
807
|
+
|
|
808
|
+
if (opts.progressive) {
|
|
809
|
+
state.pageData = { dashboard: null, doctor: null, backups: null };
|
|
810
|
+
const dashPromise = fetchJson(`/api/page-data?id=${id}&scope=dashboard`);
|
|
811
|
+
const restPromise = Promise.allSettled([
|
|
812
|
+
fetchJson(`/api/page-data?id=${id}&scope=backups`),
|
|
813
|
+
fetchJson(`/api/page-data?id=${id}&scope=doctor`),
|
|
814
|
+
]);
|
|
815
|
+
|
|
816
|
+
const dash = await dashPromise;
|
|
817
|
+
state.pageData.dashboard = dash.dashboard;
|
|
818
|
+
state.lastRefreshAt = Date.now();
|
|
819
|
+
showContent();
|
|
820
|
+
if (dash.dashboard && !dash.dashboard.error) {
|
|
821
|
+
renderStrategyBadge(dash.dashboard.strategy);
|
|
822
|
+
renderOverview(dash.dashboard);
|
|
823
|
+
renderProtection(dash.dashboard.protectionScope);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const [backupsResult, doctorResult] = await restPromise;
|
|
827
|
+
if (backupsResult.status === 'fulfilled') {
|
|
828
|
+
state.pageData.backups = backupsResult.value.backups;
|
|
829
|
+
if (state.pageData.dashboard) {
|
|
830
|
+
renderBackupsSection(state.pageData.dashboard, Array.isArray(state.pageData.backups) ? state.pageData.backups : []);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (doctorResult.status === 'fulfilled') {
|
|
834
|
+
state.pageData.doctor = doctorResult.value.doctor;
|
|
835
|
+
if (state.pageData.doctor && !state.pageData.doctor.error) {
|
|
836
|
+
renderDiagnostics(state.pageData.doctor);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
state.pageData = await fetchJson(`/api/page-data?id=${state.currentProjectId}`);
|
|
841
|
+
state.lastRefreshAt = Date.now();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* ── Refresh ──────────────────────────────────────────────── */
|
|
846
|
+
|
|
847
|
+
function fallbackPollIntervalMs() {
|
|
848
|
+
return (typeof document !== 'undefined' && document.hidden) ? FALLBACK_POLL_MS_HIDDEN : FALLBACK_POLL_MS_VISIBLE;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function startRefresh() {
|
|
852
|
+
stopRefresh();
|
|
853
|
+
state.refreshTimer = setInterval(async () => {
|
|
854
|
+
try { await loadPageData(); renderAll(); } catch { /* keep existing */ }
|
|
855
|
+
}, fallbackPollIntervalMs());
|
|
856
|
+
state.tickTimer = setInterval(updateRefreshDisplay, 1000);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function stopRefresh() {
|
|
860
|
+
if (state.refreshTimer) { clearInterval(state.refreshTimer); state.refreshTimer = null; }
|
|
861
|
+
if (state.tickTimer) { clearInterval(state.tickTimer); state.tickTimer = null; }
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function disconnectLivePush() {
|
|
865
|
+
if (state.eventSource) {
|
|
866
|
+
try { state.eventSource.close(); } catch { /* ignore */ }
|
|
867
|
+
state.eventSource = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Subscribe to server push (fs.watch → SSE). No token in page = skip (dev mode). */
|
|
872
|
+
function connectLivePush() {
|
|
873
|
+
disconnectLivePush();
|
|
874
|
+
if (!state.currentProjectId || typeof EventSource === 'undefined') return;
|
|
875
|
+
if (!window.__GUARD_TOKEN__) return;
|
|
876
|
+
const base = window.__GUARD_BASE_URL__ || '';
|
|
877
|
+
const id = encodeURIComponent(state.currentProjectId);
|
|
878
|
+
const tk = encodeURIComponent(window.__GUARD_TOKEN__);
|
|
879
|
+
const url = `${base}/api/events?id=${id}&token=${tk}`;
|
|
880
|
+
try {
|
|
881
|
+
const es = new EventSource(url);
|
|
882
|
+
state.eventSource = es;
|
|
883
|
+
es.onmessage = (ev) => {
|
|
884
|
+
try {
|
|
885
|
+
const msg = JSON.parse(ev.data);
|
|
886
|
+
if (msg.type === 'guard-changed' && msg.projectId === state.currentProjectId) {
|
|
887
|
+
void (async () => {
|
|
888
|
+
try {
|
|
889
|
+
await loadPageData();
|
|
890
|
+
renderAll();
|
|
891
|
+
} catch { /* keep existing UI */ }
|
|
892
|
+
})();
|
|
893
|
+
}
|
|
894
|
+
} catch { /* ignore non-json */ }
|
|
895
|
+
};
|
|
896
|
+
} catch { /* ignore */ }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function manualRefresh() {
|
|
900
|
+
const icon = $('#refresh-btn .icon-spin');
|
|
901
|
+
if (icon) icon.classList.add('icon-spin-active');
|
|
902
|
+
stopRefresh();
|
|
903
|
+
try { await loadPageData(); renderAll(); }
|
|
904
|
+
catch (e) { showGlobalError(e.message); }
|
|
905
|
+
if (icon) icon.classList.remove('icon-spin-active');
|
|
906
|
+
startRefresh();
|
|
907
|
+
connectLivePush();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function updateRefreshDisplay() {
|
|
911
|
+
const el = $('#last-refresh');
|
|
912
|
+
if (el && state.lastRefreshAt) {
|
|
913
|
+
const sec = Math.floor((Date.now() - state.lastRefreshAt) / 1000);
|
|
914
|
+
el.textContent = `${t('topbar.lastRefresh')}: ${sec}s`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const cdEl = document.querySelector('.alert-countdown');
|
|
918
|
+
if (cdEl && state.alertExpiresAt) {
|
|
919
|
+
const remain = Math.max(0, state.alertExpiresAt - Date.now());
|
|
920
|
+
const s = Math.ceil(remain / 1000);
|
|
921
|
+
cdEl.textContent = s > 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/* ── Rendering: Top bar ───────────────────────────────────── */
|
|
926
|
+
|
|
927
|
+
function renderProjectSelect() {
|
|
928
|
+
const sel = $('#project-select');
|
|
929
|
+
sel.innerHTML = state.projects.map(p =>
|
|
930
|
+
`<option value="${esc(p.id)}" title="${esc(p.pathLabel)}">${esc(p.name)}</option>`
|
|
931
|
+
).join('');
|
|
932
|
+
if (state.currentProjectId) sel.value = state.currentProjectId;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function renderStrategyBadge(strategy) {
|
|
936
|
+
const el = $('#strategy-badge');
|
|
937
|
+
el.textContent = t('strategy.' + (strategy || 'git'));
|
|
938
|
+
el.className = 'badge badge-strategy';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* ── Rendering: Global states ─────────────────────────────── */
|
|
942
|
+
|
|
943
|
+
function showSkeleton() {
|
|
944
|
+
hide($('#error-state'));
|
|
945
|
+
$$('.screen').forEach(s => show(s));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function showGlobalError(msg) {
|
|
949
|
+
show($('#error-state'));
|
|
950
|
+
$$('.screen').forEach(s => hide(s));
|
|
951
|
+
$('#error-message').textContent = msg || t('error.fetchFailed');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function showContent() {
|
|
955
|
+
hide($('#error-state'));
|
|
956
|
+
$$('.screen').forEach(s => show(s));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/* ── Rendering: Main dispatch ─────────────────────────────── */
|
|
960
|
+
|
|
961
|
+
function renderAll() {
|
|
962
|
+
if (!state.pageData) return;
|
|
963
|
+
const { dashboard, doctor, backups } = state.pageData;
|
|
964
|
+
|
|
965
|
+
showContent();
|
|
966
|
+
|
|
967
|
+
if (dashboard && !dashboard.error) {
|
|
968
|
+
renderStrategyBadge(dashboard.strategy);
|
|
969
|
+
renderOverview(dashboard);
|
|
970
|
+
renderBackupsSection(dashboard, Array.isArray(backups) ? backups : []);
|
|
971
|
+
renderProtection(dashboard.protectionScope);
|
|
972
|
+
} else {
|
|
973
|
+
renderSectionError('overview-grid', dashboard?.error);
|
|
974
|
+
renderSectionError('backup-stats', dashboard?.error);
|
|
975
|
+
renderSectionError('protection-content', dashboard?.error);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (doctor && !doctor.error) {
|
|
979
|
+
renderDiagnostics(doctor);
|
|
980
|
+
} else {
|
|
981
|
+
renderSectionError('diagnostics-summary', doctor?.error);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
updateStaticI18n();
|
|
985
|
+
updateRefreshDisplay();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/* ── Rendering: Overview ──────────────────────────────────── */
|
|
989
|
+
|
|
990
|
+
function renderOverview(d) {
|
|
991
|
+
renderHealthCard(d.health);
|
|
992
|
+
renderGitBackupCard(d.lastBackup);
|
|
993
|
+
renderShadowBackupCard(d.lastBackup);
|
|
994
|
+
renderPreWarningCard(d.preWarnings);
|
|
995
|
+
renderWatcherCard(d.watcher);
|
|
996
|
+
renderAlertCard(d.alerts);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function renderHealthCard(health) {
|
|
1000
|
+
const el = $('#card-health');
|
|
1001
|
+
const st = health?.status || 'unknown';
|
|
1002
|
+
const issues = health?.issues || [];
|
|
1003
|
+
el.className = `card card-health`;
|
|
1004
|
+
el.style.borderLeft = `3px solid var(--${st === 'healthy' ? 'green' : st === 'warning' ? 'yellow' : st === 'critical' ? 'red' : 'gray'})`;
|
|
1005
|
+
el.innerHTML = `
|
|
1006
|
+
<div class="card-status">
|
|
1007
|
+
<span class="status-dot status-${st}"></span>
|
|
1008
|
+
<span class="status-text status-${st}">${t('health.' + st)}</span>
|
|
1009
|
+
</div>
|
|
1010
|
+
${issues.length > 0 ? `<ul class="issue-list">${issues.map(i => `<li class="text-sm">${esc(translateIssue(i))}</li>`).join('')}</ul>` : ''}
|
|
1011
|
+
`;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function renderGitBackupCard(lastBackup) {
|
|
1015
|
+
const el = $('#card-git-backup');
|
|
1016
|
+
const g = lastBackup?.git;
|
|
1017
|
+
if (!g) {
|
|
1018
|
+
el.innerHTML = `<div class="card-label">${t('gitBackup.title')}</div><div class="card-empty">${t('gitBackup.none')}</div>`;
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
el.innerHTML = `
|
|
1022
|
+
<div class="card-label">${t('gitBackup.title')}</div>
|
|
1023
|
+
<div class="card-value">${esc(relativeTime(g.timestamp))}</div>
|
|
1024
|
+
<div class="card-detail text-muted">
|
|
1025
|
+
<span class="text-mono">${esc(g.shortHash)}</span> · <span class="text-sm">${esc(formatTime(g.timestamp))}</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function renderShadowBackupCard(lastBackup) {
|
|
1031
|
+
const el = $('#card-shadow-backup');
|
|
1032
|
+
const s = lastBackup?.shadow;
|
|
1033
|
+
if (!s) {
|
|
1034
|
+
el.innerHTML = `<div class="card-label">${t('shadowBackup.title')}</div><div class="card-empty">${t('shadowBackup.none')}</div>`;
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
el.innerHTML = `
|
|
1038
|
+
<div class="card-label">${t('shadowBackup.title')}</div>
|
|
1039
|
+
<div class="card-value">${esc(relativeTime(s.timestamp))}</div>
|
|
1040
|
+
<div class="card-detail text-muted text-sm">${esc(formatTime(s.timestamp))}</div>
|
|
1041
|
+
`;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function renderPreWarningCard(preWarnings) {
|
|
1045
|
+
const el = $('#card-prewarning');
|
|
1046
|
+
if (!preWarnings?.active) {
|
|
1047
|
+
el.innerHTML = `
|
|
1048
|
+
<div class="card-label">${t('preWarning.title')}</div>
|
|
1049
|
+
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('preWarning.none')}</span></div>
|
|
1050
|
+
`;
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const warning = preWarnings.latest || {};
|
|
1055
|
+
const file = warning.file || '-';
|
|
1056
|
+
const risk = warning.riskPercent !== undefined ? `${warning.riskPercent}%` : '-';
|
|
1057
|
+
const methods = warning.removedMethodCount || 0;
|
|
1058
|
+
|
|
1059
|
+
el.innerHTML = `
|
|
1060
|
+
<div class="card-label">${t('preWarning.title')}</div>
|
|
1061
|
+
<div class="card-status"><span class="status-dot status-warning"></span><span class="status-text status-warning">${t('preWarning.active')}</span></div>
|
|
1062
|
+
<div class="alert-details">
|
|
1063
|
+
<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.file')}</span><span class="text-mono">${esc(file)}</span></div>
|
|
1064
|
+
<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.risk')}</span><span>${esc(risk)}</span></div>
|
|
1065
|
+
${methods > 0 ? `<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.methods')}</span><span>${methods}</span></div>` : ''}
|
|
1066
|
+
<div class="alert-detail-row alert-breakdown text-sm">${esc(warning.summary || t('preWarning.suggestion'))}</div>
|
|
1067
|
+
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('preWarning.suggestion')}</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function renderWatcherCard(watcher) {
|
|
1073
|
+
const el = $('#card-watcher');
|
|
1074
|
+
let st = 'stopped';
|
|
1075
|
+
if (watcher?.running) st = 'running';
|
|
1076
|
+
else if (watcher?.stale) st = 'stale';
|
|
1077
|
+
const lastScan = state.lastRefreshAt ? relativeTime(new Date(state.lastRefreshAt).toISOString()) : null;
|
|
1078
|
+
el.innerHTML = `
|
|
1079
|
+
<div class="card-label">${t('watcher.title')}</div>
|
|
1080
|
+
<div class="card-status">
|
|
1081
|
+
<span class="status-dot status-${st}"></span>
|
|
1082
|
+
<span>${t('watcher.' + st)}</span>
|
|
1083
|
+
</div>
|
|
1084
|
+
${watcher?.pid ? `<div class="card-detail text-muted text-sm">${t('watcher.pid')}: ${watcher.pid}</div>` : ''}
|
|
1085
|
+
${watcher?.startedAt ? `<div class="card-detail text-muted text-sm">${t('watcher.since')}: ${esc(formatTime(watcher.startedAt))}</div>` : ''}
|
|
1086
|
+
${watcher?.running && lastScan ? `<div class="card-detail text-muted text-sm">${t('watcher.lastScan')}: ${esc(lastScan)}</div>` : ''}
|
|
1087
|
+
`;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function alertFileBreakdown(files) {
|
|
1091
|
+
if (!Array.isArray(files) || files.length === 0) return '';
|
|
1092
|
+
let added = 0, modified = 0, deleted = 0;
|
|
1093
|
+
for (const f of files) {
|
|
1094
|
+
if (f.action === 'added') added++;
|
|
1095
|
+
else if (f.action === 'deleted') deleted++;
|
|
1096
|
+
else modified++;
|
|
1097
|
+
}
|
|
1098
|
+
return t('alert.breakdown', { added, modified, deleted });
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function buildAlertHistoryHtml() {
|
|
1102
|
+
if (state.alertHistory.length === 0) return '';
|
|
1103
|
+
const count = state.alertHistory.length;
|
|
1104
|
+
return `
|
|
1105
|
+
<div class="alert-history-toggle-wrap">
|
|
1106
|
+
<button class="alert-history-toggle-btn text-sm text-muted" data-alert-history-modal>${t('alert.historyCount', { n: count })}</button>
|
|
1107
|
+
</div>`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function openAlertHistoryModal() {
|
|
1111
|
+
const list = [...state.alertHistory].reverse();
|
|
1112
|
+
if (list.length === 0) return;
|
|
1113
|
+
$('#file-modal-title').textContent = t('alert.history');
|
|
1114
|
+
const body = $('#file-modal-body');
|
|
1115
|
+
|
|
1116
|
+
const rows = list.map((h, i) => {
|
|
1117
|
+
const breakdown = alertFileBreakdown(h.files);
|
|
1118
|
+
const fileCount = Array.isArray(h.files) ? h.files.length : 0;
|
|
1119
|
+
return `<tr>
|
|
1120
|
+
<td class="text-mono">${esc(formatTime(h.timestamp))}</td>
|
|
1121
|
+
<td>${t('alert.detail', { count: h.fileCount, window: h.windowSeconds, threshold: h.threshold })}</td>
|
|
1122
|
+
<td>${breakdown ? esc(breakdown) : '-'}</td>
|
|
1123
|
+
<td>${fileCount > 0 ? `<button class="modal-restore-btn" data-history-files="${i}">${t('alert.viewFiles', { n: fileCount })}</button>` : '-'}</td>
|
|
1124
|
+
</tr>`;
|
|
1125
|
+
}).join('');
|
|
1126
|
+
|
|
1127
|
+
body.innerHTML = `<table>
|
|
1128
|
+
<thead><tr>
|
|
1129
|
+
<th>${t('alert.triggered')}</th>
|
|
1130
|
+
<th>${t('alert.col.detail')}</th>
|
|
1131
|
+
<th>${t('alert.col.breakdown')}</th>
|
|
1132
|
+
<th>${t('alert.col.files')}</th>
|
|
1133
|
+
</tr></thead>
|
|
1134
|
+
<tbody>${rows}</tbody>
|
|
1135
|
+
</table>`;
|
|
1136
|
+
|
|
1137
|
+
body.addEventListener('click', (e) => {
|
|
1138
|
+
const btn = e.target.closest('[data-history-files]');
|
|
1139
|
+
if (btn) {
|
|
1140
|
+
const idx = parseInt(btn.dataset.historyFiles);
|
|
1141
|
+
const h = list[idx];
|
|
1142
|
+
if (h?.files?.length > 0) {
|
|
1143
|
+
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
1144
|
+
openFileModal(t('modal.alertFiles'), h.files, proj, '');
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
$('#file-modal-overlay').classList.add('active');
|
|
1150
|
+
document.body.style.overflow = 'hidden';
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function renderAlertCard(alerts) {
|
|
1154
|
+
const el = $('#card-alert');
|
|
1155
|
+
if (!alerts?.active) {
|
|
1156
|
+
state.alertExpiresAt = null;
|
|
1157
|
+
el.innerHTML = `
|
|
1158
|
+
<div class="card-label">${t('alert.title')}</div>
|
|
1159
|
+
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('alert.none')}</span></div>
|
|
1160
|
+
${buildAlertHistoryHtml()}
|
|
1161
|
+
`;
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const a = alerts.latest || {};
|
|
1165
|
+
|
|
1166
|
+
// Track in history
|
|
1167
|
+
if (a.timestamp && !state.alertHistory.some(h => h.timestamp === a.timestamp)) {
|
|
1168
|
+
state.alertHistory.push({ timestamp: a.timestamp, fileCount: a.fileCount, windowSeconds: a.windowSeconds, threshold: a.threshold, expiresAt: a.expiresAt, files: a.files });
|
|
1169
|
+
if (state.alertHistory.length > 20) state.alertHistory = state.alertHistory.slice(-20);
|
|
1170
|
+
saveAlertHistory();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const triggeredAt = a.timestamp ? formatTime(a.timestamp) : '-';
|
|
1174
|
+
const expiresAt = a.expiresAt ? new Date(a.expiresAt) : null;
|
|
1175
|
+
state.alertExpiresAt = expiresAt ? expiresAt.getTime() : null;
|
|
1176
|
+
const remainMs = expiresAt ? expiresAt.getTime() - Date.now() : 0;
|
|
1177
|
+
const remainSec = Math.max(0, Math.ceil(remainMs / 1000));
|
|
1178
|
+
const remainMin = Math.floor(remainSec / 60);
|
|
1179
|
+
const remainDisplay = remainMin > 0 ? `${remainMin}m ${remainSec % 60}s` : `${remainSec}s`;
|
|
1180
|
+
const detailText = t('alert.detail', { count: a.fileCount || '?', window: a.windowSeconds || '?', threshold: a.threshold || '?' });
|
|
1181
|
+
|
|
1182
|
+
const files = Array.isArray(a.files) ? a.files : [];
|
|
1183
|
+
let filesHtml = '';
|
|
1184
|
+
if (files.length > 0) {
|
|
1185
|
+
filesHtml = `
|
|
1186
|
+
<div class="alert-files-section">
|
|
1187
|
+
<button class="alert-files-toggle" data-alert-files-modal>${t('alert.viewFiles', { n: files.length })}</button>
|
|
1188
|
+
</div>
|
|
1189
|
+
`;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
el.innerHTML = `
|
|
1193
|
+
<div class="card-label">${t('alert.title')}</div>
|
|
1194
|
+
<div class="card-status"><span class="status-dot status-warning"></span><span class="status-text status-warning">${t('alert.active')}</span></div>
|
|
1195
|
+
<div class="alert-details">
|
|
1196
|
+
<div class="alert-detail-row"><span class="alert-detail-label">${t('alert.triggered')}</span><span>${esc(triggeredAt)}</span></div>
|
|
1197
|
+
<div class="alert-detail-row"><span class="alert-detail-label">${t('alert.expires')}</span><span class="alert-countdown">${esc(remainDisplay)}</span></div>
|
|
1198
|
+
<div class="alert-detail-row alert-numbers">${esc(detailText)}</div>
|
|
1199
|
+
${alertFileBreakdown(files) ? `<div class="alert-detail-row alert-breakdown text-sm">${esc(alertFileBreakdown(files))}</div>` : ''}
|
|
1200
|
+
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('alert.suggestion')}</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
<div class="alert-card-actions">
|
|
1203
|
+
${filesHtml}
|
|
1204
|
+
<button type="button" class="btn-alert-dismiss" data-dismiss-active-alert>${t('alert.dismiss')}</button>
|
|
1205
|
+
</div>
|
|
1206
|
+
${buildAlertHistoryHtml()}
|
|
1207
|
+
`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/* ── Rendering: Backups Section ───────────────────────────── */
|
|
1211
|
+
|
|
1212
|
+
function renderBackupsSection(dashboard, backups) {
|
|
1213
|
+
renderBackupStats(dashboard, backups);
|
|
1214
|
+
renderFilterBar(backups);
|
|
1215
|
+
renderFileSearch();
|
|
1216
|
+
renderBackupTable(backups);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function renderFileSearch() {
|
|
1220
|
+
const el = $('#file-search-wrap');
|
|
1221
|
+
if (!el) return;
|
|
1222
|
+
el.innerHTML = `<input id="file-search" type="text" class="file-search-input" placeholder="${t('backups.search')}" value="${esc(state.fileSearch)}" />`;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function renderBackupStats(d, backups) {
|
|
1226
|
+
const gitCount = d.counts?.git?.commits || 0;
|
|
1227
|
+
const shadowCount = d.counts?.shadow?.snapshots || 0;
|
|
1228
|
+
const preRestoreCount = Array.isArray(backups)
|
|
1229
|
+
? backups.filter(b => b.type === 'git-pre-restore' || b.type === 'shadow-pre-restore').length
|
|
1230
|
+
: 0;
|
|
1231
|
+
const gitDisk = d.diskUsage?.git?.display || '0B';
|
|
1232
|
+
const shadowDisk = d.diskUsage?.shadow?.display || '0B';
|
|
1233
|
+
|
|
1234
|
+
$('#backup-stats').innerHTML = `
|
|
1235
|
+
<div class="stat-card"><div class="stat-label">${t('backups.gitCommits')}</div><div class="stat-value">${gitCount}</div></div>
|
|
1236
|
+
<div class="stat-card"><div class="stat-label">${t('backups.shadowSnapshots')}</div><div class="stat-value">${shadowCount}</div></div>
|
|
1237
|
+
<div class="stat-card"><div class="stat-label">${t('backups.preRestore')}</div><div class="stat-value">${preRestoreCount}</div></div>
|
|
1238
|
+
<div class="stat-card"><div class="stat-label">${t('backups.gitDisk')}</div><div class="stat-value">${esc(gitDisk)}</div></div>
|
|
1239
|
+
<div class="stat-card"><div class="stat-label">${t('backups.shadowDisk')}</div><div class="stat-value">${esc(shadowDisk)}</div></div>
|
|
1240
|
+
`;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function renderFilterBar(backups) {
|
|
1244
|
+
const allBackups = Array.isArray(backups) ? backups : (Array.isArray(state.pageData?.backups) ? state.pageData.backups : []);
|
|
1245
|
+
const typeCounts = {};
|
|
1246
|
+
for (const b of allBackups) { typeCounts[b.type] = (typeCounts[b.type] || 0) + 1; }
|
|
1247
|
+
|
|
1248
|
+
const types = [
|
|
1249
|
+
{ key: 'all', label: 'backups.filterAll' },
|
|
1250
|
+
{ key: 'git-auto-backup', label: 'type.git-auto-backup' },
|
|
1251
|
+
{ key: 'git-pre-restore', label: 'type.git-pre-restore' },
|
|
1252
|
+
{ key: 'git-snapshot', label: 'type.git-snapshot' },
|
|
1253
|
+
{ key: 'shadow', label: 'type.shadow' },
|
|
1254
|
+
{ key: 'shadow-pre-restore',label: 'type.shadow-pre-restore' },
|
|
1255
|
+
];
|
|
1256
|
+
const total = allBackups.length;
|
|
1257
|
+
$('#backup-filters').innerHTML = types.map(t2 => {
|
|
1258
|
+
const count = t2.key === 'all' ? total : (typeCounts[t2.key] || 0);
|
|
1259
|
+
if (t2.key !== 'all' && count === 0) return '';
|
|
1260
|
+
return `<button class="filter-btn ${state.backupFilter === t2.key ? 'active' : ''}" data-filter="${t2.key}">${t(t2.label)} <span class="filter-count">(${count})</span></button>`;
|
|
1261
|
+
}).join('');
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function translateSummary(raw) {
|
|
1265
|
+
if (!raw) return raw;
|
|
1266
|
+
return raw
|
|
1267
|
+
.replace(/\bModified (\d+)/g, (_, n) => `${t('summary.modified')} ${n}`)
|
|
1268
|
+
.replace(/\bAdded (\d+)/g, (_, n) => `${t('summary.added')} ${n}`)
|
|
1269
|
+
.replace(/\bDeleted (\d+)/g, (_, n) => `${t('summary.deleted')} ${n}`)
|
|
1270
|
+
.replace(/\bRenamed (\d+)/g, (_, n) => `${t('summary.renamed')} ${n}`);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** Strip CR / normalize newlines so per-line trailers and Summary parse reliably on Windows. */
|
|
1274
|
+
function normalizeSummaryText(summary) {
|
|
1275
|
+
if (!summary) return '';
|
|
1276
|
+
return String(summary).replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Sum all (+added -deleted) hunks in a Guard Summary line (same semantics as inline file rows).
|
|
1281
|
+
* @returns {{ added: number, deleted: number, matches: number }|null}
|
|
1282
|
+
*/
|
|
1283
|
+
function sumDeltaFromSummary(summary) {
|
|
1284
|
+
const s = normalizeSummaryText(summary);
|
|
1285
|
+
if (!s) return null;
|
|
1286
|
+
let added = 0;
|
|
1287
|
+
let deleted = 0;
|
|
1288
|
+
let matches = 0;
|
|
1289
|
+
const re = /\(\+(\d+)\s*-\s*(\d+)\)/g;
|
|
1290
|
+
let m;
|
|
1291
|
+
while ((m = re.exec(s)) !== null) {
|
|
1292
|
+
added += parseInt(m[1], 10) || 0;
|
|
1293
|
+
deleted += parseInt(m[2], 10) || 0;
|
|
1294
|
+
matches++;
|
|
1295
|
+
}
|
|
1296
|
+
if (matches === 0) return null;
|
|
1297
|
+
return { added, deleted, matches };
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Parse summary text into structured file array for inline preview.
|
|
1302
|
+
* Format: "Modified 3: a.js (+2 -1), b.js (+0 -5), ...; Added 2: c.js (+10 -0), d.js (+3 -0)"
|
|
1303
|
+
*/
|
|
1304
|
+
function parseSummaryToFiles(summary) {
|
|
1305
|
+
const normalized = normalizeSummaryText(summary);
|
|
1306
|
+
if (!normalized) return [];
|
|
1307
|
+
const ACTION_MAP = { Modified: 'modified', Added: 'added', Deleted: 'deleted', Renamed: 'renamed' };
|
|
1308
|
+
const files = [];
|
|
1309
|
+
for (const segment of normalized.split('; ')) {
|
|
1310
|
+
const seg = segment.trim();
|
|
1311
|
+
const headerMatch = seg.match(/^(Modified|Added|Deleted|Renamed)\s+\d+:\s*/);
|
|
1312
|
+
if (!headerMatch) continue;
|
|
1313
|
+
const action = ACTION_MAP[headerMatch[1]] || 'modified';
|
|
1314
|
+
const rest = seg.slice(headerMatch[0].length);
|
|
1315
|
+
for (const part of rest.split(/,\s*/)) {
|
|
1316
|
+
const p = part.trim();
|
|
1317
|
+
if (!p || p === '...') continue;
|
|
1318
|
+
const fileMatch = p.match(/^(.+?)\s*\(\+(\d+)\s*-\s*(\d+)\)\s*$/);
|
|
1319
|
+
if (fileMatch) {
|
|
1320
|
+
files.push({ path: fileMatch[1].trim(), action, added: parseInt(fileMatch[2], 10), deleted: parseInt(fileMatch[3], 10) });
|
|
1321
|
+
} else {
|
|
1322
|
+
files.push({ path: p, action, added: 0, deleted: 0 });
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
files.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
|
|
1327
|
+
return files;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
async function fetchBackupFiles(commitHash) {
|
|
1331
|
+
if (!state.currentProjectId || !commitHash) return [];
|
|
1332
|
+
try {
|
|
1333
|
+
const data = await fetchJson(`/api/backup-files?id=${state.currentProjectId}&hash=${commitHash}`);
|
|
1334
|
+
return Array.isArray(data.files) ? data.files : [];
|
|
1335
|
+
} catch { return []; }
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function formatFileActionBadge(action) {
|
|
1339
|
+
const cls = action === 'deleted' ? 'alert-action-deleted'
|
|
1340
|
+
: action === 'added' ? 'alert-action-added'
|
|
1341
|
+
: action === 'renamed' ? 'alert-action-renamed'
|
|
1342
|
+
: 'alert-action-modified';
|
|
1343
|
+
return `<span class="alert-action-badge ${cls}">${t('alert.action.' + action)}</span>`;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function formatSummaryCell(b) {
|
|
1347
|
+
const bookmarkHtml = b.guardBookmark
|
|
1348
|
+
? `<div class="summary-bookmark-row"><span class="summary-bookmark-badge" title="${esc(t('backups.bookmarkTitle'))}">${esc(t('backups.bookmarkBadge'))}</span></div>`
|
|
1349
|
+
: '';
|
|
1350
|
+
|
|
1351
|
+
const deltaTotals = sumDeltaFromSummary(b.summary);
|
|
1352
|
+
const deltaHtml = deltaTotals
|
|
1353
|
+
? `<span class="summary-line-delta text-mono" title="${esc(t('summary.linesHint'))}">+${deltaTotals.added} -${deltaTotals.deleted}</span>`
|
|
1354
|
+
: '';
|
|
1355
|
+
|
|
1356
|
+
let line1 = '';
|
|
1357
|
+
if (b.filesChanged != null) {
|
|
1358
|
+
line1 = `<div class="summary-meta"><span class="summary-files">${b.filesChanged} ${t('summary.files')}</span>${deltaHtml}</div>`;
|
|
1359
|
+
} else if (deltaHtml) {
|
|
1360
|
+
line1 = `<div class="summary-meta">${deltaHtml}</div>`;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
let line2 = '';
|
|
1364
|
+
// From / Restore-To 仅用于「恢复前快照」语义;自动备份/手动快照若误带同名 trailer(如 Summary 换行),不应抢走 Intent 展示。
|
|
1365
|
+
const isPreRestoreType = b.type === 'git-pre-restore' || b.type === 'shadow-pre-restore';
|
|
1366
|
+
if (isPreRestoreType && b.from && b.restoreTo) {
|
|
1367
|
+
const label = b.restoreFile ? `${esc(b.restoreFile)}: ` : '';
|
|
1368
|
+
line2 = `<div class="summary-restore-ctx">${label}<span class="text-mono">${esc(b.from)}</span> → <span class="text-mono">${esc(b.restoreTo)}</span></div>`;
|
|
1369
|
+
} else {
|
|
1370
|
+
const parts = [];
|
|
1371
|
+
if (b.guardEvent) {
|
|
1372
|
+
const evShort = b.guardEvent.length > 72 ? b.guardEvent.substring(0, 69) + '...' : b.guardEvent;
|
|
1373
|
+
parts.push(`<div class="summary-guard-event"><span class="summary-guard-event-label">${t('drawer.field.guardEvent')}:</span> ${esc(evShort)}</div>`);
|
|
1374
|
+
}
|
|
1375
|
+
if (b.intent && (!b.guardEvent || b.intent !== b.guardEvent)) {
|
|
1376
|
+
const intentShort = b.intent.length > 70 ? b.intent.substring(0, 67) + '...' : b.intent;
|
|
1377
|
+
parts.push(`<div class="summary-intent"><span class="summary-intent-label">${t('drawer.field.intent')}:</span> ${esc(intentShort)}</div>`);
|
|
1378
|
+
} else if (!b.guardEvent && b.message && !b.message.startsWith('guard:')) {
|
|
1379
|
+
const msgShort = b.message.length > 70 ? b.message.substring(0, 67) + '...' : b.message;
|
|
1380
|
+
parts.push(`<div class="summary-message">${esc(msgShort)}</div>`);
|
|
1381
|
+
}
|
|
1382
|
+
line2 = parts.join('');
|
|
1383
|
+
}
|
|
1384
|
+
if (b.trigger && !line2) {
|
|
1385
|
+
line2 = `<div class="summary-trigger text-sm text-muted">${t('trigger.' + b.trigger)}</div>`;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
let line3 = '';
|
|
1389
|
+
if (b.summary) {
|
|
1390
|
+
const parsed = parseSummaryToFiles(b.summary);
|
|
1391
|
+
if (parsed.length > 0) {
|
|
1392
|
+
const MAX_INLINE = 3;
|
|
1393
|
+
const visible = parsed.slice(0, MAX_INLINE).map(f =>
|
|
1394
|
+
`<div class="summary-file-row"><span class="text-mono summary-file-path">${esc(f.path)}</span>${formatFileActionBadge(f.action)}<span class="text-mono text-muted">+${f.added} -${f.deleted}</span></div>`
|
|
1395
|
+
).join('');
|
|
1396
|
+
const remaining = parsed.length > MAX_INLINE ? parsed.length - MAX_INLINE : 0;
|
|
1397
|
+
const truncated = b.summary.includes('...');
|
|
1398
|
+
const moreCount = truncated ? (b.filesChanged || '?') - MAX_INLINE : remaining;
|
|
1399
|
+
const moreHtml = (remaining > 0 || truncated) ? `<div class="summary-file-more text-sm text-muted">${t('summary.andMore', { n: moreCount > 0 ? moreCount : '…' })}</div>` : '';
|
|
1400
|
+
line3 = visible + moreHtml;
|
|
1401
|
+
} else {
|
|
1402
|
+
const categories = normalizeSummaryText(b.summary).split('; ').map(s => translateSummary(s.trim())).filter(Boolean);
|
|
1403
|
+
line3 = categories.slice(0, 2).map(c => `<div class="summary-detail-line">${esc(c)}</div>`).join('');
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (!bookmarkHtml && !line1 && !line2 && !line3) return '<span class="text-muted text-sm">-</span>';
|
|
1408
|
+
return `<div class="summary-stack">${bookmarkHtml}${line1}${line2}${line3}</div>`;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function formatGuardScopeLabel(scope) {
|
|
1412
|
+
if (scope === 'full') return t('backups.scope.full');
|
|
1413
|
+
if (scope === 'narrow') return t('backups.scope.narrow');
|
|
1414
|
+
return t('backups.scope.unknown');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function formatGuardBaselineLabel(base) {
|
|
1418
|
+
const map = {
|
|
1419
|
+
'auto-backup': 'backups.baseline.autoBackup',
|
|
1420
|
+
snapshot: 'backups.baseline.snapshot',
|
|
1421
|
+
initial: 'backups.baseline.initial',
|
|
1422
|
+
other: 'backups.baseline.other',
|
|
1423
|
+
};
|
|
1424
|
+
const k = map[base];
|
|
1425
|
+
return k ? t(k) : t('backups.baseline.unknown');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** Git backup rows only: scope + shared-baseline hint from commit trailers */
|
|
1429
|
+
function formatBackupHumanMeta(b) {
|
|
1430
|
+
const show = b.type === 'git-auto-backup' || b.type === 'git-snapshot';
|
|
1431
|
+
if (!show) return '<span class="text-muted text-sm">-</span>';
|
|
1432
|
+
if (b.guardScope == null && b.guardDiffBase == null) {
|
|
1433
|
+
return `<span class="text-muted text-sm" title="${esc(t('backups.meta.legacyHint'))}">-</span>`;
|
|
1434
|
+
}
|
|
1435
|
+
const lines = [];
|
|
1436
|
+
if (b.guardScope != null) lines.push(`<span class="backup-meta-line">${esc(formatGuardScopeLabel(b.guardScope))}</span>`);
|
|
1437
|
+
if (b.guardDiffBase != null) lines.push(`<span class="backup-meta-line">${esc(formatGuardBaselineLabel(b.guardDiffBase))}</span>`);
|
|
1438
|
+
return `<div class="backup-human-meta">${lines.join('')}</div>`;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function renderBackupTable(backups) {
|
|
1442
|
+
if (!Array.isArray(backups)) {
|
|
1443
|
+
$('#backup-table-wrap').innerHTML = `<div class="error-panel">${t('error.sectionFailed')}</div>`;
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
let filtered = state.backupFilter === 'all'
|
|
1447
|
+
? backups
|
|
1448
|
+
: backups.filter(b => b.type === state.backupFilter);
|
|
1449
|
+
|
|
1450
|
+
if (state.fileSearch) {
|
|
1451
|
+
const q = state.fileSearch.toLowerCase();
|
|
1452
|
+
filtered = filtered.filter(b =>
|
|
1453
|
+
(b.summary && b.summary.toLowerCase().includes(q)) ||
|
|
1454
|
+
(b.message && b.message.toLowerCase().includes(q)) ||
|
|
1455
|
+
(b.intent && b.intent.toLowerCase().includes(q)) ||
|
|
1456
|
+
(b.guardEvent && b.guardEvent.toLowerCase().includes(q)) ||
|
|
1457
|
+
(b.restoreFile && b.restoreFile.toLowerCase().includes(q)) ||
|
|
1458
|
+
(b.guardScope && String(b.guardScope).toLowerCase().includes(q)) ||
|
|
1459
|
+
(b.guardDiffBase && String(b.guardDiffBase).toLowerCase().includes(q))
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
state.filteredBackups = filtered;
|
|
1463
|
+
|
|
1464
|
+
if (state.filteredBackups.length === 0) {
|
|
1465
|
+
$('#backup-table-wrap').innerHTML = `<div class="empty-state">${t('backups.noBackups')}</div>`;
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const rows = state.filteredBackups.map((b, i) => {
|
|
1470
|
+
const badgeClass = b.type.startsWith('git') ? (b.type.includes('pre') ? 'badge-pre' : 'badge-git') : (b.type.includes('pre') ? 'badge-pre' : 'badge-shadow');
|
|
1471
|
+
const summaryCell = formatSummaryCell(b);
|
|
1472
|
+
const metaCell = formatBackupHumanMeta(b);
|
|
1473
|
+
return `<tr data-bi="${i}">
|
|
1474
|
+
<td><div>${esc(formatTime(b.timestamp))}</div><div class="text-muted text-sm">${esc(relativeTime(b.timestamp))}</div></td>
|
|
1475
|
+
<td><span class="badge ${badgeClass}">${t('type.' + b.type)}</span></td>
|
|
1476
|
+
<td class="backup-meta-cell">${metaCell}</td>
|
|
1477
|
+
<td class="text-mono">${esc(b.shortHash || b.timestamp || '-')}</td>
|
|
1478
|
+
<td class="backup-summary-cell">${summaryCell}</td>
|
|
1479
|
+
</tr>`;
|
|
1480
|
+
}).join('');
|
|
1481
|
+
|
|
1482
|
+
$('#backup-table-wrap').innerHTML = `
|
|
1483
|
+
<table class="data-table">
|
|
1484
|
+
<thead><tr>
|
|
1485
|
+
<th>${t('backups.col.time')}</th>
|
|
1486
|
+
<th>${t('backups.col.type')}</th>
|
|
1487
|
+
<th>${t('backups.col.meta')}</th>
|
|
1488
|
+
<th>${t('backups.col.ref')}</th>
|
|
1489
|
+
<th>${t('backups.col.summary')}</th>
|
|
1490
|
+
</tr></thead>
|
|
1491
|
+
<tbody>${rows}</tbody>
|
|
1492
|
+
</table>
|
|
1493
|
+
`;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/* ── Rendering: Protection Scope ──────────────────────────── */
|
|
1497
|
+
|
|
1498
|
+
function renderProtection(scope) {
|
|
1499
|
+
const el = $('#protection-content');
|
|
1500
|
+
if (!scope) { el.innerHTML = `<div class="empty-state">${t('empty.noData')}</div>`; return; }
|
|
1501
|
+
|
|
1502
|
+
const protectList = scope.protect || [];
|
|
1503
|
+
const ignoreList = scope.ignore || [];
|
|
1504
|
+
const isAll = protectList.length === 1 && protectList[0] === '**';
|
|
1505
|
+
|
|
1506
|
+
el.innerHTML = `
|
|
1507
|
+
<div class="protection-grid">
|
|
1508
|
+
<div class="pattern-card pattern-card--protect">
|
|
1509
|
+
<h4>${t('protection.protect')}</h4>
|
|
1510
|
+
${isAll
|
|
1511
|
+
? `<p class="pattern-empty pattern-empty--protect text-sm">${t('protection.allFiles')}</p>`
|
|
1512
|
+
: `<ul class="pattern-list">${protectList.map(p => `<li class="pattern-item pattern-item--protect">${esc(p)}</li>`).join('')}</ul>`
|
|
1513
|
+
}
|
|
1514
|
+
</div>
|
|
1515
|
+
<div class="pattern-card pattern-card--ignore">
|
|
1516
|
+
<h4>${t('protection.ignore')}</h4>
|
|
1517
|
+
${ignoreList.length === 0
|
|
1518
|
+
? `<p class="pattern-empty pattern-empty--ignore text-sm">${t('protection.noIgnore')}</p>`
|
|
1519
|
+
: `<ul class="pattern-list">${ignoreList.map(p => `<li class="pattern-item pattern-item--ignore">${esc(p)}</li>`).join('')}</ul>`
|
|
1520
|
+
}
|
|
1521
|
+
</div>
|
|
1522
|
+
</div>
|
|
1523
|
+
<div class="protection-count">${t('protection.fileCount', { n: scope.fileCount || 0 })}</div>
|
|
1524
|
+
<p class="protection-note">${t('protection.note')}</p>
|
|
1525
|
+
`;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/* ── Rendering: Diagnostics ───────────────────────────────── */
|
|
1529
|
+
|
|
1530
|
+
function renderDiagnostics(doctor) {
|
|
1531
|
+
const el = $('#diagnostics-summary');
|
|
1532
|
+
const s = doctor.summary || { pass: 0, warn: 0, fail: 0 };
|
|
1533
|
+
|
|
1534
|
+
el.innerHTML = `
|
|
1535
|
+
<div class="diag-summary" id="diag-summary-click">
|
|
1536
|
+
<div class="diag-counts">
|
|
1537
|
+
<div class="diag-count"><span class="num" style="color:var(--green)">${s.pass}</span><span class="label badge-pass">${t('diagnostics.pass')}</span></div>
|
|
1538
|
+
<div class="diag-count"><span class="num" style="color:var(--yellow)">${s.warn}</span><span class="label badge-warn">${t('diagnostics.warn')}</span></div>
|
|
1539
|
+
<div class="diag-count"><span class="num" style="color:var(--red)">${s.fail}</span><span class="label badge-fail">${t('diagnostics.fail')}</span></div>
|
|
1540
|
+
</div>
|
|
1541
|
+
<span class="diag-hint">${t('diagnostics.hint')}</span>
|
|
1542
|
+
</div>
|
|
1543
|
+
`;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/* ── Rendering: Error / Empty ─────────────────────────────── */
|
|
1547
|
+
|
|
1548
|
+
function renderSectionError(elementId, msg) {
|
|
1549
|
+
const el = $(`#${elementId}`);
|
|
1550
|
+
if (!el) return;
|
|
1551
|
+
el.innerHTML = `<div class="error-panel"><div class="error-icon">⚠</div><p>${esc(msg || t('error.sectionFailed'))}</p></div>`;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/* ── File Detail Modal ────────────────────────────────────── */
|
|
1555
|
+
|
|
1556
|
+
function openFileModal(title, files, projectPath, commitHash) {
|
|
1557
|
+
$('#file-modal-title').textContent = title;
|
|
1558
|
+
const body = $('#file-modal-body');
|
|
1559
|
+
let sortKey = 'changes';
|
|
1560
|
+
const render = () => {
|
|
1561
|
+
const sorted = [...files];
|
|
1562
|
+
if (sortKey === 'path') sorted.sort((a, b) => a.path.localeCompare(b.path));
|
|
1563
|
+
else if (sortKey === 'action') sorted.sort((a, b) => a.action.localeCompare(b.action));
|
|
1564
|
+
else sorted.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
|
|
1565
|
+
|
|
1566
|
+
const rows = sorted.map(f => {
|
|
1567
|
+
const isDeleted = f.action === 'D';
|
|
1568
|
+
const source = isDeleted && commitHash ? `${commitHash}~1` : commitHash;
|
|
1569
|
+
const restoreCmd = source
|
|
1570
|
+
? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${source}" })`
|
|
1571
|
+
: '';
|
|
1572
|
+
const btnLabel = isDeleted ? t('modal.restorePreDelete') : t('modal.copyRestore');
|
|
1573
|
+
return `<tr>
|
|
1574
|
+
<td class="text-mono modal-file-path" title="${esc(f.path)}">${esc(f.path)}</td>
|
|
1575
|
+
<td>${formatFileActionBadge(f.action)}</td>
|
|
1576
|
+
<td class="text-mono modal-file-changes">+${f.added || 0} -${f.deleted || 0}</td>
|
|
1577
|
+
${commitHash ? `<td><button class="modal-restore-btn${isDeleted ? ' btn-deleted' : ''}" data-restore-cmd="${esc(restoreCmd)}">${btnLabel}</button></td>` : ''}
|
|
1578
|
+
</tr>`;
|
|
1579
|
+
}).join('');
|
|
1580
|
+
|
|
1581
|
+
body.innerHTML = `<table>
|
|
1582
|
+
<thead><tr>
|
|
1583
|
+
<th data-msort="path">${t('alert.col.file')} ↕</th>
|
|
1584
|
+
<th data-msort="action">${t('alert.col.action')} ↕</th>
|
|
1585
|
+
<th data-msort="changes">${t('alert.col.changes')} ↕</th>
|
|
1586
|
+
${commitHash ? `<th>${t('modal.col.restore')}</th>` : ''}
|
|
1587
|
+
</tr></thead>
|
|
1588
|
+
<tbody>${rows}</tbody>
|
|
1589
|
+
</table>`;
|
|
1590
|
+
};
|
|
1591
|
+
render();
|
|
1592
|
+
|
|
1593
|
+
body.addEventListener('click', (e) => {
|
|
1594
|
+
const th = e.target.closest('[data-msort]');
|
|
1595
|
+
if (th) {
|
|
1596
|
+
sortKey = th.dataset.msort;
|
|
1597
|
+
render();
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const btn = e.target.closest('[data-restore-cmd]');
|
|
1601
|
+
if (btn) {
|
|
1602
|
+
copyText(btn.dataset.restoreCmd);
|
|
1603
|
+
const origLabel = btn.classList.contains('btn-deleted') ? t('modal.restorePreDelete') : t('modal.copyRestore');
|
|
1604
|
+
btn.textContent = t('modal.copied');
|
|
1605
|
+
btn.classList.add('copied');
|
|
1606
|
+
setTimeout(() => { btn.textContent = origLabel; btn.classList.remove('copied'); }, 1500);
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
$('#file-modal-overlay').classList.add('active');
|
|
1611
|
+
document.body.style.overflow = 'hidden';
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function closeFileModal() {
|
|
1615
|
+
$('#file-modal-overlay').classList.remove('active');
|
|
1616
|
+
document.body.style.overflow = state.drawerOpen ? 'hidden' : '';
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/* ── Drawers ──────────────────────────────────────────────── */
|
|
1620
|
+
|
|
1621
|
+
function openDrawer(name) {
|
|
1622
|
+
state.drawerOpen = name;
|
|
1623
|
+
$(`#${name}-drawer`).classList.add('active');
|
|
1624
|
+
$('#drawer-overlay').classList.add('active');
|
|
1625
|
+
document.body.style.overflow = 'hidden';
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function closeDrawer() {
|
|
1629
|
+
if (!state.drawerOpen) return;
|
|
1630
|
+
$(`#${state.drawerOpen}-drawer`).classList.remove('active');
|
|
1631
|
+
$('#drawer-overlay').classList.remove('active');
|
|
1632
|
+
document.body.style.overflow = '';
|
|
1633
|
+
state.drawerOpen = null;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function renderDrawerFilesTable(files, sortKey, commitHash, projectPath) {
|
|
1637
|
+
const sorted = [...files];
|
|
1638
|
+
if (sortKey === 'path') sorted.sort((a, b) => a.path.localeCompare(b.path));
|
|
1639
|
+
else if (sortKey === 'action') sorted.sort((a, b) => a.action.localeCompare(b.action));
|
|
1640
|
+
else sorted.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
|
|
1641
|
+
const hasRestore = !!commitHash;
|
|
1642
|
+
const rows = sorted.map(f => {
|
|
1643
|
+
const isDeleted = f.action === 'D';
|
|
1644
|
+
const source = isDeleted && commitHash ? `${commitHash}~1` : commitHash;
|
|
1645
|
+
const cmd = hasRestore ? `restore_file({ path: "${projectPath || ''}", file: "${f.path}", source: "${source}" })` : '';
|
|
1646
|
+
const btnLabel = isDeleted ? t('modal.restorePreDelete') : t('modal.copyRestore');
|
|
1647
|
+
return `<tr>
|
|
1648
|
+
<td class="text-mono drawer-file-path">${esc(f.path)}</td>
|
|
1649
|
+
<td>${formatFileActionBadge(f.action)}</td>
|
|
1650
|
+
<td class="text-mono drawer-file-changes">+${f.added} -${f.deleted}</td>
|
|
1651
|
+
${hasRestore ? `<td><button class="modal-restore-btn${isDeleted ? ' btn-deleted' : ''}" data-restore-cmd="${esc(cmd)}">${btnLabel}</button></td>` : ''}
|
|
1652
|
+
</tr>`;
|
|
1653
|
+
}).join('');
|
|
1654
|
+
return `<table class="drawer-files-table">
|
|
1655
|
+
<thead><tr>
|
|
1656
|
+
<th data-sort="path" class="drawer-sort-header">${t('alert.col.file')} ↕</th>
|
|
1657
|
+
<th data-sort="action" class="drawer-sort-header">${t('alert.col.action')} ↕</th>
|
|
1658
|
+
<th data-sort="changes" class="drawer-sort-header">${t('alert.col.changes')} ↕</th>
|
|
1659
|
+
${hasRestore ? `<th>${t('modal.col.restore')}</th>` : ''}
|
|
1660
|
+
</tr></thead>
|
|
1661
|
+
<tbody>${rows}</tbody>
|
|
1662
|
+
</table>`;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function openRestoreDrawer(backup) {
|
|
1666
|
+
const body = $('#restore-drawer-body');
|
|
1667
|
+
const fields = [
|
|
1668
|
+
{ key: 'drawer.field.time', val: formatTime(backup.timestamp) },
|
|
1669
|
+
{ key: 'drawer.field.type', val: t('type.' + backup.type) },
|
|
1670
|
+
];
|
|
1671
|
+
if (backup.trigger) fields.push({ key: 'drawer.field.trigger', val: t('trigger.' + backup.trigger) });
|
|
1672
|
+
if (backup.guardScope != null) {
|
|
1673
|
+
fields.push({ key: 'drawer.field.guardScope', val: formatGuardScopeLabel(backup.guardScope) });
|
|
1674
|
+
}
|
|
1675
|
+
if (backup.guardDiffBase != null) {
|
|
1676
|
+
fields.push({ key: 'drawer.field.guardDiffBase', val: formatGuardBaselineLabel(backup.guardDiffBase) });
|
|
1677
|
+
}
|
|
1678
|
+
if (backup.guardBookmark) {
|
|
1679
|
+
fields.push({ key: 'drawer.field.bookmark', val: t('backups.bookmarkTitle') });
|
|
1680
|
+
}
|
|
1681
|
+
if (backup.guardEvent) {
|
|
1682
|
+
fields.push({ key: 'drawer.field.guardEvent', val: backup.guardEvent });
|
|
1683
|
+
}
|
|
1684
|
+
if (backup.filesChanged != null) fields.push({ key: 'drawer.field.filesChanged', val: String(backup.filesChanged) });
|
|
1685
|
+
if (backup.ref) fields.push({ key: 'drawer.field.ref', val: backup.ref });
|
|
1686
|
+
if (backup.commitHash) fields.push({ key: 'drawer.field.hash', val: backup.commitHash });
|
|
1687
|
+
if (backup.path) fields.push({ key: 'drawer.field.path', val: backup.path });
|
|
1688
|
+
if (backup.intent) fields.push({ key: 'drawer.field.intent', val: backup.intent });
|
|
1689
|
+
if (backup.agent) fields.push({ key: 'drawer.field.agent', val: backup.agent });
|
|
1690
|
+
if (backup.session) fields.push({ key: 'drawer.field.session', val: backup.session });
|
|
1691
|
+
if (backup.from) fields.push({ key: 'drawer.field.from', val: backup.from });
|
|
1692
|
+
if (backup.restoreTo) fields.push({ key: 'drawer.field.restoreTo', val: backup.restoreTo });
|
|
1693
|
+
if (backup.restoreFile) fields.push({ key: 'drawer.field.restoreFile', val: backup.restoreFile });
|
|
1694
|
+
if (backup.message) fields.push({ key: 'drawer.field.message', val: backup.message });
|
|
1695
|
+
|
|
1696
|
+
const refText = backup.ref || backup.shortHash || backup.timestamp || '';
|
|
1697
|
+
const jsonText = JSON.stringify(backup, null, 2);
|
|
1698
|
+
|
|
1699
|
+
const isGit = backup.type?.startsWith('git');
|
|
1700
|
+
const hash = backup.commitHash || backup.shortHash || '';
|
|
1701
|
+
const restoreProjectCmd = isGit && hash ? `restore_project({ version: "${hash}", mode: "execute" })` : '';
|
|
1702
|
+
const restoreFileCmd = isGit && hash ? `restore_file({ file: "<filename>", version: "${hash}" })` : '';
|
|
1703
|
+
|
|
1704
|
+
body.innerHTML = `
|
|
1705
|
+
${fields.map(f => `
|
|
1706
|
+
<div class="restore-field">
|
|
1707
|
+
<div class="restore-field-label">${t(f.key)}</div>
|
|
1708
|
+
<div class="restore-field-value text-mono">${esc(f.val)}</div>
|
|
1709
|
+
</div>
|
|
1710
|
+
`).join('')}
|
|
1711
|
+
${backup.summary ? `
|
|
1712
|
+
<div class="restore-field">
|
|
1713
|
+
<div class="restore-field-label">${t('drawer.field.summary')}</div>
|
|
1714
|
+
<div id="drawer-files-container" class="drawer-files-container">
|
|
1715
|
+
<div class="drawer-files-loading text-muted text-sm">${t('state.loading')}</div>
|
|
1716
|
+
</div>
|
|
1717
|
+
</div>
|
|
1718
|
+
` : ''}
|
|
1719
|
+
<div class="restore-actions">
|
|
1720
|
+
<button class="btn btn-sm" data-copy="${esc(refText)}">${t('drawer.copyRef')}</button>
|
|
1721
|
+
<button class="btn btn-sm" data-copy-json>${t('drawer.copyJson')}</button>
|
|
1722
|
+
<button class="btn btn-sm" id="preview-toggle">${t('drawer.preview')}</button>
|
|
1723
|
+
</div>
|
|
1724
|
+
${restoreProjectCmd ? `
|
|
1725
|
+
<div class="restore-cmd-section">
|
|
1726
|
+
<div class="restore-cmd-label text-sm text-muted">MCP Restore Commands</div>
|
|
1727
|
+
<div class="restore-cmd-row">
|
|
1728
|
+
<code class="restore-cmd-code">${esc(restoreProjectCmd)}</code>
|
|
1729
|
+
<button class="btn btn-sm btn-restore-cmd" data-copy-restore-project>${t('drawer.restoreCmd')}</button>
|
|
1730
|
+
</div>
|
|
1731
|
+
<div class="restore-cmd-row">
|
|
1732
|
+
<code class="restore-cmd-code">${esc(restoreFileCmd)}</code>
|
|
1733
|
+
<button class="btn btn-sm btn-restore-cmd" data-copy-restore-file>${t('drawer.restoreCmdFile')}</button>
|
|
1734
|
+
</div>
|
|
1735
|
+
</div>
|
|
1736
|
+
` : ''}
|
|
1737
|
+
<div id="json-preview-wrap" class="hidden">
|
|
1738
|
+
<pre class="json-preview">${esc(jsonText)}</pre>
|
|
1739
|
+
</div>
|
|
1740
|
+
`;
|
|
1741
|
+
|
|
1742
|
+
body.querySelector('[data-copy-json]')?.addEventListener('click', () => copyText(jsonText));
|
|
1743
|
+
if (restoreProjectCmd) {
|
|
1744
|
+
body.querySelector('[data-copy-restore-project]')?.addEventListener('click', () => copyText(restoreProjectCmd));
|
|
1745
|
+
body.querySelector('[data-copy-restore-file]')?.addEventListener('click', () => copyText(restoreFileCmd));
|
|
1746
|
+
}
|
|
1747
|
+
body.querySelector('#preview-toggle')?.addEventListener('click', () => {
|
|
1748
|
+
const wrap = body.querySelector('#json-preview-wrap');
|
|
1749
|
+
wrap.classList.toggle('hidden');
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
const projPath = backup.path || state.pageData?.status?.config?.path || '';
|
|
1753
|
+
|
|
1754
|
+
// Lazy-load full file list for summary section
|
|
1755
|
+
if (backup.summary && isGit && hash) {
|
|
1756
|
+
let currentFiles = [];
|
|
1757
|
+
let currentSort = 'changes';
|
|
1758
|
+
const setupContainer = (container) => {
|
|
1759
|
+
container.innerHTML = renderDrawerFilesTable(currentFiles, currentSort, hash, projPath);
|
|
1760
|
+
container.addEventListener('click', (e) => {
|
|
1761
|
+
const th = e.target.closest('[data-sort]');
|
|
1762
|
+
if (th) {
|
|
1763
|
+
currentSort = th.dataset.sort;
|
|
1764
|
+
container.innerHTML = renderDrawerFilesTable(currentFiles, currentSort, hash, projPath);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const restoreBtn = e.target.closest('[data-restore-cmd]');
|
|
1768
|
+
if (restoreBtn) {
|
|
1769
|
+
copyText(restoreBtn.dataset.restoreCmd);
|
|
1770
|
+
const origLabel = restoreBtn.classList.contains('btn-deleted') ? t('modal.restorePreDelete') : t('modal.copyRestore');
|
|
1771
|
+
restoreBtn.textContent = t('modal.copied');
|
|
1772
|
+
restoreBtn.classList.add('copied');
|
|
1773
|
+
setTimeout(() => { restoreBtn.textContent = origLabel; restoreBtn.classList.remove('copied'); }, 1500);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
};
|
|
1777
|
+
fetchBackupFiles(hash).then(files => {
|
|
1778
|
+
currentFiles = files.length > 0 ? files : parseSummaryToFiles(backup.summary);
|
|
1779
|
+
const container = body.querySelector('#drawer-files-container');
|
|
1780
|
+
if (container) setupContainer(container);
|
|
1781
|
+
});
|
|
1782
|
+
} else if (backup.summary) {
|
|
1783
|
+
const fallback = parseSummaryToFiles(backup.summary);
|
|
1784
|
+
const container = body.querySelector('#drawer-files-container');
|
|
1785
|
+
if (container && fallback.length > 0) {
|
|
1786
|
+
container.innerHTML = renderDrawerFilesTable(fallback, 'changes', hash, projPath);
|
|
1787
|
+
} else if (container) {
|
|
1788
|
+
const translated = backup.summary.split('; ').map(s => translateSummary(s)).join('\n');
|
|
1789
|
+
container.innerHTML = `<pre class="restore-field-value text-mono summary-pre">${esc(translated)}</pre>`;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
openDrawer('restore');
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function openDoctorDrawer() {
|
|
1797
|
+
const doctor = state.pageData?.doctor;
|
|
1798
|
+
if (!doctor || doctor.error) return;
|
|
1799
|
+
const body = $('#doctor-drawer-body');
|
|
1800
|
+
|
|
1801
|
+
body.innerHTML = (doctor.checks || []).map(c => {
|
|
1802
|
+
const badgeClass = c.status === 'PASS' ? 'badge-pass' : c.status === 'WARN' ? 'badge-warn' : 'badge-fail';
|
|
1803
|
+
const shouldOpen = c.status !== 'PASS';
|
|
1804
|
+
return `
|
|
1805
|
+
<details class="check-item" ${shouldOpen ? 'open' : ''}>
|
|
1806
|
+
<summary>
|
|
1807
|
+
<span class="badge ${badgeClass}">${t('diagnostics.' + c.status)}</span>
|
|
1808
|
+
<span class="check-name">${esc(translateCheckName(c.name))}</span>
|
|
1809
|
+
</summary>
|
|
1810
|
+
${c.detail ? `<div class="check-detail">${esc(translateDetail(c.detail))}</div>` : ''}
|
|
1811
|
+
</details>
|
|
1812
|
+
`;
|
|
1813
|
+
}).join('');
|
|
1814
|
+
|
|
1815
|
+
openDrawer('doctor');
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/* ── Copy to clipboard ────────────────────────────────────── */
|
|
1819
|
+
|
|
1820
|
+
async function copyText(text) {
|
|
1821
|
+
if (window.__IN_VSCODE__ && window.acquireVsCodeApi) {
|
|
1822
|
+
try { window.__vscodeApi = window.__vscodeApi || acquireVsCodeApi(); } catch { /* already acquired */ }
|
|
1823
|
+
window.__vscodeApi?.postMessage({ type: 'copy', text });
|
|
1824
|
+
} else {
|
|
1825
|
+
try {
|
|
1826
|
+
await navigator.clipboard.writeText(text);
|
|
1827
|
+
} catch {
|
|
1828
|
+
const ta = document.createElement('textarea');
|
|
1829
|
+
ta.value = text;
|
|
1830
|
+
ta.style.cssText = 'position:fixed;left:-9999px';
|
|
1831
|
+
document.body.appendChild(ta);
|
|
1832
|
+
ta.select();
|
|
1833
|
+
document.execCommand('copy');
|
|
1834
|
+
document.body.removeChild(ta);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
showToast(t('drawer.copied'));
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function showToast(msg) {
|
|
1841
|
+
let toast = $('#copy-toast');
|
|
1842
|
+
if (!toast) {
|
|
1843
|
+
toast = document.createElement('div');
|
|
1844
|
+
toast.id = 'copy-toast';
|
|
1845
|
+
toast.className = 'copy-toast';
|
|
1846
|
+
document.body.appendChild(toast);
|
|
1847
|
+
}
|
|
1848
|
+
toast.textContent = msg;
|
|
1849
|
+
toast.classList.add('show');
|
|
1850
|
+
setTimeout(() => toast.classList.remove('show'), 1500);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/* ── Event Listeners ──────────────────────────────────────── */
|
|
1854
|
+
|
|
1855
|
+
function setupEvents() {
|
|
1856
|
+
$('#project-select').addEventListener('change', async (e) => {
|
|
1857
|
+
state.currentProjectId = e.target.value;
|
|
1858
|
+
state.backupFilter = 'all';
|
|
1859
|
+
disconnectLivePush();
|
|
1860
|
+
stopRefresh();
|
|
1861
|
+
showSkeleton();
|
|
1862
|
+
try { await loadPageData(); renderAll(); }
|
|
1863
|
+
catch (err) { showGlobalError(err.message); }
|
|
1864
|
+
startRefresh();
|
|
1865
|
+
connectLivePush();
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
$('#refresh-btn').addEventListener('click', manualRefresh);
|
|
1869
|
+
$('#error-retry').addEventListener('click', manualRefresh);
|
|
1870
|
+
|
|
1871
|
+
document.addEventListener('visibilitychange', () => {
|
|
1872
|
+
if (!state.currentProjectId) return;
|
|
1873
|
+
if (document.hidden) {
|
|
1874
|
+
stopRefresh();
|
|
1875
|
+
startRefresh();
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
void (async () => {
|
|
1879
|
+
try {
|
|
1880
|
+
await loadPageData();
|
|
1881
|
+
renderAll();
|
|
1882
|
+
} catch { /* keep existing UI */ }
|
|
1883
|
+
})();
|
|
1884
|
+
stopRefresh();
|
|
1885
|
+
startRefresh();
|
|
1886
|
+
connectLivePush();
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
$('#lang-toggle').addEventListener('click', () => {
|
|
1890
|
+
setLocale(state.locale === 'zh-CN' ? 'en-US' : 'zh-CN');
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// Filter buttons (event delegation)
|
|
1894
|
+
$('#backup-filters').addEventListener('click', (e) => {
|
|
1895
|
+
const btn = e.target.closest('.filter-btn');
|
|
1896
|
+
if (!btn) return;
|
|
1897
|
+
state.backupFilter = btn.dataset.filter;
|
|
1898
|
+
const backups = state.pageData?.backups;
|
|
1899
|
+
if (Array.isArray(backups)) {
|
|
1900
|
+
renderFilterBar(backups);
|
|
1901
|
+
renderBackupTable(backups);
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
// File search (event delegation on parent)
|
|
1906
|
+
document.addEventListener('input', (e) => {
|
|
1907
|
+
if (e.target.id === 'file-search') {
|
|
1908
|
+
state.fileSearch = e.target.value;
|
|
1909
|
+
const backups = state.pageData?.backups;
|
|
1910
|
+
if (Array.isArray(backups)) renderBackupTable(backups);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// Summary expand toggle (must come before row click to prevent drawer open)
|
|
1915
|
+
$('#backup-table-wrap').addEventListener('click', (e) => {
|
|
1916
|
+
const toggleBtn = e.target.closest('[data-summary-toggle]');
|
|
1917
|
+
if (toggleBtn) {
|
|
1918
|
+
e.stopPropagation();
|
|
1919
|
+
const cell = toggleBtn.closest('.summary-stack');
|
|
1920
|
+
if (cell) cell.classList.toggle('summary-expanded');
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
const row = e.target.closest('tr[data-bi]');
|
|
1924
|
+
if (!row) return;
|
|
1925
|
+
const idx = parseInt(row.dataset.bi, 10);
|
|
1926
|
+
const backup = state.filteredBackups[idx];
|
|
1927
|
+
if (backup) openRestoreDrawer(backup);
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
// Alert history modal + file modal (event delegation)
|
|
1931
|
+
$('#card-alert').addEventListener('click', (e) => {
|
|
1932
|
+
const historyBtn = e.target.closest('[data-alert-history-modal]');
|
|
1933
|
+
if (historyBtn) {
|
|
1934
|
+
openAlertHistoryModal();
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const modalBtn = e.target.closest('[data-alert-files-modal]');
|
|
1938
|
+
if (modalBtn) {
|
|
1939
|
+
const alerts = state.pageData?.dashboard?.alerts;
|
|
1940
|
+
const files = alerts?.latest?.files || [];
|
|
1941
|
+
if (files.length > 0) {
|
|
1942
|
+
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
1943
|
+
const hash = alerts?.latest?.commitHash || 'HEAD';
|
|
1944
|
+
openFileModal(t('modal.alertFiles'), files, proj, hash);
|
|
1945
|
+
}
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
if (e.target.closest('[data-dismiss-active-alert]')) {
|
|
1949
|
+
e.preventDefault();
|
|
1950
|
+
dismissActiveAlert();
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// Diagnostics summary click
|
|
1955
|
+
$('#diagnostics-summary').addEventListener('click', (e) => {
|
|
1956
|
+
if (e.target.closest('#diag-summary-click')) openDoctorDrawer();
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
// Copy ref buttons (event delegation on drawers)
|
|
1960
|
+
document.addEventListener('click', (e) => {
|
|
1961
|
+
const copyBtn = e.target.closest('[data-copy]');
|
|
1962
|
+
if (copyBtn) copyText(copyBtn.dataset.copy);
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// Close drawer
|
|
1966
|
+
$('#drawer-overlay').addEventListener('click', closeDrawer);
|
|
1967
|
+
document.querySelectorAll('[data-action="close-drawer"]').forEach(btn => {
|
|
1968
|
+
btn.addEventListener('click', closeDrawer);
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// Close modal
|
|
1972
|
+
$('#file-modal-overlay').addEventListener('click', (e) => {
|
|
1973
|
+
if (e.target === $('#file-modal-overlay')) closeFileModal();
|
|
1974
|
+
});
|
|
1975
|
+
document.querySelectorAll('[data-action="close-modal"]').forEach(btn => {
|
|
1976
|
+
btn.addEventListener('click', closeFileModal);
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
document.addEventListener('keydown', (e) => {
|
|
1980
|
+
if (e.key === 'Escape') { closeFileModal(); closeDrawer(); }
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/* ── Version Check ────────────────────────────────────────── */
|
|
1985
|
+
|
|
1986
|
+
async function checkServerVersion() {
|
|
1987
|
+
try {
|
|
1988
|
+
const data = await fetchJson('/api/version');
|
|
1989
|
+
if (data.updateAvailable) showUpgradeBanner(data);
|
|
1990
|
+
} catch { /* non-critical */ }
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function showUpgradeBanner(data) {
|
|
1994
|
+
if ($('#upgrade-banner')) return;
|
|
1995
|
+
const banner = document.createElement('div');
|
|
1996
|
+
banner.id = 'upgrade-banner';
|
|
1997
|
+
banner.className = 'upgrade-banner';
|
|
1998
|
+
banner.innerHTML = `
|
|
1999
|
+
<span class="upgrade-banner-text">${t('upgrade.banner', { installed: esc(data.installedVersion), server: esc(data.serverVersion) })}</span>
|
|
2000
|
+
<button class="upgrade-banner-restart-btn">${t('upgrade.restartNow')}</button>
|
|
2001
|
+
<button class="upgrade-banner-hint-btn">${t('upgrade.restart')}</button>
|
|
2002
|
+
<button class="upgrade-banner-close" aria-label="${t('upgrade.dismiss')}">×</button>
|
|
2003
|
+
`;
|
|
2004
|
+
const topbar = $('#topbar');
|
|
2005
|
+
topbar.parentNode.insertBefore(banner, topbar.nextSibling);
|
|
2006
|
+
banner.querySelector('.upgrade-banner-close').addEventListener('click', () => banner.remove());
|
|
2007
|
+
banner.querySelector('.upgrade-banner-hint-btn').addEventListener('click', () => {
|
|
2008
|
+
const hint = banner.querySelector('.upgrade-banner-hint');
|
|
2009
|
+
if (hint) { hint.remove(); return; }
|
|
2010
|
+
const el = document.createElement('div');
|
|
2011
|
+
el.className = 'upgrade-banner-hint';
|
|
2012
|
+
el.innerHTML = `<code>${t('upgrade.hint')}</code>`;
|
|
2013
|
+
banner.appendChild(el);
|
|
2014
|
+
});
|
|
2015
|
+
banner.querySelector('.upgrade-banner-restart-btn').addEventListener('click', () => {
|
|
2016
|
+
restartServer(banner);
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async function restartServer(banner) {
|
|
2021
|
+
const btn = banner.querySelector('.upgrade-banner-restart-btn');
|
|
2022
|
+
btn.disabled = true;
|
|
2023
|
+
btn.textContent = t('upgrade.restarting');
|
|
2024
|
+
banner.querySelector('.upgrade-banner-hint-btn').style.display = 'none';
|
|
2025
|
+
banner.querySelector('.upgrade-banner-close').style.display = 'none';
|
|
2026
|
+
|
|
2027
|
+
try {
|
|
2028
|
+
const base = window.__GUARD_BASE_URL__ || '';
|
|
2029
|
+
const sep = '/api/restart'.includes('?') ? '&' : '?';
|
|
2030
|
+
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
|
|
2031
|
+
await fetch(base + '/api/restart' + tokenParam, { method: 'POST' });
|
|
2032
|
+
} catch { /* server may close connection */ }
|
|
2033
|
+
|
|
2034
|
+
btn.textContent = t('upgrade.waiting');
|
|
2035
|
+
let ready = false;
|
|
2036
|
+
for (let i = 0; i < 20; i++) {
|
|
2037
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2038
|
+
try {
|
|
2039
|
+
const r = await fetch((window.__GUARD_BASE_URL__ || '') + '/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
|
|
2040
|
+
if (r.ok) { ready = true; break; }
|
|
2041
|
+
} catch { /* still restarting */ }
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
if (ready) {
|
|
2045
|
+
location.reload();
|
|
2046
|
+
} else {
|
|
2047
|
+
btn.textContent = t('upgrade.failed');
|
|
2048
|
+
btn.disabled = false;
|
|
2049
|
+
banner.querySelector('.upgrade-banner-hint-btn').style.display = '';
|
|
2050
|
+
banner.querySelector('.upgrade-banner-close').style.display = '';
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/* ── Init ─────────────────────────────────────────────────── */
|
|
2055
|
+
|
|
2056
|
+
async function init() {
|
|
2057
|
+
state.locale = detectLocale();
|
|
2058
|
+
document.documentElement.lang = state.locale === 'zh-CN' ? 'zh-CN' : 'en';
|
|
2059
|
+
document.title = t('app.title');
|
|
2060
|
+
updateStaticI18n();
|
|
2061
|
+
showSkeleton();
|
|
2062
|
+
|
|
2063
|
+
try {
|
|
2064
|
+
await loadProjects();
|
|
2065
|
+
renderProjectSelect();
|
|
2066
|
+
await loadPageData({ progressive: true });
|
|
2067
|
+
renderAll();
|
|
2068
|
+
startRefresh();
|
|
2069
|
+
connectLivePush();
|
|
2070
|
+
checkServerVersion();
|
|
2071
|
+
} catch (e) {
|
|
2072
|
+
showGlobalError(e.message);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2077
|
+
setupEvents();
|
|
2078
|
+
init();
|
|
2079
|
+
});
|