averecion-lite 1.6.4 → 1.7.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/dashboard/dash.css +278 -8
- package/dashboard/dash.js +191 -27
- package/dashboard/index.html +40 -3
- package/dist/log-watcher.d.ts +4 -6
- package/dist/log-watcher.d.ts.map +1 -1
- package/dist/log-watcher.js +59 -80
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +3 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +45 -14
- package/dist/storage.d.ts +22 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +106 -0
- package/package.json +1 -1
package/dashboard/dash.css
CHANGED
|
@@ -1215,6 +1215,11 @@ body {
|
|
|
1215
1215
|
color: var(--success);
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
+
.reset-dropdown {
|
|
1219
|
+
position: relative;
|
|
1220
|
+
display: inline-block;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1218
1223
|
.reset-btn {
|
|
1219
1224
|
background: var(--bg-card);
|
|
1220
1225
|
border: 1px solid var(--border);
|
|
@@ -1231,16 +1236,158 @@ body {
|
|
|
1231
1236
|
color: var(--text-primary);
|
|
1232
1237
|
}
|
|
1233
1238
|
|
|
1234
|
-
.reset-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
+
.reset-menu {
|
|
1240
|
+
position: absolute;
|
|
1241
|
+
top: calc(100% + 6px);
|
|
1242
|
+
right: 0;
|
|
1243
|
+
background: var(--bg-card);
|
|
1244
|
+
border: 1px solid var(--border);
|
|
1245
|
+
border-radius: 12px;
|
|
1246
|
+
padding: 0.5rem;
|
|
1247
|
+
min-width: 280px;
|
|
1248
|
+
z-index: 200;
|
|
1249
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
1250
|
+
animation: dropdownFade 0.15s ease-out;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
.reset-menu.hidden {
|
|
1254
|
+
display: none;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
@keyframes dropdownFade {
|
|
1258
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1259
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.reset-option {
|
|
1263
|
+
display: flex;
|
|
1264
|
+
align-items: center;
|
|
1265
|
+
gap: 0.75rem;
|
|
1266
|
+
width: 100%;
|
|
1267
|
+
padding: 0.75rem;
|
|
1268
|
+
background: none;
|
|
1269
|
+
border: 1px solid transparent;
|
|
1270
|
+
border-radius: 8px;
|
|
1271
|
+
cursor: pointer;
|
|
1272
|
+
text-align: left;
|
|
1273
|
+
transition: all 0.15s;
|
|
1274
|
+
color: var(--text-primary);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.reset-option:hover {
|
|
1278
|
+
background: rgba(255,255,255,0.05);
|
|
1279
|
+
border-color: var(--border);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.reset-option-icon {
|
|
1283
|
+
font-size: 1.3rem;
|
|
1284
|
+
flex-shrink: 0;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.reset-option-text {
|
|
1288
|
+
display: flex;
|
|
1289
|
+
flex-direction: column;
|
|
1290
|
+
gap: 2px;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.reset-option-title {
|
|
1294
|
+
font-weight: 600;
|
|
1295
|
+
font-size: 0.9rem;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.reset-option-desc {
|
|
1299
|
+
font-size: 0.75rem;
|
|
1300
|
+
color: var(--text-secondary);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.reset-toast {
|
|
1304
|
+
position: fixed;
|
|
1305
|
+
bottom: 1.5rem;
|
|
1306
|
+
left: 50%;
|
|
1307
|
+
transform: translateX(-50%);
|
|
1308
|
+
background: var(--bg-card);
|
|
1309
|
+
border: 1px solid var(--border);
|
|
1310
|
+
border-radius: 10px;
|
|
1311
|
+
padding: 0.75rem 1.25rem;
|
|
1312
|
+
font-size: 0.9rem;
|
|
1313
|
+
color: var(--text-primary);
|
|
1314
|
+
z-index: 300;
|
|
1315
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
|
1316
|
+
animation: toastSlide 0.3s ease-out;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.reset-toast.success { border-color: var(--success); }
|
|
1320
|
+
.reset-toast.info { border-color: var(--accent); }
|
|
1321
|
+
|
|
1322
|
+
@keyframes toastSlide {
|
|
1323
|
+
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
|
1324
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.reset-confirm-overlay {
|
|
1328
|
+
position: fixed;
|
|
1329
|
+
inset: 0;
|
|
1330
|
+
background: rgba(0,0,0,0.5);
|
|
1331
|
+
display: flex;
|
|
1332
|
+
align-items: center;
|
|
1333
|
+
justify-content: center;
|
|
1334
|
+
z-index: 250;
|
|
1335
|
+
animation: dropdownFade 0.15s ease-out;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.reset-confirm-box {
|
|
1339
|
+
background: var(--bg-card);
|
|
1340
|
+
border: 1px solid var(--border);
|
|
1341
|
+
border-radius: 16px;
|
|
1342
|
+
padding: 1.5rem;
|
|
1343
|
+
max-width: 360px;
|
|
1344
|
+
width: 90%;
|
|
1345
|
+
text-align: center;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.reset-confirm-box h3 {
|
|
1349
|
+
margin: 0 0 0.5rem;
|
|
1350
|
+
font-size: 1.1rem;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.reset-confirm-box p {
|
|
1354
|
+
margin: 0 0 1.25rem;
|
|
1355
|
+
color: var(--text-secondary);
|
|
1356
|
+
font-size: 0.85rem;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.reset-confirm-actions {
|
|
1360
|
+
display: flex;
|
|
1361
|
+
gap: 0.75rem;
|
|
1362
|
+
justify-content: center;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.reset-confirm-actions button {
|
|
1366
|
+
padding: 0.5rem 1.25rem;
|
|
1367
|
+
border-radius: 8px;
|
|
1368
|
+
font-size: 0.85rem;
|
|
1369
|
+
cursor: pointer;
|
|
1370
|
+
border: 1px solid var(--border);
|
|
1371
|
+
transition: all 0.15s;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.btn-confirm-yes {
|
|
1375
|
+
background: var(--accent);
|
|
1376
|
+
color: white;
|
|
1377
|
+
border-color: var(--accent) !important;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.btn-confirm-yes:hover {
|
|
1381
|
+
opacity: 0.9;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.btn-confirm-cancel {
|
|
1385
|
+
background: var(--bg-card);
|
|
1386
|
+
color: var(--text-secondary);
|
|
1239
1387
|
}
|
|
1240
1388
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
50% { border-color: rgba(239, 68, 68, 0.4); }
|
|
1389
|
+
.btn-confirm-cancel:hover {
|
|
1390
|
+
color: var(--text-primary);
|
|
1244
1391
|
}
|
|
1245
1392
|
|
|
1246
1393
|
@keyframes slideInLeft {
|
|
@@ -1523,3 +1670,126 @@ body {
|
|
|
1523
1670
|
margin-top: 0.5rem;
|
|
1524
1671
|
}
|
|
1525
1672
|
}
|
|
1673
|
+
|
|
1674
|
+
/* Archives Section */
|
|
1675
|
+
.archives-section {
|
|
1676
|
+
margin-top: 1.5rem;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.archives-header {
|
|
1680
|
+
display: flex;
|
|
1681
|
+
align-items: center;
|
|
1682
|
+
justify-content: space-between;
|
|
1683
|
+
margin-bottom: 0.75rem;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.archives-header h2 {
|
|
1687
|
+
font-size: 1rem;
|
|
1688
|
+
font-weight: 600;
|
|
1689
|
+
margin: 0;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
.archives-toggle-btn {
|
|
1693
|
+
background: var(--bg-card);
|
|
1694
|
+
border: 1px solid var(--border);
|
|
1695
|
+
color: var(--text-secondary);
|
|
1696
|
+
padding: 0.35rem 0.75rem;
|
|
1697
|
+
border-radius: 6px;
|
|
1698
|
+
font-size: 0.8rem;
|
|
1699
|
+
cursor: pointer;
|
|
1700
|
+
transition: all 0.15s;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
.archives-toggle-btn:hover {
|
|
1704
|
+
color: var(--text-primary);
|
|
1705
|
+
border-color: var(--accent);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.archives-content.hidden {
|
|
1709
|
+
display: none;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
.archives-empty {
|
|
1713
|
+
color: var(--text-muted);
|
|
1714
|
+
font-size: 0.85rem;
|
|
1715
|
+
text-align: center;
|
|
1716
|
+
padding: 1rem;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
.archive-card {
|
|
1720
|
+
background: var(--bg-card);
|
|
1721
|
+
border: 1px solid var(--border);
|
|
1722
|
+
border-radius: 10px;
|
|
1723
|
+
padding: 1rem;
|
|
1724
|
+
margin-bottom: 0.75rem;
|
|
1725
|
+
transition: border-color 0.15s;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.archive-card:hover {
|
|
1729
|
+
border-color: var(--accent);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
.archive-card-header {
|
|
1733
|
+
display: flex;
|
|
1734
|
+
justify-content: space-between;
|
|
1735
|
+
align-items: center;
|
|
1736
|
+
margin-bottom: 0.5rem;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.archive-date {
|
|
1740
|
+
font-weight: 600;
|
|
1741
|
+
font-size: 0.9rem;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.archive-duration {
|
|
1745
|
+
font-size: 0.8rem;
|
|
1746
|
+
color: var(--text-muted);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.archive-stats {
|
|
1750
|
+
display: flex;
|
|
1751
|
+
gap: 1rem;
|
|
1752
|
+
flex-wrap: wrap;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
.archive-stat {
|
|
1756
|
+
font-size: 0.8rem;
|
|
1757
|
+
display: flex;
|
|
1758
|
+
align-items: center;
|
|
1759
|
+
gap: 0.3rem;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
.archive-stat .safe { color: var(--success); }
|
|
1763
|
+
.archive-stat .flagged { color: var(--warning); }
|
|
1764
|
+
.archive-stat .attack { color: var(--danger); }
|
|
1765
|
+
.archive-stat .reviewed { color: var(--accent); }
|
|
1766
|
+
.archive-stat .total { color: var(--text-secondary); }
|
|
1767
|
+
|
|
1768
|
+
.archive-actions {
|
|
1769
|
+
display: flex;
|
|
1770
|
+
gap: 0.5rem;
|
|
1771
|
+
margin-top: 0.75rem;
|
|
1772
|
+
padding-top: 0.75rem;
|
|
1773
|
+
border-top: 1px solid var(--border);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
.archive-actions button {
|
|
1777
|
+
background: var(--bg-card-hover);
|
|
1778
|
+
border: 1px solid var(--border);
|
|
1779
|
+
color: var(--text-secondary);
|
|
1780
|
+
padding: 0.3rem 0.6rem;
|
|
1781
|
+
border-radius: 6px;
|
|
1782
|
+
font-size: 0.75rem;
|
|
1783
|
+
cursor: pointer;
|
|
1784
|
+
transition: all 0.15s;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
.archive-actions button:hover {
|
|
1788
|
+
color: var(--text-primary);
|
|
1789
|
+
border-color: var(--accent);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
.archive-actions .btn-delete-archive:hover {
|
|
1793
|
+
border-color: var(--danger);
|
|
1794
|
+
color: var(--danger);
|
|
1795
|
+
}
|
package/dashboard/dash.js
CHANGED
|
@@ -899,6 +899,7 @@
|
|
|
899
899
|
initResetButton();
|
|
900
900
|
initThreatModal();
|
|
901
901
|
initSecurityAccordion();
|
|
902
|
+
initArchives();
|
|
902
903
|
|
|
903
904
|
function initNotificationToggle() {
|
|
904
905
|
const btn = document.getElementById("btn-notifications");
|
|
@@ -1069,40 +1070,203 @@
|
|
|
1069
1070
|
|
|
1070
1071
|
function initResetButton() {
|
|
1071
1072
|
const btn = document.getElementById("btn-reset");
|
|
1072
|
-
|
|
1073
|
+
const menu = document.getElementById("reset-menu");
|
|
1074
|
+
if (!btn || !menu) return;
|
|
1073
1075
|
|
|
1074
|
-
|
|
1076
|
+
btn.addEventListener("click", (e) => {
|
|
1077
|
+
e.stopPropagation();
|
|
1078
|
+
menu.classList.toggle("hidden");
|
|
1079
|
+
});
|
|
1075
1080
|
|
|
1076
|
-
|
|
1077
|
-
if (
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
btn.disabled = true;
|
|
1081
|
+
document.addEventListener("click", (e) => {
|
|
1082
|
+
if (!e.target.closest("#reset-dropdown")) {
|
|
1083
|
+
menu.classList.add("hidden");
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1082
1086
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
+
const CONFIRM_MESSAGES = {
|
|
1088
|
+
"new-session": { title: "Start New Session?", desc: "Counters will reset to zero. Only new activity will be shown. Your log files are not deleted." },
|
|
1089
|
+
"rebuild": { title: "Rebuild from Logs?", desc: "All log files will be re-scanned from the beginning and counters recalculated." },
|
|
1090
|
+
"archive-reset": { title: "Archive & Reset?", desc: "The current session will be saved to an archive file, then counters reset to zero." },
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const TOAST_MESSAGES = {
|
|
1094
|
+
"new-session": "New session started — counters at zero",
|
|
1095
|
+
"rebuild": "All logs re-scanned and rebuilt",
|
|
1096
|
+
"archive-reset": "Session archived and reset",
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
menu.querySelectorAll(".reset-option").forEach(option => {
|
|
1100
|
+
option.addEventListener("click", () => {
|
|
1101
|
+
const mode = option.dataset.mode;
|
|
1102
|
+
menu.classList.add("hidden");
|
|
1103
|
+
showResetConfirm(mode);
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
function showResetConfirm(mode) {
|
|
1108
|
+
const info = CONFIRM_MESSAGES[mode];
|
|
1109
|
+
const overlay = document.createElement("div");
|
|
1110
|
+
overlay.className = "reset-confirm-overlay";
|
|
1111
|
+
overlay.innerHTML = `
|
|
1112
|
+
<div class="reset-confirm-box">
|
|
1113
|
+
<h3>${info.title}</h3>
|
|
1114
|
+
<p>${info.desc}</p>
|
|
1115
|
+
<div class="reset-confirm-actions">
|
|
1116
|
+
<button class="btn-confirm-cancel" data-testid="btn-confirm-cancel">Cancel</button>
|
|
1117
|
+
<button class="btn-confirm-yes" data-testid="btn-confirm-yes">Confirm</button>
|
|
1118
|
+
</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
`;
|
|
1121
|
+
|
|
1122
|
+
document.body.appendChild(overlay);
|
|
1123
|
+
|
|
1124
|
+
overlay.querySelector(".btn-confirm-cancel").addEventListener("click", () => overlay.remove());
|
|
1125
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
1126
|
+
|
|
1127
|
+
overlay.querySelector(".btn-confirm-yes").addEventListener("click", async () => {
|
|
1128
|
+
overlay.remove();
|
|
1129
|
+
await executeReset(mode);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function executeReset(mode) {
|
|
1134
|
+
btn.disabled = true;
|
|
1135
|
+
btn.textContent = "🔄 Resetting...";
|
|
1136
|
+
|
|
1137
|
+
try {
|
|
1138
|
+
const headers = { "Content-Type": "application/json" };
|
|
1139
|
+
if (SECRET) headers["X-Lite-Secret"] = SECRET;
|
|
1140
|
+
const res = await fetch("/lite-reset", { method: "POST", headers, body: JSON.stringify({ mode }) });
|
|
1141
|
+
const data = await res.json();
|
|
1142
|
+
|
|
1143
|
+
if (data.success) {
|
|
1087
1144
|
await loadDashboard();
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
btn.disabled = false;
|
|
1092
|
-
}, 2000);
|
|
1093
|
-
} catch (err) {
|
|
1094
|
-
console.error("Reset failed:", err);
|
|
1095
|
-
btn.textContent = "🔄 Reset";
|
|
1096
|
-
btn.disabled = false;
|
|
1145
|
+
showToast(TOAST_MESSAGES[mode] || "Reset complete", "success");
|
|
1146
|
+
} else {
|
|
1147
|
+
showToast("Reset failed — " + (data.error || "unknown error"), "info");
|
|
1097
1148
|
}
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
console.error("Reset failed:", err);
|
|
1151
|
+
showToast("Reset failed — network error", "info");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
btn.textContent = "🔄 Reset ▾";
|
|
1155
|
+
btn.disabled = false;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function showToast(message, type) {
|
|
1160
|
+
const existing = document.querySelector(".reset-toast");
|
|
1161
|
+
if (existing) existing.remove();
|
|
1162
|
+
|
|
1163
|
+
const toast = document.createElement("div");
|
|
1164
|
+
toast.className = `reset-toast ${type}`;
|
|
1165
|
+
toast.textContent = message;
|
|
1166
|
+
document.body.appendChild(toast);
|
|
1167
|
+
setTimeout(() => toast.remove(), 3000);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function initArchives() {
|
|
1171
|
+
const toggleBtn = document.getElementById("btn-toggle-archives");
|
|
1172
|
+
const content = document.getElementById("archives-content");
|
|
1173
|
+
if (!toggleBtn || !content) return;
|
|
1174
|
+
|
|
1175
|
+
toggleBtn.addEventListener("click", () => {
|
|
1176
|
+
const isHidden = content.classList.contains("hidden");
|
|
1177
|
+
if (isHidden) {
|
|
1178
|
+
content.classList.remove("hidden");
|
|
1179
|
+
toggleBtn.textContent = "Hide";
|
|
1180
|
+
loadArchives();
|
|
1098
1181
|
} else {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
confirmTimeout = setTimeout(() => {
|
|
1102
|
-
btn.classList.remove("confirming");
|
|
1103
|
-
btn.textContent = "🔄 Reset";
|
|
1104
|
-
}, 3000);
|
|
1182
|
+
content.classList.add("hidden");
|
|
1183
|
+
toggleBtn.textContent = "Show";
|
|
1105
1184
|
}
|
|
1106
1185
|
});
|
|
1107
1186
|
}
|
|
1187
|
+
|
|
1188
|
+
async function loadArchives() {
|
|
1189
|
+
const list = document.getElementById("archives-list");
|
|
1190
|
+
const emptyEl = document.getElementById("archives-empty");
|
|
1191
|
+
if (!list) return;
|
|
1192
|
+
|
|
1193
|
+
try {
|
|
1194
|
+
const headers = {};
|
|
1195
|
+
if (SECRET) headers["X-Lite-Secret"] = SECRET;
|
|
1196
|
+
const res = await fetch("/api/archives", { headers });
|
|
1197
|
+
if (!res.ok) throw new Error("Failed to load archives");
|
|
1198
|
+
const archives = await res.json();
|
|
1199
|
+
|
|
1200
|
+
if (archives.length === 0) {
|
|
1201
|
+
if (emptyEl) emptyEl.style.display = "block";
|
|
1202
|
+
list.innerHTML = emptyEl ? emptyEl.outerHTML : '<p class="archives-empty">No archived sessions yet.</p>';
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (emptyEl) emptyEl.style.display = "none";
|
|
1207
|
+
|
|
1208
|
+
list.innerHTML = archives.map(a => {
|
|
1209
|
+
const start = new Date(a.startTime);
|
|
1210
|
+
const dateStr = start.toLocaleDateString() + " " + start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1211
|
+
return `
|
|
1212
|
+
<div class="archive-card" data-id="${a.id}" data-testid="archive-${a.id}">
|
|
1213
|
+
<div class="archive-card-header">
|
|
1214
|
+
<span class="archive-date">${dateStr}</span>
|
|
1215
|
+
<span class="archive-duration">${a.duration}</span>
|
|
1216
|
+
</div>
|
|
1217
|
+
<div class="archive-stats">
|
|
1218
|
+
<span class="archive-stat"><span class="safe">${a.totals.safe}</span> safe</span>
|
|
1219
|
+
<span class="archive-stat"><span class="flagged">${a.totals.flagged}</span> flagged</span>
|
|
1220
|
+
<span class="archive-stat"><span class="attack">${a.totals.attacks}</span> attacks</span>
|
|
1221
|
+
<span class="archive-stat"><span class="reviewed">${a.totals.reviewed}</span> reviewed</span>
|
|
1222
|
+
<span class="archive-stat"><span class="total">${a.totals.total}</span> total</span>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div class="archive-actions">
|
|
1225
|
+
<button class="btn-export-archive" data-id="${a.id}" data-testid="btn-export-${a.id}">📥 Export JSON</button>
|
|
1226
|
+
<button class="btn-delete-archive" data-id="${a.id}" data-testid="btn-delete-${a.id}">🗑️ Delete</button>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
`;
|
|
1230
|
+
}).join("");
|
|
1231
|
+
|
|
1232
|
+
list.querySelectorAll(".btn-export-archive").forEach(btn => {
|
|
1233
|
+
btn.addEventListener("click", async () => {
|
|
1234
|
+
const id = btn.dataset.id;
|
|
1235
|
+
try {
|
|
1236
|
+
const headers = {};
|
|
1237
|
+
if (SECRET) headers["X-Lite-Secret"] = SECRET;
|
|
1238
|
+
const res = await fetch(`/api/archives/${id}`, { headers });
|
|
1239
|
+
const data = await res.json();
|
|
1240
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
1241
|
+
const url = URL.createObjectURL(blob);
|
|
1242
|
+
const a = document.createElement("a");
|
|
1243
|
+
a.href = url;
|
|
1244
|
+
a.download = `${id}.json`;
|
|
1245
|
+
a.click();
|
|
1246
|
+
URL.revokeObjectURL(url);
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
console.error("Export failed:", err);
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
list.querySelectorAll(".btn-delete-archive").forEach(btn => {
|
|
1254
|
+
btn.addEventListener("click", async () => {
|
|
1255
|
+
const id = btn.dataset.id;
|
|
1256
|
+
if (!confirm("Delete this archived session?")) return;
|
|
1257
|
+
try {
|
|
1258
|
+
const headers = {};
|
|
1259
|
+
if (SECRET) headers["X-Lite-Secret"] = SECRET;
|
|
1260
|
+
await fetch(`/api/archives/${id}`, { method: "DELETE", headers });
|
|
1261
|
+
loadArchives();
|
|
1262
|
+
showToast("Archive deleted", "info");
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
console.error("Delete failed:", err);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
console.error("Failed to load archives:", err);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1108
1272
|
})();
|
package/dashboard/index.html
CHANGED
|
@@ -112,9 +112,34 @@
|
|
|
112
112
|
<button id="btn-notifications" class="notification-toggle" data-testid="btn-notifications" title="Enable browser notifications">
|
|
113
113
|
🔔 Notifications
|
|
114
114
|
</button>
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
<div class="reset-dropdown" id="reset-dropdown">
|
|
116
|
+
<button id="btn-reset" class="reset-btn" data-testid="btn-reset" title="Reset options">
|
|
117
|
+
🔄 Reset ▾
|
|
118
|
+
</button>
|
|
119
|
+
<div class="reset-menu hidden" id="reset-menu">
|
|
120
|
+
<button class="reset-option" data-mode="new-session" data-testid="btn-reset-new-session">
|
|
121
|
+
<span class="reset-option-icon">🆕</span>
|
|
122
|
+
<div class="reset-option-text">
|
|
123
|
+
<span class="reset-option-title">New Session</span>
|
|
124
|
+
<span class="reset-option-desc">Counters to zero, only new events shown</span>
|
|
125
|
+
</div>
|
|
126
|
+
</button>
|
|
127
|
+
<button class="reset-option" data-mode="rebuild" data-testid="btn-reset-rebuild">
|
|
128
|
+
<span class="reset-option-icon">🔁</span>
|
|
129
|
+
<div class="reset-option-text">
|
|
130
|
+
<span class="reset-option-title">Rebuild</span>
|
|
131
|
+
<span class="reset-option-desc">Re-scan all logs, recalculate everything</span>
|
|
132
|
+
</div>
|
|
133
|
+
</button>
|
|
134
|
+
<button class="reset-option" data-mode="archive-reset" data-testid="btn-reset-archive">
|
|
135
|
+
<span class="reset-option-icon">📦</span>
|
|
136
|
+
<div class="reset-option-text">
|
|
137
|
+
<span class="reset-option-title">Archive & Reset</span>
|
|
138
|
+
<span class="reset-option-desc">Save this session, then start fresh</span>
|
|
139
|
+
</div>
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
118
143
|
<div class="status-badge protected" id="global-status" data-testid="badge-status">
|
|
119
144
|
● Monitoring
|
|
120
145
|
</div>
|
|
@@ -319,6 +344,18 @@
|
|
|
319
344
|
</div>
|
|
320
345
|
</div>
|
|
321
346
|
</section>
|
|
347
|
+
|
|
348
|
+
<section class="archives-section" id="archives-section" data-testid="archives-section">
|
|
349
|
+
<div class="archives-header">
|
|
350
|
+
<h2>📦 Session Archives</h2>
|
|
351
|
+
<button class="archives-toggle-btn" id="btn-toggle-archives" data-testid="btn-toggle-archives">Show</button>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="archives-content hidden" id="archives-content">
|
|
354
|
+
<div class="archives-list" id="archives-list" data-testid="archives-list">
|
|
355
|
+
<p class="archives-empty" id="archives-empty">No archived sessions yet. Use "Archive & Reset" to save a session.</p>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</section>
|
|
322
359
|
</div>
|
|
323
360
|
</div>
|
|
324
361
|
</main>
|
package/dist/log-watcher.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
export declare class LogWatcher extends EventEmitter {
|
|
3
|
-
private
|
|
4
|
-
private watcher;
|
|
5
|
-
private filePosition;
|
|
3
|
+
private watchedFiles;
|
|
6
4
|
private activeRuns;
|
|
7
5
|
private pollInterval;
|
|
8
6
|
private recentContentHashes;
|
|
@@ -10,11 +8,11 @@ export declare class LogWatcher extends EventEmitter {
|
|
|
10
8
|
constructor();
|
|
11
9
|
start(): void;
|
|
12
10
|
reset(): void;
|
|
11
|
+
bookmark(): void;
|
|
13
12
|
stop(): void;
|
|
14
|
-
private
|
|
15
|
-
private
|
|
13
|
+
private findAllLogFiles;
|
|
14
|
+
private scanAndWatch;
|
|
16
15
|
private watchFile;
|
|
17
|
-
private scheduleRewatch;
|
|
18
16
|
private readNewLines;
|
|
19
17
|
private processLine;
|
|
20
18
|
private parseLogLine;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-watcher.d.ts","sourceRoot":"","sources":["../log-watcher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"log-watcher.d.ts","sourceRoot":"","sources":["../log-watcher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAuDtC,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,YAAY,CAAyE;IAC7F,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,qBAAqB,CAA+B;;IAY5D,KAAK,IAAI,IAAI;IAKb,KAAK,IAAI,IAAI;IAUb,QAAQ,IAAI,IAAI;IAMhB,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAwMpB,OAAO,CAAC,kBAAkB;IA+D1B,OAAO,CAAC,qBAAqB;IAwE7B,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,eAAe;IAmCvB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,gBAAgB;CAWzB;AAID,wBAAgB,eAAe,IAAI,UAAU,CAM5C;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
|
package/dist/log-watcher.js
CHANGED
|
@@ -33,8 +33,9 @@ const os = __importStar(require("os"));
|
|
|
33
33
|
const events_1 = require("events");
|
|
34
34
|
const storage_1 = require("./storage");
|
|
35
35
|
const CLAWDBOT_LOG_DIRS = [
|
|
36
|
-
path.join(os.homedir(), ".clawdbot", "logs"),
|
|
37
36
|
"/tmp/clawdbot",
|
|
37
|
+
path.join(os.homedir(), ".clawdbot", "logs"),
|
|
38
|
+
path.join(os.homedir(), ".pm2", "logs"),
|
|
38
39
|
];
|
|
39
40
|
const DANGEROUS_PATTERNS = [
|
|
40
41
|
{ pattern: /rm\s+-rf?\s+\//, reason: "Dangerous rm command (root delete)" },
|
|
@@ -64,9 +65,7 @@ const INJECTION_PATTERNS = [
|
|
|
64
65
|
{ pattern: /delete\s+everything/i, reason: "Prompt injection: destructive command" },
|
|
65
66
|
];
|
|
66
67
|
class LogWatcher extends events_1.EventEmitter {
|
|
67
|
-
|
|
68
|
-
watcher = null;
|
|
69
|
-
filePosition = 0;
|
|
68
|
+
watchedFiles = new Map();
|
|
70
69
|
activeRuns = new Map();
|
|
71
70
|
pollInterval = null;
|
|
72
71
|
recentContentHashes = new Map();
|
|
@@ -82,28 +81,29 @@ class LogWatcher extends events_1.EventEmitter {
|
|
|
82
81
|
}, 30000);
|
|
83
82
|
}
|
|
84
83
|
start() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
console.log(`[LogWatcher] No log files found in ${CLAWDBOT_LOG_DIRS.join(", ")} — polling every 5s...`);
|
|
88
|
-
this.pollInterval = setInterval(() => this.checkForLogFile(), 5000);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
this.watchFile(logFile);
|
|
84
|
+
this.scanAndWatch();
|
|
85
|
+
this.pollInterval = setInterval(() => this.scanAndWatch(), 5000);
|
|
92
86
|
}
|
|
93
87
|
reset() {
|
|
94
|
-
this.filePosition = 0;
|
|
95
88
|
this.activeRuns.clear();
|
|
96
89
|
this.recentContentHashes.clear();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
for (const [filePath, state] of this.watchedFiles) {
|
|
91
|
+
state.position = 0;
|
|
92
|
+
console.log(`[LogWatcher] Reset — will re-read ${filePath} from beginning`);
|
|
93
|
+
this.readNewLines(filePath);
|
|
100
94
|
}
|
|
101
95
|
}
|
|
96
|
+
bookmark() {
|
|
97
|
+
this.activeRuns.clear();
|
|
98
|
+
this.recentContentHashes.clear();
|
|
99
|
+
console.log(`[LogWatcher] Session bookmarked — only new events from now`);
|
|
100
|
+
}
|
|
102
101
|
stop() {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
for (const [, state] of this.watchedFiles) {
|
|
103
|
+
if (state.watcher)
|
|
104
|
+
state.watcher.close();
|
|
106
105
|
}
|
|
106
|
+
this.watchedFiles.clear();
|
|
107
107
|
if (this.pollInterval) {
|
|
108
108
|
clearInterval(this.pollInterval);
|
|
109
109
|
this.pollInterval = null;
|
|
@@ -113,95 +113,74 @@ class LogWatcher extends events_1.EventEmitter {
|
|
|
113
113
|
this.dedupeCleanupInterval = null;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
findAllLogFiles() {
|
|
117
|
+
const files = [];
|
|
117
118
|
for (const dir of CLAWDBOT_LOG_DIRS) {
|
|
118
119
|
if (!fs.existsSync(dir))
|
|
119
120
|
continue;
|
|
120
|
-
const commandsLog = path.join(dir, "commands.log");
|
|
121
|
-
if (fs.existsSync(commandsLog))
|
|
122
|
-
return commandsLog;
|
|
123
|
-
const today = new Date().toISOString().split("T")[0];
|
|
124
|
-
const dateLog = path.join(dir, `clawdbot-${today}.log`);
|
|
125
|
-
if (fs.existsSync(dateLog))
|
|
126
|
-
return dateLog;
|
|
127
121
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
if (files.length > 0)
|
|
133
|
-
return path.join(dir, files[0].name);
|
|
122
|
+
const entries = fs.readdirSync(dir).filter(f => f.endsWith(".log"));
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
files.push(path.join(dir, entry));
|
|
125
|
+
}
|
|
134
126
|
}
|
|
135
127
|
catch { }
|
|
136
128
|
}
|
|
137
|
-
return
|
|
129
|
+
return files;
|
|
138
130
|
}
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
131
|
+
scanAndWatch() {
|
|
132
|
+
const logFiles = this.findAllLogFiles();
|
|
133
|
+
if (logFiles.length === 0 && this.watchedFiles.size === 0) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const filePath of logFiles) {
|
|
137
|
+
if (!this.watchedFiles.has(filePath)) {
|
|
138
|
+
this.watchFile(filePath);
|
|
145
139
|
}
|
|
146
|
-
this.watchFile(logFile);
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
142
|
watchFile(logFile) {
|
|
150
|
-
if (this.
|
|
143
|
+
if (this.watchedFiles.has(logFile))
|
|
151
144
|
return;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
this.watchedFile = logFile;
|
|
156
|
-
this.filePosition = 0;
|
|
145
|
+
const state = { position: 0, watcher: null };
|
|
146
|
+
this.watchedFiles.set(logFile, state);
|
|
157
147
|
console.log(`[LogWatcher] Watching ${logFile} from beginning`);
|
|
158
148
|
try {
|
|
159
|
-
this.readNewLines();
|
|
160
|
-
|
|
149
|
+
this.readNewLines(logFile);
|
|
150
|
+
state.watcher = fs.watch(logFile, (eventType) => {
|
|
161
151
|
if (eventType === "change") {
|
|
162
|
-
this.readNewLines();
|
|
152
|
+
this.readNewLines(logFile);
|
|
163
153
|
}
|
|
164
154
|
});
|
|
165
|
-
|
|
166
|
-
console.error(
|
|
167
|
-
|
|
155
|
+
state.watcher.on("error", (err) => {
|
|
156
|
+
console.error(`[LogWatcher] Watch error on ${logFile}:`, err);
|
|
157
|
+
if (state.watcher)
|
|
158
|
+
state.watcher.close();
|
|
159
|
+
this.watchedFiles.delete(logFile);
|
|
168
160
|
});
|
|
169
|
-
setInterval(() => {
|
|
170
|
-
const currentLogFile = this.getCurrentLogFile();
|
|
171
|
-
if (currentLogFile && currentLogFile !== this.watchedFile) {
|
|
172
|
-
console.log("[LogWatcher] Log file rotated, switching to new file");
|
|
173
|
-
this.watchFile(currentLogFile);
|
|
174
|
-
}
|
|
175
|
-
}, 60000);
|
|
176
161
|
}
|
|
177
162
|
catch (err) {
|
|
178
|
-
console.error(
|
|
179
|
-
this.
|
|
163
|
+
console.error(`[LogWatcher] Failed to watch ${logFile}:`, err);
|
|
164
|
+
this.watchedFiles.delete(logFile);
|
|
180
165
|
}
|
|
181
166
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (logFile)
|
|
186
|
-
this.watchFile(logFile);
|
|
187
|
-
}, 5000);
|
|
188
|
-
}
|
|
189
|
-
readNewLines() {
|
|
190
|
-
if (!this.watchedFile)
|
|
167
|
+
readNewLines(filePath) {
|
|
168
|
+
const state = this.watchedFiles.get(filePath);
|
|
169
|
+
if (!state)
|
|
191
170
|
return;
|
|
192
171
|
try {
|
|
193
|
-
const stat = fs.statSync(
|
|
194
|
-
if (stat.size <
|
|
195
|
-
console.log(
|
|
196
|
-
|
|
172
|
+
const stat = fs.statSync(filePath);
|
|
173
|
+
if (stat.size < state.position) {
|
|
174
|
+
console.log(`[LogWatcher] ${path.basename(filePath)} truncated, resetting position`);
|
|
175
|
+
state.position = 0;
|
|
197
176
|
}
|
|
198
|
-
if (stat.size <=
|
|
177
|
+
if (stat.size <= state.position)
|
|
199
178
|
return;
|
|
200
|
-
const fd = fs.openSync(
|
|
201
|
-
const buffer = Buffer.alloc(stat.size -
|
|
202
|
-
fs.readSync(fd, buffer, 0, buffer.length,
|
|
179
|
+
const fd = fs.openSync(filePath, "r");
|
|
180
|
+
const buffer = Buffer.alloc(stat.size - state.position);
|
|
181
|
+
fs.readSync(fd, buffer, 0, buffer.length, state.position);
|
|
203
182
|
fs.closeSync(fd);
|
|
204
|
-
|
|
183
|
+
state.position = stat.size;
|
|
205
184
|
const lines = buffer.toString("utf-8").split("\n");
|
|
206
185
|
for (const line of lines) {
|
|
207
186
|
if (line.trim()) {
|
|
@@ -210,7 +189,7 @@ class LogWatcher extends events_1.EventEmitter {
|
|
|
210
189
|
}
|
|
211
190
|
}
|
|
212
191
|
catch (err) {
|
|
213
|
-
console.error(
|
|
192
|
+
console.error(`[LogWatcher] Error reading ${filePath}:`, err);
|
|
214
193
|
}
|
|
215
194
|
}
|
|
216
195
|
processLine(line) {
|
package/dist/metrics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsE,WAAW,EAAkC,MAAM,WAAW,CAAC;AAE5I,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,uBAAuB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IACzJ,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE;QAAE,oBAAoB,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IAClG,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrE,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,wBAAwB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClH,gBAAgB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,QAAA,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAqG,CAAC;AAC1I,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,OAAO,OAAO,QAAmB;AAE1E,wBAAgB,UAAU,CAAC,SAAS,SAAK,GAAG,WAAW,CA6CtD"}
|
package/dist/metrics.js
CHANGED
|
@@ -6,8 +6,8 @@ const storage_1 = require("./storage");
|
|
|
6
6
|
const session = { approved: 0, blocked: 0, manualApproved: 0, highRiskIntercepts: 0, promptInjectionDetected: 0 };
|
|
7
7
|
function incrementMetric(k) { session[k]++; }
|
|
8
8
|
function getMetrics(hoursBack = 24) {
|
|
9
|
-
const allEvents = (0, storage_1.
|
|
10
|
-
const recentEvents = (0, storage_1.
|
|
9
|
+
const allEvents = (0, storage_1.getFilteredEvents)(0);
|
|
10
|
+
const recentEvents = (0, storage_1.getFilteredEvents)(hoursBack);
|
|
11
11
|
const kpis = { approved: 0, blocked: 0, manualApproved: 0, highRiskIntercepts: 0, promptInjectionDetected: 0, dangerDetected: 0 };
|
|
12
12
|
const egressCounts = {};
|
|
13
13
|
const trusted = new Set(), unknownBlocked = new Set();
|
|
@@ -55,7 +55,7 @@ function getMetrics(hoursBack = 24) {
|
|
|
55
55
|
skills: { trusted: trusted.size, unknownBlocked: unknownBlocked.size, outdated: 0 },
|
|
56
56
|
instance: { reverseProxyHardened: true, dashboardLocalOnly: true, secretsEnvOnly: !!process.env.LITE_ADAPTER_SECRET },
|
|
57
57
|
timeline: { hours, approved, blocked }, cost: { inputTokens, outputTokens, estimatedUSD },
|
|
58
|
-
lastActions: (0, storage_1.
|
|
58
|
+
lastActions: (0, storage_1.getFilteredLastEvents)(10),
|
|
59
59
|
config: (0, storage_1.getConfig)(),
|
|
60
60
|
pendingApprovals: (0, storage_1.getPendingApprovals)()
|
|
61
61
|
};
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiM9E;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBhD"}
|
package/dist/server.js
CHANGED
|
@@ -95,28 +95,59 @@ async function startServer(port = 4321, host = "0.0.0.0") {
|
|
|
95
95
|
const success = (0, storage_1.resolveApproval)(id, approved === true);
|
|
96
96
|
res.json({ success, id, approved });
|
|
97
97
|
});
|
|
98
|
-
app.post("/lite-reset", localOnly, validateSecret, (
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const logsDir = path.join(os.homedir(), ".clawguard", "logs");
|
|
98
|
+
app.post("/lite-reset", localOnly, validateSecret, (req, res) => {
|
|
99
|
+
const mode = req.body?.mode || "new-session";
|
|
100
|
+
const watcher = (0, log_watcher_1.getLogWatcher)();
|
|
102
101
|
try {
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
102
|
+
if (mode === "new-session") {
|
|
103
|
+
(0, storage_1.setSessionStart)(new Date().toISOString());
|
|
104
|
+
if (watcher)
|
|
105
|
+
watcher.bookmark();
|
|
106
|
+
broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
|
|
107
|
+
res.json({ success: true, mode, message: "New session started — counters reset to zero" });
|
|
108
|
+
}
|
|
109
|
+
else if (mode === "rebuild") {
|
|
110
|
+
(0, storage_1.setSessionStart)(null);
|
|
111
|
+
if (watcher)
|
|
112
|
+
watcher.reset();
|
|
113
|
+
broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
|
|
114
|
+
res.json({ success: true, mode, message: "All logs re-scanned and counters rebuilt" });
|
|
115
|
+
}
|
|
116
|
+
else if (mode === "archive-reset") {
|
|
117
|
+
const archive = (0, storage_1.archiveCurrentSession)();
|
|
118
|
+
(0, storage_1.setSessionStart)(new Date().toISOString());
|
|
119
|
+
if (watcher)
|
|
120
|
+
watcher.bookmark();
|
|
121
|
+
broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
|
|
122
|
+
res.json({ success: true, mode, message: "Session archived and reset", archiveId: archive.id });
|
|
108
123
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
watcher.reset();
|
|
124
|
+
else {
|
|
125
|
+
res.status(400).json({ error: `Unknown reset mode: ${mode}` });
|
|
112
126
|
}
|
|
113
|
-
broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
|
|
114
|
-
res.json({ success: true, message: "Session reset" });
|
|
115
127
|
}
|
|
116
128
|
catch (err) {
|
|
117
129
|
res.status(500).json({ error: "Failed to reset" });
|
|
118
130
|
}
|
|
119
131
|
});
|
|
132
|
+
app.get("/api/archives", localOnly, validateSecret, (_req, res) => {
|
|
133
|
+
res.json((0, storage_1.listArchives)());
|
|
134
|
+
});
|
|
135
|
+
app.get("/api/archives/:id", localOnly, validateSecret, (req, res) => {
|
|
136
|
+
const archive = (0, storage_1.getArchive)(req.params.id);
|
|
137
|
+
if (!archive) {
|
|
138
|
+
res.status(404).json({ error: "Archive not found" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
res.json(archive);
|
|
142
|
+
});
|
|
143
|
+
app.delete("/api/archives/:id", localOnly, validateSecret, (req, res) => {
|
|
144
|
+
const success = (0, storage_1.deleteArchive)(req.params.id);
|
|
145
|
+
if (!success) {
|
|
146
|
+
res.status(404).json({ error: "Archive not found" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
res.json({ success: true });
|
|
150
|
+
});
|
|
120
151
|
app.get("/", localOnly, (_req, res) => res.sendFile(path.join(dashboardDir, "landing.html")));
|
|
121
152
|
app.get("/clawguard", localOnly, (_req, res) => res.sendFile(path.join(dashboardDir, "index.html")));
|
|
122
153
|
app.get("/lite-dash", localOnly, (_req, res) => res.redirect("/clawguard"));
|
package/dist/storage.d.ts
CHANGED
|
@@ -25,4 +25,26 @@ export declare function getPendingApprovals(): {
|
|
|
25
25
|
createdAt: string;
|
|
26
26
|
}[];
|
|
27
27
|
export declare function resolveApproval(id: string, approved: boolean): boolean;
|
|
28
|
+
export declare function setSessionStart(ts: string | null): void;
|
|
29
|
+
export declare function getSessionStart(): string | null;
|
|
30
|
+
export declare function getFilteredEvents(hoursBack?: number): ActionEvent[];
|
|
31
|
+
export declare function getFilteredLastEvents(count?: number): ActionEvent[];
|
|
32
|
+
export interface SessionArchive {
|
|
33
|
+
id: string;
|
|
34
|
+
startTime: string;
|
|
35
|
+
endTime: string;
|
|
36
|
+
duration: string;
|
|
37
|
+
totals: {
|
|
38
|
+
safe: number;
|
|
39
|
+
flagged: number;
|
|
40
|
+
attacks: number;
|
|
41
|
+
reviewed: number;
|
|
42
|
+
total: number;
|
|
43
|
+
};
|
|
44
|
+
events: ActionEvent[];
|
|
45
|
+
}
|
|
46
|
+
export declare function archiveCurrentSession(): SessionArchive;
|
|
47
|
+
export declare function listArchives(): Omit<SessionArchive, "events">[];
|
|
48
|
+
export declare function getArchive(id: string): SessionArchive | null;
|
|
49
|
+
export declare function deleteArchive(id: string): boolean;
|
|
28
50
|
//# sourceMappingURL=storage.d.ts.map
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../storage.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../storage.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA4CD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,QAW7C;AAED,wBAAgB,SAAS,CAAC,SAAS,SAAK,GAAG,WAAW,EAAE,CAIvD;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAK,GAAG,WAAW,EAAE,CAEvD;AAED,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,wBAAwB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAOzJ;AAED,wBAAgB,mBAAmB,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,CAgBvG;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAYtE;AAID,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEvD;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,iBAAiB,CAAC,SAAS,SAAI,GAAG,WAAW,EAAE,CAM9D;AAED,wBAAgB,qBAAqB,CAAC,KAAK,SAAK,GAAG,WAAW,EAAE,CAM/D;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5F,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,wBAAgB,qBAAqB,IAAI,cAAc,CAwCtD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,EAAE,CAY/D;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAQ5D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CASjD"}
|
package/dist/storage.js
CHANGED
|
@@ -29,6 +29,14 @@ exports.getLastEvents = getLastEvents;
|
|
|
29
29
|
exports.getConfig = getConfig;
|
|
30
30
|
exports.getPendingApprovals = getPendingApprovals;
|
|
31
31
|
exports.resolveApproval = resolveApproval;
|
|
32
|
+
exports.setSessionStart = setSessionStart;
|
|
33
|
+
exports.getSessionStart = getSessionStart;
|
|
34
|
+
exports.getFilteredEvents = getFilteredEvents;
|
|
35
|
+
exports.getFilteredLastEvents = getFilteredLastEvents;
|
|
36
|
+
exports.archiveCurrentSession = archiveCurrentSession;
|
|
37
|
+
exports.listArchives = listArchives;
|
|
38
|
+
exports.getArchive = getArchive;
|
|
39
|
+
exports.deleteArchive = deleteArchive;
|
|
32
40
|
const fs = __importStar(require("fs"));
|
|
33
41
|
const path = __importStar(require("path"));
|
|
34
42
|
const os = __importStar(require("os"));
|
|
@@ -36,6 +44,7 @@ const CLAWGUARD_DIR = path.join(os.homedir(), ".clawguard");
|
|
|
36
44
|
const LOGS_DIR = path.join(CLAWGUARD_DIR, "logs");
|
|
37
45
|
const CONFIG_FILE = path.join(CLAWGUARD_DIR, "config.json");
|
|
38
46
|
const PENDING_DIR = path.join(CLAWGUARD_DIR, "pending");
|
|
47
|
+
const ARCHIVES_DIR = path.join(CLAWGUARD_DIR, "archives");
|
|
39
48
|
function ensureDir() {
|
|
40
49
|
if (!fs.existsSync(CLAWGUARD_DIR))
|
|
41
50
|
fs.mkdirSync(CLAWGUARD_DIR, { recursive: true });
|
|
@@ -134,3 +143,100 @@ function resolveApproval(id, approved) {
|
|
|
134
143
|
catch { }
|
|
135
144
|
return false;
|
|
136
145
|
}
|
|
146
|
+
let sessionStartTime = null;
|
|
147
|
+
function setSessionStart(ts) {
|
|
148
|
+
sessionStartTime = ts;
|
|
149
|
+
}
|
|
150
|
+
function getSessionStart() {
|
|
151
|
+
return sessionStartTime;
|
|
152
|
+
}
|
|
153
|
+
function getFilteredEvents(hoursBack = 0) {
|
|
154
|
+
let events = hoursBack <= 0 ? loadLogsFromJsonl() : loadLogsFromJsonl().filter(e => e.ts >= new Date(Date.now() - hoursBack * 3600000).toISOString());
|
|
155
|
+
if (sessionStartTime) {
|
|
156
|
+
events = events.filter(e => e.ts >= sessionStartTime);
|
|
157
|
+
}
|
|
158
|
+
return events;
|
|
159
|
+
}
|
|
160
|
+
function getFilteredLastEvents(count = 10) {
|
|
161
|
+
let events = loadLogsFromJsonl();
|
|
162
|
+
if (sessionStartTime) {
|
|
163
|
+
events = events.filter(e => e.ts >= sessionStartTime);
|
|
164
|
+
}
|
|
165
|
+
return events.slice(-count).reverse();
|
|
166
|
+
}
|
|
167
|
+
function archiveCurrentSession() {
|
|
168
|
+
if (!fs.existsSync(ARCHIVES_DIR))
|
|
169
|
+
fs.mkdirSync(ARCHIVES_DIR, { recursive: true });
|
|
170
|
+
const allEvents = sessionStartTime
|
|
171
|
+
? loadLogsFromJsonl().filter(e => e.ts >= sessionStartTime)
|
|
172
|
+
: loadLogsFromJsonl();
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
const start = sessionStartTime || (allEvents.length > 0 ? allEvents[0].ts : now);
|
|
175
|
+
let safe = 0, flagged = 0, attacks = 0, reviewed = 0;
|
|
176
|
+
for (const e of allEvents) {
|
|
177
|
+
if (e.decision === "approved")
|
|
178
|
+
safe++;
|
|
179
|
+
else if (e.decision === "manual")
|
|
180
|
+
reviewed++;
|
|
181
|
+
else if (e.decision === "blocked") {
|
|
182
|
+
const isInjection = e.reason.includes("promptInjection") || e.reason.includes("Prompt injection");
|
|
183
|
+
if (isInjection)
|
|
184
|
+
attacks++;
|
|
185
|
+
else
|
|
186
|
+
flagged++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const startDate = new Date(start);
|
|
190
|
+
const endDate = new Date(now);
|
|
191
|
+
const diffMs = endDate.getTime() - startDate.getTime();
|
|
192
|
+
const hours = Math.floor(diffMs / 3600000);
|
|
193
|
+
const mins = Math.floor((diffMs % 3600000) / 60000);
|
|
194
|
+
const duration = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
195
|
+
const archive = {
|
|
196
|
+
id: `session-${now.replace(/[:.]/g, "-")}`,
|
|
197
|
+
startTime: start,
|
|
198
|
+
endTime: now,
|
|
199
|
+
duration,
|
|
200
|
+
totals: { safe, flagged, attacks, reviewed, total: allEvents.length },
|
|
201
|
+
events: allEvents,
|
|
202
|
+
};
|
|
203
|
+
const fileName = `${archive.id}.json`;
|
|
204
|
+
fs.writeFileSync(path.join(ARCHIVES_DIR, fileName), JSON.stringify(archive, null, 2));
|
|
205
|
+
return archive;
|
|
206
|
+
}
|
|
207
|
+
function listArchives() {
|
|
208
|
+
if (!fs.existsSync(ARCHIVES_DIR))
|
|
209
|
+
return [];
|
|
210
|
+
const files = fs.readdirSync(ARCHIVES_DIR).filter(f => f.endsWith(".json")).sort().reverse();
|
|
211
|
+
const archives = [];
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
try {
|
|
214
|
+
const data = JSON.parse(fs.readFileSync(path.join(ARCHIVES_DIR, file), "utf-8"));
|
|
215
|
+
const { events, ...meta } = data;
|
|
216
|
+
archives.push(meta);
|
|
217
|
+
}
|
|
218
|
+
catch { }
|
|
219
|
+
}
|
|
220
|
+
return archives;
|
|
221
|
+
}
|
|
222
|
+
function getArchive(id) {
|
|
223
|
+
const filePath = path.join(ARCHIVES_DIR, `${id}.json`);
|
|
224
|
+
try {
|
|
225
|
+
if (fs.existsSync(filePath)) {
|
|
226
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function deleteArchive(id) {
|
|
233
|
+
const filePath = path.join(ARCHIVES_DIR, `${id}.json`);
|
|
234
|
+
try {
|
|
235
|
+
if (fs.existsSync(filePath)) {
|
|
236
|
+
fs.unlinkSync(filePath);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
return false;
|
|
242
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "averecion-lite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Real-time AI agent monitoring - watches logs, detects dangerous commands and prompt injection attempts",
|
|
5
5
|
"author": "Averecion <hello@averecion.com>",
|
|
6
6
|
"homepage": "https://github.com/averecion/clawguard#readme",
|