cursor-guard 4.5.9 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ROADMAP.md +24 -2
- package/package.json +1 -1
- package/references/dashboard/public/app.js +53 -26
- package/references/lib/core/dashboard.js +5 -5
package/ROADMAP.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
|
|
4
4
|
> 每一代向下兼容,低版本功能永远不废弃。
|
|
5
5
|
>
|
|
6
|
-
> **当前版本**:`V4.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.
|
|
6
|
+
> **当前版本**:`V4.6.0`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.6.0` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -465,6 +465,7 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
465
465
|
| V4.5.6 | **Bug 修复 + 告警 UX + init 优化**:见下方详细说明 | ✅ |
|
|
466
466
|
| V4.5.7 | **文件详情 Modal 修复 + Dashboard 端口复用**:见下方详细说明 | ✅ |
|
|
467
467
|
| V4.5.8 | **Dashboard 版本更新检测 + 一键重启**:见下方详细说明 | ✅ |
|
|
468
|
+
| V4.6.0 | **告警 UX 全面升级**:见下方详细说明 | ✅ |
|
|
468
469
|
|
|
469
470
|
#### V4.4.1 详细内容
|
|
470
471
|
|
|
@@ -685,6 +686,18 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
685
686
|
| 重启流程 | 前端发 POST `/api/restart` → 显示"正在重启..." → 每 500ms 轮询 `/api/version`(最多 10s)→ 服务就绪后 `location.reload()` 自动刷新页面 |
|
|
686
687
|
| 双语支持 | 新增 `upgrade.*` 共 8 组 i18n key(banner/dismiss/restartNow/restart/hint/restarting/waiting/failed) |
|
|
687
688
|
|
|
689
|
+
#### V4.6.0 详细内容
|
|
690
|
+
|
|
691
|
+
**告警 UX 全面升级**:
|
|
692
|
+
|
|
693
|
+
| 修复项 | 说明 |
|
|
694
|
+
|------|------|
|
|
695
|
+
| 倒计时秒级实时更新 | `state.alertExpiresAt` 在 `renderAlertCard` 时记录过期时间;已有的 1 秒定时器 `updateRefreshDisplay` 中实时更新 `.alert-countdown` 元素,无需额外 `setInterval` |
|
|
696
|
+
| 告警文件详情复制/恢复操作 | 告警详情弹窗现在传入 `commitHash`(从 `alerts.latest.commitHash` 或 `'HEAD'` 回退),使每行文件的"复制恢复命令"按钮正常显示和工作 |
|
|
697
|
+
| 备份过时阈值修正 | 阈值从 `interval * 5 / 60` 分钟改为 `max(interval * 10, 300)` 秒(至少 5 分钟);仅在 watcher 正在运行时才检查过时状态;文案从"stale"改为中性的"Last git backup: X ago" |
|
|
698
|
+
| 告警历史常驻入口 | `buildAlertHistoryHtml()` 提取为独立函数,在活跃告警和无告警两种状态下都渲染"历史 N 条"折叠按钮,告警激活时也能查看历史 |
|
|
699
|
+
| 告警历史 localStorage 持久化 | 新增 `loadAlertHistory()` / `saveAlertHistory()`,使用 `localStorage` key `cursorGuard_alertHistory` 持久化最近 20 条告警历史,刷新页面不丢失 |
|
|
700
|
+
|
|
688
701
|
#### V4.5.x 新增配置参考
|
|
689
702
|
|
|
690
703
|
| 字段 | 类型 | 默认值 | 引入版本 | 说明 |
|
|
@@ -709,6 +722,15 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
709
722
|
}
|
|
710
723
|
```
|
|
711
724
|
|
|
725
|
+
### V4.7 规划:IDE 集成(VSCode/Cursor Extension)
|
|
726
|
+
|
|
727
|
+
| 项目 | 说明 |
|
|
728
|
+
|------|------|
|
|
729
|
+
| 定位 | 将 Dashboard 从浏览器迁移到 IDE 内置 WebView,减少上下文切换 |
|
|
730
|
+
| 实现方式 | VSCode Extension + WebView Panel,复用现有 `dashboard/public/` 前端代码 |
|
|
731
|
+
| 核心能力 | IDE 侧边栏入口、状态栏告警指示器、WebView 内嵌 Dashboard、一键快照按钮 |
|
|
732
|
+
| 状态 | 🔮 规划中 |
|
|
733
|
+
|
|
712
734
|
### V4 不做的事
|
|
713
735
|
|
|
714
736
|
- 不做自动恢复(恢复永远需要人确认,这是产品底线)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Protects code from accidental AI overwrite or deletion in Cursor IDE — mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | 保护代码免受 Cursor AI 代理意外覆写或删除——强制写前快照、预览再执行、本地 Git 安全网、确定性恢复。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
@@ -470,10 +470,23 @@ const state = {
|
|
|
470
470
|
tickTimer: null,
|
|
471
471
|
lastRefreshAt: null,
|
|
472
472
|
drawerOpen: null,
|
|
473
|
-
alertHistory:
|
|
473
|
+
alertHistory: loadAlertHistory(),
|
|
474
|
+
alertExpiresAt: null,
|
|
474
475
|
};
|
|
475
476
|
|
|
476
477
|
const REFRESH_MS = 15000;
|
|
478
|
+
const ALERT_HISTORY_KEY = 'cursorGuard_alertHistory';
|
|
479
|
+
|
|
480
|
+
function loadAlertHistory() {
|
|
481
|
+
try {
|
|
482
|
+
const raw = localStorage.getItem(ALERT_HISTORY_KEY);
|
|
483
|
+
return raw ? JSON.parse(raw) : [];
|
|
484
|
+
} catch { return []; }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function saveAlertHistory() {
|
|
488
|
+
try { localStorage.setItem(ALERT_HISTORY_KEY, JSON.stringify(state.alertHistory)); } catch { /* quota */ }
|
|
489
|
+
}
|
|
477
490
|
|
|
478
491
|
/* ── DOM helpers ──────────────────────────────────────────── */
|
|
479
492
|
|
|
@@ -737,9 +750,17 @@ async function manualRefresh() {
|
|
|
737
750
|
|
|
738
751
|
function updateRefreshDisplay() {
|
|
739
752
|
const el = $('#last-refresh');
|
|
740
|
-
if (
|
|
741
|
-
|
|
742
|
-
|
|
753
|
+
if (el && state.lastRefreshAt) {
|
|
754
|
+
const sec = Math.floor((Date.now() - state.lastRefreshAt) / 1000);
|
|
755
|
+
el.textContent = `${t('topbar.lastRefresh')}: ${sec}s`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const cdEl = document.querySelector('.alert-countdown');
|
|
759
|
+
if (cdEl && state.alertExpiresAt) {
|
|
760
|
+
const remain = Math.max(0, state.alertExpiresAt - Date.now());
|
|
761
|
+
const s = Math.ceil(remain / 1000);
|
|
762
|
+
cdEl.textContent = s > 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
|
|
763
|
+
}
|
|
743
764
|
}
|
|
744
765
|
|
|
745
766
|
/* ── Rendering: Top bar ───────────────────────────────────── */
|
|
@@ -889,33 +910,35 @@ function alertFileBreakdown(files) {
|
|
|
889
910
|
return t('alert.breakdown', { added, modified, deleted });
|
|
890
911
|
}
|
|
891
912
|
|
|
913
|
+
function buildAlertHistoryHtml() {
|
|
914
|
+
if (state.alertHistory.length === 0) return '';
|
|
915
|
+
const count = state.alertHistory.length;
|
|
916
|
+
const rows = state.alertHistory.slice(-5).reverse().map(h => {
|
|
917
|
+
const breakdown = alertFileBreakdown(h.files);
|
|
918
|
+
return `<div class="alert-history-row text-sm text-muted">
|
|
919
|
+
<span class="alert-history-time">${esc(formatTime(h.timestamp))}</span>
|
|
920
|
+
<span>${t('alert.detail', { count: h.fileCount, window: h.windowSeconds, threshold: h.threshold })}</span>
|
|
921
|
+
${breakdown ? `<span class="alert-history-breakdown">${esc(breakdown)}</span>` : ''}
|
|
922
|
+
<span class="badge badge-expired">${t('alert.expired')}</span>
|
|
923
|
+
</div>`;
|
|
924
|
+
}).join('');
|
|
925
|
+
return `
|
|
926
|
+
<div class="alert-history-toggle-wrap">
|
|
927
|
+
<button class="alert-history-toggle-btn text-sm text-muted" data-alert-history-toggle>${t('alert.historyCount', { n: count })}</button>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="alert-history alert-history-collapsed">
|
|
930
|
+
<div class="alert-history-label text-sm">${t('alert.history')}</div>${rows}
|
|
931
|
+
</div>`;
|
|
932
|
+
}
|
|
933
|
+
|
|
892
934
|
function renderAlertCard(alerts) {
|
|
893
935
|
const el = $('#card-alert');
|
|
894
936
|
if (!alerts?.active) {
|
|
895
|
-
|
|
896
|
-
if (state.alertHistory.length > 0) {
|
|
897
|
-
const count = state.alertHistory.length;
|
|
898
|
-
const rows = state.alertHistory.slice(-5).reverse().map(h => {
|
|
899
|
-
const breakdown = alertFileBreakdown(h.files);
|
|
900
|
-
return `<div class="alert-history-row text-sm text-muted">
|
|
901
|
-
<span class="alert-history-time">${esc(formatTime(h.timestamp))}</span>
|
|
902
|
-
<span>${t('alert.detail', { count: h.fileCount, window: h.windowSeconds, threshold: h.threshold })}</span>
|
|
903
|
-
${breakdown ? `<span class="alert-history-breakdown">${esc(breakdown)}</span>` : ''}
|
|
904
|
-
<span class="badge badge-expired">${t('alert.expired')}</span>
|
|
905
|
-
</div>`;
|
|
906
|
-
}).join('');
|
|
907
|
-
historyHtml = `
|
|
908
|
-
<div class="alert-history-toggle-wrap">
|
|
909
|
-
<button class="alert-history-toggle-btn text-sm text-muted" data-alert-history-toggle>${t('alert.historyCount', { n: count })}</button>
|
|
910
|
-
</div>
|
|
911
|
-
<div class="alert-history alert-history-collapsed">
|
|
912
|
-
<div class="alert-history-label text-sm">${t('alert.history')}</div>${rows}
|
|
913
|
-
</div>`;
|
|
914
|
-
}
|
|
937
|
+
state.alertExpiresAt = null;
|
|
915
938
|
el.innerHTML = `
|
|
916
939
|
<div class="card-label">${t('alert.title')}</div>
|
|
917
940
|
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('alert.none')}</span></div>
|
|
918
|
-
${
|
|
941
|
+
${buildAlertHistoryHtml()}
|
|
919
942
|
`;
|
|
920
943
|
return;
|
|
921
944
|
}
|
|
@@ -925,10 +948,12 @@ function renderAlertCard(alerts) {
|
|
|
925
948
|
if (a.timestamp && !state.alertHistory.some(h => h.timestamp === a.timestamp)) {
|
|
926
949
|
state.alertHistory.push({ timestamp: a.timestamp, fileCount: a.fileCount, windowSeconds: a.windowSeconds, threshold: a.threshold, expiresAt: a.expiresAt, files: a.files });
|
|
927
950
|
if (state.alertHistory.length > 20) state.alertHistory = state.alertHistory.slice(-20);
|
|
951
|
+
saveAlertHistory();
|
|
928
952
|
}
|
|
929
953
|
|
|
930
954
|
const triggeredAt = a.timestamp ? formatTime(a.timestamp) : '-';
|
|
931
955
|
const expiresAt = a.expiresAt ? new Date(a.expiresAt) : null;
|
|
956
|
+
state.alertExpiresAt = expiresAt ? expiresAt.getTime() : null;
|
|
932
957
|
const remainMs = expiresAt ? expiresAt.getTime() - Date.now() : 0;
|
|
933
958
|
const remainSec = Math.max(0, Math.ceil(remainMs / 1000));
|
|
934
959
|
const remainMin = Math.floor(remainSec / 60);
|
|
@@ -956,6 +981,7 @@ function renderAlertCard(alerts) {
|
|
|
956
981
|
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('alert.suggestion')}</div>
|
|
957
982
|
</div>
|
|
958
983
|
${filesHtml}
|
|
984
|
+
${buildAlertHistoryHtml()}
|
|
959
985
|
`;
|
|
960
986
|
}
|
|
961
987
|
|
|
@@ -1562,7 +1588,8 @@ function setupEvents() {
|
|
|
1562
1588
|
const files = alerts?.latest?.files || [];
|
|
1563
1589
|
if (files.length > 0) {
|
|
1564
1590
|
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
1565
|
-
|
|
1591
|
+
const hash = alerts?.latest?.commitHash || 'HEAD';
|
|
1592
|
+
openFileModal(t('modal.alertFiles'), files, proj, hash);
|
|
1566
1593
|
}
|
|
1567
1594
|
return;
|
|
1568
1595
|
}
|
|
@@ -168,12 +168,12 @@ function getDashboard(projectDir) {
|
|
|
168
168
|
issues.push(`Disk space low (${status.disk.freeGB} GB free)`);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
if (status.lastBackup.git) {
|
|
171
|
+
if (status.lastBackup.git && status.watcher.running) {
|
|
172
172
|
const lastTs = new Date(status.lastBackup.git.timestamp).getTime();
|
|
173
|
-
const
|
|
174
|
-
const staleThreshold = Math.
|
|
175
|
-
if (
|
|
176
|
-
issues.push(`Last git backup
|
|
173
|
+
const staleSec = (Date.now() - lastTs) / 1000;
|
|
174
|
+
const staleThreshold = Math.max(cfg.auto_backup_interval_seconds * 10, 300);
|
|
175
|
+
if (staleSec > staleThreshold) {
|
|
176
|
+
issues.push(`Last git backup: ${relativeTime(status.lastBackup.git.timestamp)}`);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|