cursor-guard 4.9.8 → 4.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/README.zh-CN.md +10 -1
- package/ROADMAP.md +50 -5
- package/docs/RELEASE.md +196 -0
- package/package.json +69 -68
- package/references/dashboard/public/app.js +313 -95
- package/references/dashboard/public/style.css +320 -160
- package/references/dashboard/server.js +197 -4
- package/references/lib/core/backups.js +36 -21
- package/references/lib/core/core.test.js +1629 -1484
- package/references/lib/core/snapshot.js +59 -8
- package/references/mcp/server.js +73 -72
- package/references/vscode-extension/{dist/cursor-guard-ide-4.9.8.vsix → cursor-guard-ide-4.9.12.vsix} +0 -0
- package/references/vscode-extension/dist/cursor-guard-ide-4.9.12.vsix +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +313 -95
- package/references/vscode-extension/dist/dashboard/public/style.css +320 -160
- package/references/vscode-extension/dist/dashboard/server.js +197 -4
- package/references/vscode-extension/dist/extension.js +9 -2
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/core/backups.js +36 -21
- package/references/vscode-extension/dist/lib/core/snapshot.js +59 -8
- package/references/vscode-extension/dist/lib/dashboard-manager.js +110 -103
- package/references/vscode-extension/dist/lib/poller.js +161 -21
- package/references/vscode-extension/dist/lib/sidebar-webview.js +469 -156
- package/references/vscode-extension/dist/mcp/server.js +85 -31
- package/references/vscode-extension/dist/package.json +1 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +50 -5
- package/references/vscode-extension/extension.js +9 -2
- package/references/vscode-extension/lib/dashboard-manager.js +110 -103
- package/references/vscode-extension/lib/poller.js +161 -21
- package/references/vscode-extension/lib/sidebar-webview.js +469 -156
- package/references/vscode-extension/package.json +140 -140
|
@@ -60,16 +60,18 @@ const I18N = {
|
|
|
60
60
|
'alert.action.deleted': 'Deleted',
|
|
61
61
|
'alert.action.renamed': 'Renamed',
|
|
62
62
|
'alert.breakdown': '{added} added, {modified} modified, {deleted} deleted',
|
|
63
|
-
'alert.suggestion': 'Check recent changes and consider creating a manual snapshot',
|
|
64
|
-
'alert.
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'preWarning.
|
|
68
|
-
'preWarning.
|
|
69
|
-
'preWarning.
|
|
70
|
-
'preWarning.
|
|
71
|
-
'preWarning.
|
|
72
|
-
'
|
|
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',
|
|
73
75
|
'modal.col.restore': 'Restore',
|
|
74
76
|
'modal.copyRestore': 'Copy cmd',
|
|
75
77
|
'modal.restorePreDelete':'Restore pre-delete',
|
|
@@ -86,7 +88,19 @@ const I18N = {
|
|
|
86
88
|
'backups.noBackups': 'No restore points found',
|
|
87
89
|
'backups.col.time': 'Time',
|
|
88
90
|
'backups.col.type': 'Type',
|
|
91
|
+
'backups.col.meta': 'Scope / baseline',
|
|
89
92
|
'backups.col.ref': 'Ref / Hash',
|
|
93
|
+
'backups.meta.legacyHint': 'Commit predates Guard scope/baseline trailers',
|
|
94
|
+
|
|
95
|
+
'backups.scope.full': 'Full workspace',
|
|
96
|
+
'backups.scope.narrow': 'Protect patterns only',
|
|
97
|
+
'backups.scope.unknown': 'Unknown',
|
|
98
|
+
|
|
99
|
+
'backups.baseline.autoBackup': 'Δ vs last auto-backup tip',
|
|
100
|
+
'backups.baseline.snapshot': 'Δ vs last manual snapshot tip',
|
|
101
|
+
'backups.baseline.initial': 'First Guard commit (no parent)',
|
|
102
|
+
'backups.baseline.other': 'Δ vs custom ref',
|
|
103
|
+
'backups.baseline.unknown': 'Unknown baseline',
|
|
90
104
|
|
|
91
105
|
'type.git-auto-backup': 'Git Auto-Backup',
|
|
92
106
|
'type.git-pre-restore': 'Git Pre-Restore',
|
|
@@ -125,6 +139,8 @@ const I18N = {
|
|
|
125
139
|
'drawer.field.filesChanged': 'Files Changed',
|
|
126
140
|
'drawer.field.summary': 'Change Summary',
|
|
127
141
|
'drawer.field.trigger': 'Trigger',
|
|
142
|
+
'drawer.field.guardScope': 'Snapshot scope',
|
|
143
|
+
'drawer.field.guardDiffBase': 'Diff baseline',
|
|
128
144
|
'trigger.auto': 'Auto (scheduled)',
|
|
129
145
|
'trigger.manual': 'Manual (agent)',
|
|
130
146
|
'trigger.pre-restore': 'Pre-Restore',
|
|
@@ -135,6 +151,7 @@ const I18N = {
|
|
|
135
151
|
'summary.deleted': 'Deleted',
|
|
136
152
|
'summary.renamed': 'Renamed',
|
|
137
153
|
'summary.files': 'files',
|
|
154
|
+
'summary.linesHint': 'Total lines added / deleted in this backup (from Summary)',
|
|
138
155
|
'summary.andMore': 'and {n} more…',
|
|
139
156
|
'drawer.field.intent': 'Intent',
|
|
140
157
|
'drawer.field.agent': 'Agent',
|
|
@@ -177,9 +194,9 @@ const I18N = {
|
|
|
177
194
|
'issue.disk_critically_low': 'Disk space critically low ({gb} GB free)',
|
|
178
195
|
'issue.disk_low': 'Disk space low ({gb} GB free)',
|
|
179
196
|
'issue.git_backup_stale': 'Last git backup is stale ({rel})',
|
|
180
|
-
'issue.active_alert': 'Active alert: {type} — {count} files in {window}s',
|
|
181
|
-
'issue.pre_warning_active': 'Pre-warning active: {summary}',
|
|
182
|
-
'issue.alert_high_velocity': 'High volume of file changes detected. Consider reviewing recent modifications and creating a manual snapshot.',
|
|
197
|
+
'issue.active_alert': 'Active alert: {type} — {count} files in {window}s',
|
|
198
|
+
'issue.pre_warning_active': 'Pre-warning active: {summary}',
|
|
199
|
+
'issue.alert_high_velocity': 'High volume of file changes detected. Consider reviewing recent modifications and creating a manual snapshot.',
|
|
183
200
|
|
|
184
201
|
'check.Git installed': 'Git installed',
|
|
185
202
|
'check.Git repository': 'Git repository',
|
|
@@ -296,16 +313,18 @@ const I18N = {
|
|
|
296
313
|
'alert.action.deleted': '删除',
|
|
297
314
|
'alert.action.renamed': '重命名',
|
|
298
315
|
'alert.breakdown': '新增 {added} · 修改 {modified} · 删除 {deleted}',
|
|
299
|
-
'alert.suggestion': '建议检查近期变更,并考虑手动创建快照',
|
|
300
|
-
'alert.
|
|
301
|
-
'
|
|
302
|
-
'
|
|
303
|
-
'preWarning.
|
|
304
|
-
'preWarning.
|
|
305
|
-
'preWarning.
|
|
306
|
-
'preWarning.
|
|
307
|
-
'preWarning.
|
|
308
|
-
'
|
|
316
|
+
'alert.suggestion': '建议检查近期变更,并考虑手动创建快照',
|
|
317
|
+
'alert.dismiss': '忽略此告警',
|
|
318
|
+
'alert.dismissBusy': '正在忽略…',
|
|
319
|
+
'alert.viewFiles': '查看文件详情({n} 个文件)',
|
|
320
|
+
'preWarning.title': '事先预警',
|
|
321
|
+
'preWarning.none': '未检测到破坏性编辑风险',
|
|
322
|
+
'preWarning.active': '删除风险',
|
|
323
|
+
'preWarning.file': '文件',
|
|
324
|
+
'preWarning.risk': '风险',
|
|
325
|
+
'preWarning.methods': '移除的方法数',
|
|
326
|
+
'preWarning.suggestion': '建议在应用前检查这次删除,或直接从最新快照恢复',
|
|
327
|
+
'modal.alertFiles': '告警文件详情',
|
|
309
328
|
'modal.col.restore': '恢复',
|
|
310
329
|
'modal.copyRestore': '复制命令',
|
|
311
330
|
'modal.restorePreDelete':'恢复删除前',
|
|
@@ -322,7 +341,19 @@ const I18N = {
|
|
|
322
341
|
'backups.noBackups': '暂无恢复点',
|
|
323
342
|
'backups.col.time': '时间',
|
|
324
343
|
'backups.col.type': '类型',
|
|
344
|
+
'backups.col.meta': '范围 / 基线',
|
|
325
345
|
'backups.col.ref': '引用 / Hash',
|
|
346
|
+
'backups.meta.legacyHint': '该提交早于 Guard 范围/基线 trailer',
|
|
347
|
+
|
|
348
|
+
'backups.scope.full': '全工作区',
|
|
349
|
+
'backups.scope.narrow': '仅 protect 规则内',
|
|
350
|
+
'backups.scope.unknown': '未知',
|
|
351
|
+
|
|
352
|
+
'backups.baseline.autoBackup': '相对上次自动备份 tip',
|
|
353
|
+
'backups.baseline.snapshot': '相对上次手动快照 tip',
|
|
354
|
+
'backups.baseline.initial': '首条 Guard 提交(无父提交)',
|
|
355
|
+
'backups.baseline.other': '相对自定义引用',
|
|
356
|
+
'backups.baseline.unknown': '基线未知',
|
|
326
357
|
|
|
327
358
|
'type.git-auto-backup': 'Git 自动备份',
|
|
328
359
|
'type.git-pre-restore': 'Git 恢复前快照',
|
|
@@ -361,6 +392,8 @@ const I18N = {
|
|
|
361
392
|
'drawer.field.filesChanged': '变更文件数',
|
|
362
393
|
'drawer.field.summary': '变更摘要',
|
|
363
394
|
'drawer.field.trigger': '触发方式',
|
|
395
|
+
'drawer.field.guardScope': '快照范围',
|
|
396
|
+
'drawer.field.guardDiffBase': '差异基线',
|
|
364
397
|
'trigger.auto': '自动(定时)',
|
|
365
398
|
'trigger.manual': '手动(Agent)',
|
|
366
399
|
'trigger.pre-restore': '恢复前快照',
|
|
@@ -371,6 +404,7 @@ const I18N = {
|
|
|
371
404
|
'summary.deleted': '删除',
|
|
372
405
|
'summary.renamed': '重命名',
|
|
373
406
|
'summary.files': '个文件',
|
|
407
|
+
'summary.linesHint': '本条备份新增/删除行数合计(来自 Summary)',
|
|
374
408
|
'summary.andMore': '等 {n} 个文件…',
|
|
375
409
|
'drawer.field.intent': '操作意图',
|
|
376
410
|
'drawer.field.agent': 'AI 模型',
|
|
@@ -414,8 +448,8 @@ const I18N = {
|
|
|
414
448
|
'issue.disk_low': '磁盘空间不足({gb} GB 可用)',
|
|
415
449
|
'issue.git_backup_stale': '最近 Git 备份已过时({rel})',
|
|
416
450
|
'issue.active_alert': '活跃告警:{type}——{count} 个文件在 {window} 秒内变更',
|
|
417
|
-
'issue.pre_warning_active': '事先预警生效:{summary}',
|
|
418
|
-
'issue.alert_high_velocity': '检测到大量文件变更,建议检查最近修改并手动创建快照。',
|
|
451
|
+
'issue.pre_warning_active': '事先预警生效:{summary}',
|
|
452
|
+
'issue.alert_high_velocity': '检测到大量文件变更,建议检查最近修改并手动创建快照。',
|
|
419
453
|
|
|
420
454
|
'check.Git installed': 'Git 安装状态',
|
|
421
455
|
'check.Git repository': 'Git 仓库',
|
|
@@ -496,9 +530,13 @@ const state = {
|
|
|
496
530
|
drawerOpen: null,
|
|
497
531
|
alertHistory: loadAlertHistory(),
|
|
498
532
|
alertExpiresAt: null,
|
|
533
|
+
/** Server-Sent Events for push refresh (see /api/events); closed on project switch */
|
|
534
|
+
eventSource: null,
|
|
499
535
|
};
|
|
500
536
|
|
|
501
|
-
|
|
537
|
+
/** Rare fallback poll if SSE misses (OS watcher gaps); primary updates come from EventSource. */
|
|
538
|
+
const FALLBACK_POLL_MS_VISIBLE = 180000;
|
|
539
|
+
const FALLBACK_POLL_MS_HIDDEN = 600000;
|
|
502
540
|
const ALERT_HISTORY_KEY = 'cursorGuard_alertHistory';
|
|
503
541
|
|
|
504
542
|
function loadAlertHistory() {
|
|
@@ -573,9 +611,9 @@ const ISSUE_PATTERNS = [
|
|
|
573
611
|
{ re: /^Disk space low \((.+?) GB free\)$/, key: 'issue.disk_low', extract: ['gb'] },
|
|
574
612
|
{ re: /^Last git backup is stale \((.+?)\)$/, key: 'issue.git_backup_stale', extract: ['rel'] },
|
|
575
613
|
{ re: /^Active alert: (.+?) — (\d+) files in (\d+)s$/, key: 'issue.active_alert', extract: ['type', 'count', 'window'] },
|
|
576
|
-
{ re: /^Pre-warning active: (.+)$/, key: 'issue.pre_warning_active', extract: ['summary'] },
|
|
577
|
-
{ re: /^High volume of file changes/, key: 'issue.alert_high_velocity' },
|
|
578
|
-
];
|
|
614
|
+
{ re: /^Pre-warning active: (.+)$/, key: 'issue.pre_warning_active', extract: ['summary'] },
|
|
615
|
+
{ re: /^High volume of file changes/, key: 'issue.alert_high_velocity' },
|
|
616
|
+
];
|
|
579
617
|
|
|
580
618
|
function translateIssue(text) {
|
|
581
619
|
for (const p of ISSUE_PATTERNS) {
|
|
@@ -714,6 +752,38 @@ async function fetchJson(url) {
|
|
|
714
752
|
}
|
|
715
753
|
}
|
|
716
754
|
|
|
755
|
+
/** POST JSON API (same token query as GET). */
|
|
756
|
+
async function postJson(url) {
|
|
757
|
+
const base = window.__GUARD_BASE_URL__ || '';
|
|
758
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
759
|
+
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${encodeURIComponent(window.__GUARD_TOKEN__)}` : '';
|
|
760
|
+
const r = await fetch(base + url + tokenParam, { method: 'POST' });
|
|
761
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
762
|
+
_fetchFailCount = 0;
|
|
763
|
+
return r.json();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function dismissActiveAlert() {
|
|
767
|
+
if (!state.currentProjectId) return;
|
|
768
|
+
const btn = document.querySelector('[data-dismiss-active-alert]');
|
|
769
|
+
if (btn) {
|
|
770
|
+
btn.disabled = true;
|
|
771
|
+
btn.textContent = t('alert.dismissBusy');
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
await postJson(`/api/dismiss-alert?id=${encodeURIComponent(state.currentProjectId)}`);
|
|
775
|
+
await loadPageData();
|
|
776
|
+
renderAll();
|
|
777
|
+
} catch (e) {
|
|
778
|
+
showGlobalError(e.message || String(e));
|
|
779
|
+
} finally {
|
|
780
|
+
if (btn) {
|
|
781
|
+
btn.disabled = false;
|
|
782
|
+
btn.textContent = t('alert.dismiss');
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
717
787
|
async function loadProjects() {
|
|
718
788
|
state.projects = await fetchJson('/api/projects');
|
|
719
789
|
if (state.projects.length > 0 && !state.currentProjectId) {
|
|
@@ -764,11 +834,15 @@ async function loadPageData(opts = {}) {
|
|
|
764
834
|
|
|
765
835
|
/* ── Refresh ──────────────────────────────────────────────── */
|
|
766
836
|
|
|
837
|
+
function fallbackPollIntervalMs() {
|
|
838
|
+
return (typeof document !== 'undefined' && document.hidden) ? FALLBACK_POLL_MS_HIDDEN : FALLBACK_POLL_MS_VISIBLE;
|
|
839
|
+
}
|
|
840
|
+
|
|
767
841
|
function startRefresh() {
|
|
768
842
|
stopRefresh();
|
|
769
843
|
state.refreshTimer = setInterval(async () => {
|
|
770
844
|
try { await loadPageData(); renderAll(); } catch { /* keep existing */ }
|
|
771
|
-
},
|
|
845
|
+
}, fallbackPollIntervalMs());
|
|
772
846
|
state.tickTimer = setInterval(updateRefreshDisplay, 1000);
|
|
773
847
|
}
|
|
774
848
|
|
|
@@ -777,6 +851,41 @@ function stopRefresh() {
|
|
|
777
851
|
if (state.tickTimer) { clearInterval(state.tickTimer); state.tickTimer = null; }
|
|
778
852
|
}
|
|
779
853
|
|
|
854
|
+
function disconnectLivePush() {
|
|
855
|
+
if (state.eventSource) {
|
|
856
|
+
try { state.eventSource.close(); } catch { /* ignore */ }
|
|
857
|
+
state.eventSource = null;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Subscribe to server push (fs.watch → SSE). No token in page = skip (dev mode). */
|
|
862
|
+
function connectLivePush() {
|
|
863
|
+
disconnectLivePush();
|
|
864
|
+
if (!state.currentProjectId || typeof EventSource === 'undefined') return;
|
|
865
|
+
if (!window.__GUARD_TOKEN__) return;
|
|
866
|
+
const base = window.__GUARD_BASE_URL__ || '';
|
|
867
|
+
const id = encodeURIComponent(state.currentProjectId);
|
|
868
|
+
const tk = encodeURIComponent(window.__GUARD_TOKEN__);
|
|
869
|
+
const url = `${base}/api/events?id=${id}&token=${tk}`;
|
|
870
|
+
try {
|
|
871
|
+
const es = new EventSource(url);
|
|
872
|
+
state.eventSource = es;
|
|
873
|
+
es.onmessage = (ev) => {
|
|
874
|
+
try {
|
|
875
|
+
const msg = JSON.parse(ev.data);
|
|
876
|
+
if (msg.type === 'guard-changed' && msg.projectId === state.currentProjectId) {
|
|
877
|
+
void (async () => {
|
|
878
|
+
try {
|
|
879
|
+
await loadPageData();
|
|
880
|
+
renderAll();
|
|
881
|
+
} catch { /* keep existing UI */ }
|
|
882
|
+
})();
|
|
883
|
+
}
|
|
884
|
+
} catch { /* ignore non-json */ }
|
|
885
|
+
};
|
|
886
|
+
} catch { /* ignore */ }
|
|
887
|
+
}
|
|
888
|
+
|
|
780
889
|
async function manualRefresh() {
|
|
781
890
|
const icon = $('#refresh-btn .icon-spin');
|
|
782
891
|
if (icon) icon.classList.add('icon-spin-active');
|
|
@@ -785,6 +894,7 @@ async function manualRefresh() {
|
|
|
785
894
|
catch (e) { showGlobalError(e.message); }
|
|
786
895
|
if (icon) icon.classList.remove('icon-spin-active');
|
|
787
896
|
startRefresh();
|
|
897
|
+
connectLivePush();
|
|
788
898
|
}
|
|
789
899
|
|
|
790
900
|
function updateRefreshDisplay() {
|
|
@@ -867,14 +977,14 @@ function renderAll() {
|
|
|
867
977
|
|
|
868
978
|
/* ── Rendering: Overview ──────────────────────────────────── */
|
|
869
979
|
|
|
870
|
-
function renderOverview(d) {
|
|
871
|
-
renderHealthCard(d.health);
|
|
872
|
-
renderGitBackupCard(d.lastBackup);
|
|
873
|
-
renderShadowBackupCard(d.lastBackup);
|
|
874
|
-
renderPreWarningCard(d.preWarnings);
|
|
875
|
-
renderWatcherCard(d.watcher);
|
|
876
|
-
renderAlertCard(d.alerts);
|
|
877
|
-
}
|
|
980
|
+
function renderOverview(d) {
|
|
981
|
+
renderHealthCard(d.health);
|
|
982
|
+
renderGitBackupCard(d.lastBackup);
|
|
983
|
+
renderShadowBackupCard(d.lastBackup);
|
|
984
|
+
renderPreWarningCard(d.preWarnings);
|
|
985
|
+
renderWatcherCard(d.watcher);
|
|
986
|
+
renderAlertCard(d.alerts);
|
|
987
|
+
}
|
|
878
988
|
|
|
879
989
|
function renderHealthCard(health) {
|
|
880
990
|
const el = $('#card-health');
|
|
@@ -907,9 +1017,9 @@ function renderGitBackupCard(lastBackup) {
|
|
|
907
1017
|
`;
|
|
908
1018
|
}
|
|
909
1019
|
|
|
910
|
-
function renderShadowBackupCard(lastBackup) {
|
|
911
|
-
const el = $('#card-shadow-backup');
|
|
912
|
-
const s = lastBackup?.shadow;
|
|
1020
|
+
function renderShadowBackupCard(lastBackup) {
|
|
1021
|
+
const el = $('#card-shadow-backup');
|
|
1022
|
+
const s = lastBackup?.shadow;
|
|
913
1023
|
if (!s) {
|
|
914
1024
|
el.innerHTML = `<div class="card-label">${t('shadowBackup.title')}</div><div class="card-empty">${t('shadowBackup.none')}</div>`;
|
|
915
1025
|
return;
|
|
@@ -917,40 +1027,40 @@ function renderShadowBackupCard(lastBackup) {
|
|
|
917
1027
|
el.innerHTML = `
|
|
918
1028
|
<div class="card-label">${t('shadowBackup.title')}</div>
|
|
919
1029
|
<div class="card-value">${esc(relativeTime(s.timestamp))}</div>
|
|
920
|
-
<div class="card-detail text-muted text-sm">${esc(formatTime(s.timestamp))}</div>
|
|
921
|
-
`;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function renderPreWarningCard(preWarnings) {
|
|
925
|
-
const el = $('#card-prewarning');
|
|
926
|
-
if (!preWarnings?.active) {
|
|
927
|
-
el.innerHTML = `
|
|
928
|
-
<div class="card-label">${t('preWarning.title')}</div>
|
|
929
|
-
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('preWarning.none')}</span></div>
|
|
930
|
-
`;
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const warning = preWarnings.latest || {};
|
|
935
|
-
const file = warning.file || '-';
|
|
936
|
-
const risk = warning.riskPercent !== undefined ? `${warning.riskPercent}%` : '-';
|
|
937
|
-
const methods = warning.removedMethodCount || 0;
|
|
938
|
-
|
|
939
|
-
el.innerHTML = `
|
|
940
|
-
<div class="card-label">${t('preWarning.title')}</div>
|
|
941
|
-
<div class="card-status"><span class="status-dot status-warning"></span><span class="status-text status-warning">${t('preWarning.active')}</span></div>
|
|
942
|
-
<div class="alert-details">
|
|
943
|
-
<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.file')}</span><span class="text-mono">${esc(file)}</span></div>
|
|
944
|
-
<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.risk')}</span><span>${esc(risk)}</span></div>
|
|
945
|
-
${methods > 0 ? `<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.methods')}</span><span>${methods}</span></div>` : ''}
|
|
946
|
-
<div class="alert-detail-row alert-breakdown text-sm">${esc(warning.summary || t('preWarning.suggestion'))}</div>
|
|
947
|
-
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('preWarning.suggestion')}</div>
|
|
948
|
-
</div>
|
|
949
|
-
`;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function renderWatcherCard(watcher) {
|
|
953
|
-
const el = $('#card-watcher');
|
|
1030
|
+
<div class="card-detail text-muted text-sm">${esc(formatTime(s.timestamp))}</div>
|
|
1031
|
+
`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function renderPreWarningCard(preWarnings) {
|
|
1035
|
+
const el = $('#card-prewarning');
|
|
1036
|
+
if (!preWarnings?.active) {
|
|
1037
|
+
el.innerHTML = `
|
|
1038
|
+
<div class="card-label">${t('preWarning.title')}</div>
|
|
1039
|
+
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('preWarning.none')}</span></div>
|
|
1040
|
+
`;
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const warning = preWarnings.latest || {};
|
|
1045
|
+
const file = warning.file || '-';
|
|
1046
|
+
const risk = warning.riskPercent !== undefined ? `${warning.riskPercent}%` : '-';
|
|
1047
|
+
const methods = warning.removedMethodCount || 0;
|
|
1048
|
+
|
|
1049
|
+
el.innerHTML = `
|
|
1050
|
+
<div class="card-label">${t('preWarning.title')}</div>
|
|
1051
|
+
<div class="card-status"><span class="status-dot status-warning"></span><span class="status-text status-warning">${t('preWarning.active')}</span></div>
|
|
1052
|
+
<div class="alert-details">
|
|
1053
|
+
<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.file')}</span><span class="text-mono">${esc(file)}</span></div>
|
|
1054
|
+
<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.risk')}</span><span>${esc(risk)}</span></div>
|
|
1055
|
+
${methods > 0 ? `<div class="alert-detail-row"><span class="alert-detail-label">${t('preWarning.methods')}</span><span>${methods}</span></div>` : ''}
|
|
1056
|
+
<div class="alert-detail-row alert-breakdown text-sm">${esc(warning.summary || t('preWarning.suggestion'))}</div>
|
|
1057
|
+
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('preWarning.suggestion')}</div>
|
|
1058
|
+
</div>
|
|
1059
|
+
`;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function renderWatcherCard(watcher) {
|
|
1063
|
+
const el = $('#card-watcher');
|
|
954
1064
|
let st = 'stopped';
|
|
955
1065
|
if (watcher?.running) st = 'running';
|
|
956
1066
|
else if (watcher?.stale) st = 'stale';
|
|
@@ -1079,7 +1189,10 @@ function renderAlertCard(alerts) {
|
|
|
1079
1189
|
${alertFileBreakdown(files) ? `<div class="alert-detail-row alert-breakdown text-sm">${esc(alertFileBreakdown(files))}</div>` : ''}
|
|
1080
1190
|
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('alert.suggestion')}</div>
|
|
1081
1191
|
</div>
|
|
1082
|
-
|
|
1192
|
+
<div class="alert-card-actions">
|
|
1193
|
+
${filesHtml}
|
|
1194
|
+
<button type="button" class="btn-alert-dismiss" data-dismiss-active-alert>${t('alert.dismiss')}</button>
|
|
1195
|
+
</div>
|
|
1083
1196
|
${buildAlertHistoryHtml()}
|
|
1084
1197
|
`;
|
|
1085
1198
|
}
|
|
@@ -1147,26 +1260,56 @@ function translateSummary(raw) {
|
|
|
1147
1260
|
.replace(/\bRenamed (\d+)/g, (_, n) => `${t('summary.renamed')} ${n}`);
|
|
1148
1261
|
}
|
|
1149
1262
|
|
|
1263
|
+
/** Strip CR / normalize newlines so per-line trailers and Summary parse reliably on Windows. */
|
|
1264
|
+
function normalizeSummaryText(summary) {
|
|
1265
|
+
if (!summary) return '';
|
|
1266
|
+
return String(summary).replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Sum all (+added -deleted) hunks in a Guard Summary line (same semantics as inline file rows).
|
|
1271
|
+
* @returns {{ added: number, deleted: number, matches: number }|null}
|
|
1272
|
+
*/
|
|
1273
|
+
function sumDeltaFromSummary(summary) {
|
|
1274
|
+
const s = normalizeSummaryText(summary);
|
|
1275
|
+
if (!s) return null;
|
|
1276
|
+
let added = 0;
|
|
1277
|
+
let deleted = 0;
|
|
1278
|
+
let matches = 0;
|
|
1279
|
+
const re = /\(\+(\d+)\s*-\s*(\d+)\)/g;
|
|
1280
|
+
let m;
|
|
1281
|
+
while ((m = re.exec(s)) !== null) {
|
|
1282
|
+
added += parseInt(m[1], 10) || 0;
|
|
1283
|
+
deleted += parseInt(m[2], 10) || 0;
|
|
1284
|
+
matches++;
|
|
1285
|
+
}
|
|
1286
|
+
if (matches === 0) return null;
|
|
1287
|
+
return { added, deleted, matches };
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1150
1290
|
/**
|
|
1151
1291
|
* Parse summary text into structured file array for inline preview.
|
|
1152
1292
|
* Format: "Modified 3: a.js (+2 -1), b.js (+0 -5), ...; Added 2: c.js (+10 -0), d.js (+3 -0)"
|
|
1153
1293
|
*/
|
|
1154
1294
|
function parseSummaryToFiles(summary) {
|
|
1155
|
-
|
|
1295
|
+
const normalized = normalizeSummaryText(summary);
|
|
1296
|
+
if (!normalized) return [];
|
|
1156
1297
|
const ACTION_MAP = { Modified: 'modified', Added: 'added', Deleted: 'deleted', Renamed: 'renamed' };
|
|
1157
1298
|
const files = [];
|
|
1158
|
-
for (const segment of
|
|
1159
|
-
const
|
|
1299
|
+
for (const segment of normalized.split('; ')) {
|
|
1300
|
+
const seg = segment.trim();
|
|
1301
|
+
const headerMatch = seg.match(/^(Modified|Added|Deleted|Renamed)\s+\d+:\s*/);
|
|
1160
1302
|
if (!headerMatch) continue;
|
|
1161
1303
|
const action = ACTION_MAP[headerMatch[1]] || 'modified';
|
|
1162
|
-
const rest =
|
|
1304
|
+
const rest = seg.slice(headerMatch[0].length);
|
|
1163
1305
|
for (const part of rest.split(/,\s*/)) {
|
|
1164
|
-
|
|
1165
|
-
|
|
1306
|
+
const p = part.trim();
|
|
1307
|
+
if (!p || p === '...') continue;
|
|
1308
|
+
const fileMatch = p.match(/^(.+?)\s*\(\+(\d+)\s*-\s*(\d+)\)\s*$/);
|
|
1166
1309
|
if (fileMatch) {
|
|
1167
|
-
files.push({ path: fileMatch[1], action, added: parseInt(fileMatch[2], 10), deleted: parseInt(fileMatch[3], 10) });
|
|
1168
|
-
} else
|
|
1169
|
-
files.push({ path:
|
|
1310
|
+
files.push({ path: fileMatch[1].trim(), action, added: parseInt(fileMatch[2], 10), deleted: parseInt(fileMatch[3], 10) });
|
|
1311
|
+
} else {
|
|
1312
|
+
files.push({ path: p, action, added: 0, deleted: 0 });
|
|
1170
1313
|
}
|
|
1171
1314
|
}
|
|
1172
1315
|
}
|
|
@@ -1191,13 +1334,22 @@ function formatFileActionBadge(action) {
|
|
|
1191
1334
|
}
|
|
1192
1335
|
|
|
1193
1336
|
function formatSummaryCell(b) {
|
|
1337
|
+
const deltaTotals = sumDeltaFromSummary(b.summary);
|
|
1338
|
+
const deltaHtml = deltaTotals
|
|
1339
|
+
? `<span class="summary-line-delta text-mono" title="${esc(t('summary.linesHint'))}">+${deltaTotals.added} -${deltaTotals.deleted}</span>`
|
|
1340
|
+
: '';
|
|
1341
|
+
|
|
1194
1342
|
let line1 = '';
|
|
1195
1343
|
if (b.filesChanged != null) {
|
|
1196
|
-
line1 = `<div class="summary-meta"><span class="summary-files">${b.filesChanged} ${t('summary.files')}</span
|
|
1344
|
+
line1 = `<div class="summary-meta"><span class="summary-files">${b.filesChanged} ${t('summary.files')}</span>${deltaHtml}</div>`;
|
|
1345
|
+
} else if (deltaHtml) {
|
|
1346
|
+
line1 = `<div class="summary-meta">${deltaHtml}</div>`;
|
|
1197
1347
|
}
|
|
1198
1348
|
|
|
1199
1349
|
let line2 = '';
|
|
1200
|
-
|
|
1350
|
+
// From / Restore-To 仅用于「恢复前快照」语义;自动备份/手动快照若误带同名 trailer(如 Summary 换行),不应抢走 Intent 展示。
|
|
1351
|
+
const isPreRestoreType = b.type === 'git-pre-restore' || b.type === 'shadow-pre-restore';
|
|
1352
|
+
if (isPreRestoreType && b.from && b.restoreTo) {
|
|
1201
1353
|
const label = b.restoreFile ? `${esc(b.restoreFile)}: ` : '';
|
|
1202
1354
|
line2 = `<div class="summary-restore-ctx">${label}<span class="text-mono">${esc(b.from)}</span> → <span class="text-mono">${esc(b.restoreTo)}</span></div>`;
|
|
1203
1355
|
} else if (b.intent) {
|
|
@@ -1225,7 +1377,7 @@ function formatSummaryCell(b) {
|
|
|
1225
1377
|
const moreHtml = (remaining > 0 || truncated) ? `<div class="summary-file-more text-sm text-muted">${t('summary.andMore', { n: moreCount > 0 ? moreCount : '…' })}</div>` : '';
|
|
1226
1378
|
line3 = visible + moreHtml;
|
|
1227
1379
|
} else {
|
|
1228
|
-
const categories = b.summary.split('; ').map(s => translateSummary(s));
|
|
1380
|
+
const categories = normalizeSummaryText(b.summary).split('; ').map(s => translateSummary(s.trim())).filter(Boolean);
|
|
1229
1381
|
line3 = categories.slice(0, 2).map(c => `<div class="summary-detail-line">${esc(c)}</div>`).join('');
|
|
1230
1382
|
}
|
|
1231
1383
|
}
|
|
@@ -1234,6 +1386,36 @@ function formatSummaryCell(b) {
|
|
|
1234
1386
|
return `<div class="summary-stack">${line1}${line2}${line3}</div>`;
|
|
1235
1387
|
}
|
|
1236
1388
|
|
|
1389
|
+
function formatGuardScopeLabel(scope) {
|
|
1390
|
+
if (scope === 'full') return t('backups.scope.full');
|
|
1391
|
+
if (scope === 'narrow') return t('backups.scope.narrow');
|
|
1392
|
+
return t('backups.scope.unknown');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function formatGuardBaselineLabel(base) {
|
|
1396
|
+
const map = {
|
|
1397
|
+
'auto-backup': 'backups.baseline.autoBackup',
|
|
1398
|
+
snapshot: 'backups.baseline.snapshot',
|
|
1399
|
+
initial: 'backups.baseline.initial',
|
|
1400
|
+
other: 'backups.baseline.other',
|
|
1401
|
+
};
|
|
1402
|
+
const k = map[base];
|
|
1403
|
+
return k ? t(k) : t('backups.baseline.unknown');
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** Git backup rows only: scope + shared-baseline hint from commit trailers */
|
|
1407
|
+
function formatBackupHumanMeta(b) {
|
|
1408
|
+
const show = b.type === 'git-auto-backup' || b.type === 'git-snapshot';
|
|
1409
|
+
if (!show) return '<span class="text-muted text-sm">-</span>';
|
|
1410
|
+
if (b.guardScope == null && b.guardDiffBase == null) {
|
|
1411
|
+
return `<span class="text-muted text-sm" title="${esc(t('backups.meta.legacyHint'))}">-</span>`;
|
|
1412
|
+
}
|
|
1413
|
+
const lines = [];
|
|
1414
|
+
if (b.guardScope != null) lines.push(`<span class="backup-meta-line">${esc(formatGuardScopeLabel(b.guardScope))}</span>`);
|
|
1415
|
+
if (b.guardDiffBase != null) lines.push(`<span class="backup-meta-line">${esc(formatGuardBaselineLabel(b.guardDiffBase))}</span>`);
|
|
1416
|
+
return `<div class="backup-human-meta">${lines.join('')}</div>`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1237
1419
|
function renderBackupTable(backups) {
|
|
1238
1420
|
if (!Array.isArray(backups)) {
|
|
1239
1421
|
$('#backup-table-wrap').innerHTML = `<div class="error-panel">${t('error.sectionFailed')}</div>`;
|
|
@@ -1249,7 +1431,9 @@ function renderBackupTable(backups) {
|
|
|
1249
1431
|
(b.summary && b.summary.toLowerCase().includes(q)) ||
|
|
1250
1432
|
(b.message && b.message.toLowerCase().includes(q)) ||
|
|
1251
1433
|
(b.intent && b.intent.toLowerCase().includes(q)) ||
|
|
1252
|
-
(b.restoreFile && b.restoreFile.toLowerCase().includes(q))
|
|
1434
|
+
(b.restoreFile && b.restoreFile.toLowerCase().includes(q)) ||
|
|
1435
|
+
(b.guardScope && String(b.guardScope).toLowerCase().includes(q)) ||
|
|
1436
|
+
(b.guardDiffBase && String(b.guardDiffBase).toLowerCase().includes(q))
|
|
1253
1437
|
);
|
|
1254
1438
|
}
|
|
1255
1439
|
state.filteredBackups = filtered;
|
|
@@ -1262,9 +1446,11 @@ function renderBackupTable(backups) {
|
|
|
1262
1446
|
const rows = state.filteredBackups.map((b, i) => {
|
|
1263
1447
|
const badgeClass = b.type.startsWith('git') ? (b.type.includes('pre') ? 'badge-pre' : 'badge-git') : (b.type.includes('pre') ? 'badge-pre' : 'badge-shadow');
|
|
1264
1448
|
const summaryCell = formatSummaryCell(b);
|
|
1449
|
+
const metaCell = formatBackupHumanMeta(b);
|
|
1265
1450
|
return `<tr data-bi="${i}">
|
|
1266
1451
|
<td><div>${esc(formatTime(b.timestamp))}</div><div class="text-muted text-sm">${esc(relativeTime(b.timestamp))}</div></td>
|
|
1267
1452
|
<td><span class="badge ${badgeClass}">${t('type.' + b.type)}</span></td>
|
|
1453
|
+
<td class="backup-meta-cell">${metaCell}</td>
|
|
1268
1454
|
<td class="text-mono">${esc(b.shortHash || b.timestamp || '-')}</td>
|
|
1269
1455
|
<td class="backup-summary-cell">${summaryCell}</td>
|
|
1270
1456
|
</tr>`;
|
|
@@ -1275,6 +1461,7 @@ function renderBackupTable(backups) {
|
|
|
1275
1461
|
<thead><tr>
|
|
1276
1462
|
<th>${t('backups.col.time')}</th>
|
|
1277
1463
|
<th>${t('backups.col.type')}</th>
|
|
1464
|
+
<th>${t('backups.col.meta')}</th>
|
|
1278
1465
|
<th>${t('backups.col.ref')}</th>
|
|
1279
1466
|
<th>${t('backups.col.summary')}</th>
|
|
1280
1467
|
</tr></thead>
|
|
@@ -1295,18 +1482,18 @@ function renderProtection(scope) {
|
|
|
1295
1482
|
|
|
1296
1483
|
el.innerHTML = `
|
|
1297
1484
|
<div class="protection-grid">
|
|
1298
|
-
<div class="pattern-card">
|
|
1485
|
+
<div class="pattern-card pattern-card--protect">
|
|
1299
1486
|
<h4>${t('protection.protect')}</h4>
|
|
1300
1487
|
${isAll
|
|
1301
|
-
? `<p class="
|
|
1302
|
-
: `<ul class="pattern-list">${protectList.map(p => `<li class="pattern-item">${esc(p)}</li>`).join('')}</ul>`
|
|
1488
|
+
? `<p class="pattern-empty pattern-empty--protect text-sm">${t('protection.allFiles')}</p>`
|
|
1489
|
+
: `<ul class="pattern-list">${protectList.map(p => `<li class="pattern-item pattern-item--protect">${esc(p)}</li>`).join('')}</ul>`
|
|
1303
1490
|
}
|
|
1304
1491
|
</div>
|
|
1305
|
-
<div class="pattern-card">
|
|
1492
|
+
<div class="pattern-card pattern-card--ignore">
|
|
1306
1493
|
<h4>${t('protection.ignore')}</h4>
|
|
1307
1494
|
${ignoreList.length === 0
|
|
1308
|
-
? `<p class="
|
|
1309
|
-
: `<ul class="pattern-list">${ignoreList.map(p => `<li class="pattern-item">${esc(p)}</li>`).join('')}</ul>`
|
|
1495
|
+
? `<p class="pattern-empty pattern-empty--ignore text-sm">${t('protection.noIgnore')}</p>`
|
|
1496
|
+
: `<ul class="pattern-list">${ignoreList.map(p => `<li class="pattern-item pattern-item--ignore">${esc(p)}</li>`).join('')}</ul>`
|
|
1310
1497
|
}
|
|
1311
1498
|
</div>
|
|
1312
1499
|
</div>
|
|
@@ -1459,6 +1646,12 @@ function openRestoreDrawer(backup) {
|
|
|
1459
1646
|
{ key: 'drawer.field.type', val: t('type.' + backup.type) },
|
|
1460
1647
|
];
|
|
1461
1648
|
if (backup.trigger) fields.push({ key: 'drawer.field.trigger', val: t('trigger.' + backup.trigger) });
|
|
1649
|
+
if (backup.guardScope != null) {
|
|
1650
|
+
fields.push({ key: 'drawer.field.guardScope', val: formatGuardScopeLabel(backup.guardScope) });
|
|
1651
|
+
}
|
|
1652
|
+
if (backup.guardDiffBase != null) {
|
|
1653
|
+
fields.push({ key: 'drawer.field.guardDiffBase', val: formatGuardBaselineLabel(backup.guardDiffBase) });
|
|
1654
|
+
}
|
|
1462
1655
|
if (backup.filesChanged != null) fields.push({ key: 'drawer.field.filesChanged', val: String(backup.filesChanged) });
|
|
1463
1656
|
if (backup.ref) fields.push({ key: 'drawer.field.ref', val: backup.ref });
|
|
1464
1657
|
if (backup.commitHash) fields.push({ key: 'drawer.field.hash', val: backup.commitHash });
|
|
@@ -1634,16 +1827,36 @@ function setupEvents() {
|
|
|
1634
1827
|
$('#project-select').addEventListener('change', async (e) => {
|
|
1635
1828
|
state.currentProjectId = e.target.value;
|
|
1636
1829
|
state.backupFilter = 'all';
|
|
1830
|
+
disconnectLivePush();
|
|
1637
1831
|
stopRefresh();
|
|
1638
1832
|
showSkeleton();
|
|
1639
1833
|
try { await loadPageData(); renderAll(); }
|
|
1640
1834
|
catch (err) { showGlobalError(err.message); }
|
|
1641
1835
|
startRefresh();
|
|
1836
|
+
connectLivePush();
|
|
1642
1837
|
});
|
|
1643
1838
|
|
|
1644
1839
|
$('#refresh-btn').addEventListener('click', manualRefresh);
|
|
1645
1840
|
$('#error-retry').addEventListener('click', manualRefresh);
|
|
1646
1841
|
|
|
1842
|
+
document.addEventListener('visibilitychange', () => {
|
|
1843
|
+
if (!state.currentProjectId) return;
|
|
1844
|
+
if (document.hidden) {
|
|
1845
|
+
stopRefresh();
|
|
1846
|
+
startRefresh();
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
void (async () => {
|
|
1850
|
+
try {
|
|
1851
|
+
await loadPageData();
|
|
1852
|
+
renderAll();
|
|
1853
|
+
} catch { /* keep existing UI */ }
|
|
1854
|
+
})();
|
|
1855
|
+
stopRefresh();
|
|
1856
|
+
startRefresh();
|
|
1857
|
+
connectLivePush();
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1647
1860
|
$('#lang-toggle').addEventListener('click', () => {
|
|
1648
1861
|
setLocale(state.locale === 'zh-CN' ? 'en-US' : 'zh-CN');
|
|
1649
1862
|
});
|
|
@@ -1703,6 +1916,10 @@ function setupEvents() {
|
|
|
1703
1916
|
}
|
|
1704
1917
|
return;
|
|
1705
1918
|
}
|
|
1919
|
+
if (e.target.closest('[data-dismiss-active-alert]')) {
|
|
1920
|
+
e.preventDefault();
|
|
1921
|
+
dismissActiveAlert();
|
|
1922
|
+
}
|
|
1706
1923
|
});
|
|
1707
1924
|
|
|
1708
1925
|
// Diagnostics summary click
|
|
@@ -1820,6 +2037,7 @@ async function init() {
|
|
|
1820
2037
|
await loadPageData({ progressive: true });
|
|
1821
2038
|
renderAll();
|
|
1822
2039
|
startRefresh();
|
|
2040
|
+
connectLivePush();
|
|
1823
2041
|
checkServerVersion();
|
|
1824
2042
|
} catch (e) {
|
|
1825
2043
|
showGlobalError(e.message);
|