cursor-guard 4.5.8 → 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
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
|
|
|
@@ -464,7 +464,8 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
464
464
|
| V4.5.4 | **Shadow 硬链接增量优化 + always_watch 强保护模式**:见下方详细说明 | ✅ |
|
|
465
465
|
| V4.5.6 | **Bug 修复 + 告警 UX + init 优化**:见下方详细说明 | ✅ |
|
|
466
466
|
| V4.5.7 | **文件详情 Modal 修复 + Dashboard 端口复用**:见下方详细说明 | ✅ |
|
|
467
|
-
| V4.5.8 | **Dashboard
|
|
467
|
+
| V4.5.8 | **Dashboard 版本更新检测 + 一键重启**:见下方详细说明 | ✅ |
|
|
468
|
+
| V4.6.0 | **告警 UX 全面升级**:见下方详细说明 | ✅ |
|
|
468
469
|
|
|
469
470
|
#### V4.4.1 详细内容
|
|
470
471
|
|
|
@@ -674,14 +675,28 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
674
675
|
|
|
675
676
|
#### V4.5.8 详细内容
|
|
676
677
|
|
|
677
|
-
**Dashboard
|
|
678
|
+
**Dashboard 版本更新检测 + 一键重启**:
|
|
678
679
|
|
|
679
680
|
| 组件 | 说明 |
|
|
680
681
|
|------|------|
|
|
681
|
-
| `/api/version` 端点 |
|
|
682
|
-
|
|
|
683
|
-
|
|
|
684
|
-
|
|
|
682
|
+
| `/api/version` 端点 | 返回 `serverVersion`(启动时 require cache)和 `installedVersion`(实时从磁盘读取),以及 `updateAvailable` 布尔值 |
|
|
683
|
+
| `/api/restart` 端点 (POST) | 触发服务热重启:关闭旧 HTTP server → `clearGuardCache()` 清除所有 cursor-guard 模块的 require cache → 更新 `SERVER_VERSION` → 在同端口重启新 server |
|
|
684
|
+
| 懒加载 core deps | `handleApi` 中 `getDashboard`/`runDiagnostics`/`listBackups`/`getBackupFiles` 改为通过 `coreDeps()` 懒加载,重启后自动加载新代码 |
|
|
685
|
+
| 升级横幅 | `updateAvailable === true` 时显示黄色横幅。包含"一键重启"按钮(发送 POST → 轮询等待 → 自动 reload)、"手动重启"按钮(展开命令示例)、关闭按钮 |
|
|
686
|
+
| 重启流程 | 前端发 POST `/api/restart` → 显示"正在重启..." → 每 500ms 轮询 `/api/version`(最多 10s)→ 服务就绪后 `location.reload()` 自动刷新页面 |
|
|
687
|
+
| 双语支持 | 新增 `upgrade.*` 共 8 组 i18n key(banner/dismiss/restartNow/restart/hint/restarting/waiting/failed) |
|
|
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 条告警历史,刷新页面不丢失 |
|
|
685
700
|
|
|
686
701
|
#### V4.5.x 新增配置参考
|
|
687
702
|
|
|
@@ -707,6 +722,15 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
707
722
|
}
|
|
708
723
|
```
|
|
709
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
|
+
|
|
710
734
|
### V4 不做的事
|
|
711
735
|
|
|
712
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",
|
|
@@ -142,8 +142,12 @@ const I18N = {
|
|
|
142
142
|
|
|
143
143
|
'upgrade.banner': 'New version {installed} available (current server: {server}). Please restart the Dashboard service to load the latest features.',
|
|
144
144
|
'upgrade.dismiss': 'Dismiss',
|
|
145
|
-
'upgrade.
|
|
145
|
+
'upgrade.restartNow': 'Restart Now',
|
|
146
|
+
'upgrade.restart': 'Manual restart',
|
|
146
147
|
'upgrade.hint': 'Stop the current process (Ctrl+C), then run: cursor-guard-backup --path <dir> --dashboard',
|
|
148
|
+
'upgrade.restarting': 'Restarting...',
|
|
149
|
+
'upgrade.waiting': 'Waiting for server...',
|
|
150
|
+
'upgrade.failed': 'Restart failed, try manually',
|
|
147
151
|
|
|
148
152
|
'strategy.git': 'Git',
|
|
149
153
|
'strategy.shadow': 'Shadow',
|
|
@@ -362,8 +366,12 @@ const I18N = {
|
|
|
362
366
|
|
|
363
367
|
'upgrade.banner': '检测到新版本 {installed}(当前服务: {server}),请重启 Dashboard 服务以加载最新功能',
|
|
364
368
|
'upgrade.dismiss': '关闭',
|
|
365
|
-
'upgrade.
|
|
369
|
+
'upgrade.restartNow': '一键重启',
|
|
370
|
+
'upgrade.restart': '手动重启',
|
|
366
371
|
'upgrade.hint': '停止当前进程 (Ctrl+C),然后运行: cursor-guard-backup --path <目录> --dashboard',
|
|
372
|
+
'upgrade.restarting': '正在重启...',
|
|
373
|
+
'upgrade.waiting': '等待服务就绪...',
|
|
374
|
+
'upgrade.failed': '重启失败,请手动重启',
|
|
367
375
|
|
|
368
376
|
'strategy.git': 'Git',
|
|
369
377
|
'strategy.shadow': '影子',
|
|
@@ -462,10 +470,23 @@ const state = {
|
|
|
462
470
|
tickTimer: null,
|
|
463
471
|
lastRefreshAt: null,
|
|
464
472
|
drawerOpen: null,
|
|
465
|
-
alertHistory:
|
|
473
|
+
alertHistory: loadAlertHistory(),
|
|
474
|
+
alertExpiresAt: null,
|
|
466
475
|
};
|
|
467
476
|
|
|
468
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
|
+
}
|
|
469
490
|
|
|
470
491
|
/* ── DOM helpers ──────────────────────────────────────────── */
|
|
471
492
|
|
|
@@ -729,9 +750,17 @@ async function manualRefresh() {
|
|
|
729
750
|
|
|
730
751
|
function updateRefreshDisplay() {
|
|
731
752
|
const el = $('#last-refresh');
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
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
|
+
}
|
|
735
764
|
}
|
|
736
765
|
|
|
737
766
|
/* ── Rendering: Top bar ───────────────────────────────────── */
|
|
@@ -881,33 +910,35 @@ function alertFileBreakdown(files) {
|
|
|
881
910
|
return t('alert.breakdown', { added, modified, deleted });
|
|
882
911
|
}
|
|
883
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
|
+
|
|
884
934
|
function renderAlertCard(alerts) {
|
|
885
935
|
const el = $('#card-alert');
|
|
886
936
|
if (!alerts?.active) {
|
|
887
|
-
|
|
888
|
-
if (state.alertHistory.length > 0) {
|
|
889
|
-
const count = state.alertHistory.length;
|
|
890
|
-
const rows = state.alertHistory.slice(-5).reverse().map(h => {
|
|
891
|
-
const breakdown = alertFileBreakdown(h.files);
|
|
892
|
-
return `<div class="alert-history-row text-sm text-muted">
|
|
893
|
-
<span class="alert-history-time">${esc(formatTime(h.timestamp))}</span>
|
|
894
|
-
<span>${t('alert.detail', { count: h.fileCount, window: h.windowSeconds, threshold: h.threshold })}</span>
|
|
895
|
-
${breakdown ? `<span class="alert-history-breakdown">${esc(breakdown)}</span>` : ''}
|
|
896
|
-
<span class="badge badge-expired">${t('alert.expired')}</span>
|
|
897
|
-
</div>`;
|
|
898
|
-
}).join('');
|
|
899
|
-
historyHtml = `
|
|
900
|
-
<div class="alert-history-toggle-wrap">
|
|
901
|
-
<button class="alert-history-toggle-btn text-sm text-muted" data-alert-history-toggle>${t('alert.historyCount', { n: count })}</button>
|
|
902
|
-
</div>
|
|
903
|
-
<div class="alert-history alert-history-collapsed">
|
|
904
|
-
<div class="alert-history-label text-sm">${t('alert.history')}</div>${rows}
|
|
905
|
-
</div>`;
|
|
906
|
-
}
|
|
937
|
+
state.alertExpiresAt = null;
|
|
907
938
|
el.innerHTML = `
|
|
908
939
|
<div class="card-label">${t('alert.title')}</div>
|
|
909
940
|
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('alert.none')}</span></div>
|
|
910
|
-
${
|
|
941
|
+
${buildAlertHistoryHtml()}
|
|
911
942
|
`;
|
|
912
943
|
return;
|
|
913
944
|
}
|
|
@@ -917,10 +948,12 @@ function renderAlertCard(alerts) {
|
|
|
917
948
|
if (a.timestamp && !state.alertHistory.some(h => h.timestamp === a.timestamp)) {
|
|
918
949
|
state.alertHistory.push({ timestamp: a.timestamp, fileCount: a.fileCount, windowSeconds: a.windowSeconds, threshold: a.threshold, expiresAt: a.expiresAt, files: a.files });
|
|
919
950
|
if (state.alertHistory.length > 20) state.alertHistory = state.alertHistory.slice(-20);
|
|
951
|
+
saveAlertHistory();
|
|
920
952
|
}
|
|
921
953
|
|
|
922
954
|
const triggeredAt = a.timestamp ? formatTime(a.timestamp) : '-';
|
|
923
955
|
const expiresAt = a.expiresAt ? new Date(a.expiresAt) : null;
|
|
956
|
+
state.alertExpiresAt = expiresAt ? expiresAt.getTime() : null;
|
|
924
957
|
const remainMs = expiresAt ? expiresAt.getTime() - Date.now() : 0;
|
|
925
958
|
const remainSec = Math.max(0, Math.ceil(remainMs / 1000));
|
|
926
959
|
const remainMin = Math.floor(remainSec / 60);
|
|
@@ -948,6 +981,7 @@ function renderAlertCard(alerts) {
|
|
|
948
981
|
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('alert.suggestion')}</div>
|
|
949
982
|
</div>
|
|
950
983
|
${filesHtml}
|
|
984
|
+
${buildAlertHistoryHtml()}
|
|
951
985
|
`;
|
|
952
986
|
}
|
|
953
987
|
|
|
@@ -1554,7 +1588,8 @@ function setupEvents() {
|
|
|
1554
1588
|
const files = alerts?.latest?.files || [];
|
|
1555
1589
|
if (files.length > 0) {
|
|
1556
1590
|
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
1557
|
-
|
|
1591
|
+
const hash = alerts?.latest?.commitHash || 'HEAD';
|
|
1592
|
+
openFileModal(t('modal.alertFiles'), files, proj, hash);
|
|
1558
1593
|
}
|
|
1559
1594
|
return;
|
|
1560
1595
|
}
|
|
@@ -1606,7 +1641,8 @@ function showUpgradeBanner(data) {
|
|
|
1606
1641
|
banner.className = 'upgrade-banner';
|
|
1607
1642
|
banner.innerHTML = `
|
|
1608
1643
|
<span class="upgrade-banner-text">${t('upgrade.banner', { installed: esc(data.installedVersion), server: esc(data.serverVersion) })}</span>
|
|
1609
|
-
<button class="upgrade-banner-
|
|
1644
|
+
<button class="upgrade-banner-restart-btn">${t('upgrade.restartNow')}</button>
|
|
1645
|
+
<button class="upgrade-banner-hint-btn">${t('upgrade.restart')}</button>
|
|
1610
1646
|
<button class="upgrade-banner-close" aria-label="${t('upgrade.dismiss')}">×</button>
|
|
1611
1647
|
`;
|
|
1612
1648
|
const topbar = $('#topbar');
|
|
@@ -1620,6 +1656,42 @@ function showUpgradeBanner(data) {
|
|
|
1620
1656
|
el.innerHTML = `<code>${t('upgrade.hint')}</code>`;
|
|
1621
1657
|
banner.appendChild(el);
|
|
1622
1658
|
});
|
|
1659
|
+
banner.querySelector('.upgrade-banner-restart-btn').addEventListener('click', () => {
|
|
1660
|
+
restartServer(banner);
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async function restartServer(banner) {
|
|
1665
|
+
const btn = banner.querySelector('.upgrade-banner-restart-btn');
|
|
1666
|
+
btn.disabled = true;
|
|
1667
|
+
btn.textContent = t('upgrade.restarting');
|
|
1668
|
+
banner.querySelector('.upgrade-banner-hint-btn').style.display = 'none';
|
|
1669
|
+
banner.querySelector('.upgrade-banner-close').style.display = 'none';
|
|
1670
|
+
|
|
1671
|
+
try {
|
|
1672
|
+
const sep = '/api/restart'.includes('?') ? '&' : '?';
|
|
1673
|
+
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
|
|
1674
|
+
await fetch('/api/restart' + tokenParam, { method: 'POST' });
|
|
1675
|
+
} catch { /* server may close connection */ }
|
|
1676
|
+
|
|
1677
|
+
btn.textContent = t('upgrade.waiting');
|
|
1678
|
+
let ready = false;
|
|
1679
|
+
for (let i = 0; i < 20; i++) {
|
|
1680
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1681
|
+
try {
|
|
1682
|
+
const r = await fetch('/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
|
|
1683
|
+
if (r.ok) { ready = true; break; }
|
|
1684
|
+
} catch { /* still restarting */ }
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
if (ready) {
|
|
1688
|
+
location.reload();
|
|
1689
|
+
} else {
|
|
1690
|
+
btn.textContent = t('upgrade.failed');
|
|
1691
|
+
btn.disabled = false;
|
|
1692
|
+
banner.querySelector('.upgrade-banner-hint-btn').style.display = '';
|
|
1693
|
+
banner.querySelector('.upgrade-banner-close').style.display = '';
|
|
1694
|
+
}
|
|
1623
1695
|
}
|
|
1624
1696
|
|
|
1625
1697
|
/* ── Init ─────────────────────────────────────────────────── */
|
|
@@ -143,6 +143,25 @@ body {
|
|
|
143
143
|
.upgrade-banner-text {
|
|
144
144
|
flex: 1;
|
|
145
145
|
}
|
|
146
|
+
.upgrade-banner-restart-btn {
|
|
147
|
+
padding: 4px 14px;
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
border: 1px solid var(--yellow);
|
|
151
|
+
border-radius: var(--radius-sm);
|
|
152
|
+
background: var(--yellow);
|
|
153
|
+
color: var(--bg);
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
transition: all 0.15s;
|
|
157
|
+
}
|
|
158
|
+
.upgrade-banner-restart-btn:hover {
|
|
159
|
+
filter: brightness(1.1);
|
|
160
|
+
}
|
|
161
|
+
.upgrade-banner-restart-btn:disabled {
|
|
162
|
+
opacity: 0.7;
|
|
163
|
+
cursor: wait;
|
|
164
|
+
}
|
|
146
165
|
.upgrade-banner-hint-btn {
|
|
147
166
|
padding: 3px 10px;
|
|
148
167
|
font-size: 11px;
|
|
@@ -6,13 +6,26 @@ const crypto = require('crypto');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const GUARD_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
function coreDeps() {
|
|
12
|
+
return {
|
|
13
|
+
getDashboard: require('../lib/core/dashboard').getDashboard,
|
|
14
|
+
runDiagnostics: require('../lib/core/doctor').runDiagnostics,
|
|
15
|
+
listBackups: require('../lib/core/backups').listBackups,
|
|
16
|
+
getBackupFiles: require('../lib/core/backups').getBackupFiles,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clearGuardCache() {
|
|
21
|
+
Object.keys(require.cache).forEach(key => {
|
|
22
|
+
if (key.startsWith(GUARD_ROOT)) delete require.cache[key];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
14
27
|
const PKG_PATH = path.resolve(__dirname, '..', '..', 'package.json');
|
|
15
|
-
|
|
28
|
+
let SERVER_VERSION = require('../../package.json').version;
|
|
16
29
|
const DEFAULT_PORT = 3120;
|
|
17
30
|
const MAX_PORT_RETRIES = 10;
|
|
18
31
|
const ALLOWED_HOSTS = /^(127\.0\.0\.1|localhost)(:\d+)?$/;
|
|
@@ -126,7 +139,7 @@ function serveStatic(reqUrl, res, serverToken) {
|
|
|
126
139
|
|
|
127
140
|
/* ── API routes ─────────────────────────────────────────────── */
|
|
128
141
|
|
|
129
|
-
function handleApi(pathname, query, registry, res) {
|
|
142
|
+
function handleApi(pathname, query, registry, res, req) {
|
|
130
143
|
if (pathname === '/api/version') {
|
|
131
144
|
let installedVersion = SERVER_VERSION;
|
|
132
145
|
try {
|
|
@@ -140,6 +153,17 @@ function handleApi(pathname, query, registry, res) {
|
|
|
140
153
|
});
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
if (pathname === '/api/restart') {
|
|
157
|
+
if (req.method !== 'POST') {
|
|
158
|
+
res.writeHead(405);
|
|
159
|
+
return res.end('Method Not Allowed');
|
|
160
|
+
}
|
|
161
|
+
if (!_instance) return json(res, { error: 'No running instance' }, 500);
|
|
162
|
+
json(res, { restarting: true });
|
|
163
|
+
setTimeout(() => restartDashboard(), 300);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
if (pathname === '/api/projects') {
|
|
144
168
|
const list = [...registry.values()].map(({ id, name, pathLabel }) => ({ id, name, pathLabel }));
|
|
145
169
|
return json(res, list);
|
|
@@ -154,6 +178,8 @@ function handleApi(pathname, query, registry, res) {
|
|
|
154
178
|
return json(res, { error: `Project directory not accessible: ${project.pathLabel}` }, 500);
|
|
155
179
|
}
|
|
156
180
|
|
|
181
|
+
const { getDashboard, runDiagnostics, listBackups, getBackupFiles } = coreDeps();
|
|
182
|
+
|
|
157
183
|
if (pathname === '/api/page-data') {
|
|
158
184
|
const scope = query.get('scope');
|
|
159
185
|
const result = { timestamp: new Date().toISOString() };
|
|
@@ -258,7 +284,7 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
258
284
|
return res.end('Forbidden: invalid host');
|
|
259
285
|
}
|
|
260
286
|
|
|
261
|
-
if (req.method !== 'GET') {
|
|
287
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
262
288
|
res.writeHead(405);
|
|
263
289
|
return res.end('Method Not Allowed');
|
|
264
290
|
}
|
|
@@ -272,8 +298,9 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
272
298
|
res.writeHead(403);
|
|
273
299
|
return res.end('Forbidden: invalid token');
|
|
274
300
|
}
|
|
275
|
-
handleApi(parsed.pathname, parsed.searchParams, registry, res);
|
|
301
|
+
handleApi(parsed.pathname, parsed.searchParams, registry, res, req);
|
|
276
302
|
} else {
|
|
303
|
+
if (req.method !== 'GET') { res.writeHead(405); return res.end('Method Not Allowed'); }
|
|
277
304
|
serveStatic(req.url, res, token);
|
|
278
305
|
}
|
|
279
306
|
});
|
|
@@ -309,6 +336,26 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
309
336
|
});
|
|
310
337
|
}
|
|
311
338
|
|
|
339
|
+
/* ── Hot Restart ───────────────────────────────────────────── */
|
|
340
|
+
|
|
341
|
+
async function restartDashboard() {
|
|
342
|
+
if (!_instance) return;
|
|
343
|
+
const paths = [..._instance.registry.values()].map(p => p._path);
|
|
344
|
+
const port = _instance.port;
|
|
345
|
+
|
|
346
|
+
_instance.server.close();
|
|
347
|
+
_instance = null;
|
|
348
|
+
|
|
349
|
+
clearGuardCache();
|
|
350
|
+
try {
|
|
351
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
|
|
352
|
+
SERVER_VERSION = pkg.version;
|
|
353
|
+
} catch { /* keep old version */ }
|
|
354
|
+
|
|
355
|
+
console.log(` [dashboard] Restarting on port ${port}...`);
|
|
356
|
+
await startDashboardServer(paths, { port, silent: false });
|
|
357
|
+
}
|
|
358
|
+
|
|
312
359
|
/* ── CLI entry ─────────────────────────────────────────────── */
|
|
313
360
|
|
|
314
361
|
if (require.main === module) {
|
|
@@ -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
|
|