memorix 1.6.2 → 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/CHANGELOG.md +34 -0
- package/dist/cli/index.js +2042 -1430
- package/dist/cli/memcode.js +0 -0
- package/dist/dashboard/static/app.js +293 -33
- package/dist/dashboard/static/index.html +17 -0
- package/dist/dashboard/static/style.css +310 -0
- package/dist/index.js +1669 -970
- package/dist/maintenance-runner.js +1061 -1017
- package/dist/media-audio-runner.js +1052 -1009
- package/dist/media-video-runner.js +1052 -1009
- package/dist/memcode-runtime/CHANGELOG.md +34 -0
- package/dist/sdk.js +2293 -1684
- package/dist/vector-backfill-runner.js +1050 -1007
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/DASHBOARD.md +54 -0
- package/docs/README.md +2 -0
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/background.ts +17 -3
- package/src/cli/commands/cleanup.ts +13 -114
- package/src/cli/commands/memory.ts +12 -43
- package/src/cli/commands/operator-shared.ts +12 -8
- package/src/cli/commands/serve-http.ts +25 -7
- package/src/cli/commands/serve-shared.ts +49 -6
- package/src/cli/commands/serve.ts +4 -0
- package/src/dashboard/maintenance.ts +303 -0
- package/src/dashboard/server.ts +100 -2
- package/src/dashboard/static/index.html +17 -0
- package/src/dashboard/static/style.css +310 -0
- package/src/memory/cleanup.ts +143 -0
- package/src/memory/deduplication.ts +178 -0
- package/src/memory/session.ts +13 -3
- package/src/project/launch-root.ts +115 -0
- package/src/runtime/isolated-maintenance.ts +12 -1
- package/src/server.ts +50 -51
- package/src/store/obs-store.ts +2 -2
- package/src/store/sqlite-db.ts +2 -0
- package/src/store/sqlite-store.ts +19 -4
package/dist/cli/memcode.js
CHANGED
|
File without changes
|
|
@@ -49,6 +49,38 @@ const i18n = {
|
|
|
49
49
|
deleteObs: 'Delete',
|
|
50
50
|
deleteConfirm: 'Delete observation #%id%?',
|
|
51
51
|
batchCleanup: 'Cleanup',
|
|
52
|
+
selectRecords: 'Select',
|
|
53
|
+
memoryMaintenance: 'Memory Maintenance',
|
|
54
|
+
previewRequired: 'Preview required',
|
|
55
|
+
cleanupAction: 'Cleanup',
|
|
56
|
+
consolidateAction: 'Consolidate',
|
|
57
|
+
deduplicateAction: 'Deduplicate',
|
|
58
|
+
archiveAction: 'Archive',
|
|
59
|
+
previewAction: 'Preview',
|
|
60
|
+
executeAction: 'Execute',
|
|
61
|
+
closeAction: 'Close',
|
|
62
|
+
cleanupNoise: 'Include test and demo noise',
|
|
63
|
+
maintenancePreview: 'Maintenance preview',
|
|
64
|
+
maintenanceEmpty: 'No candidates found',
|
|
65
|
+
maintenanceUnavailable: 'Action unavailable',
|
|
66
|
+
maintenanceChanged: 'Memory changed. Preview again before executing.',
|
|
67
|
+
maintenanceSuccess: 'Maintenance completed',
|
|
68
|
+
maintenanceFailed: 'Maintenance failed',
|
|
69
|
+
willDelete: 'Delete',
|
|
70
|
+
willArchive: 'Archive',
|
|
71
|
+
willResolve: 'Resolve',
|
|
72
|
+
willMerge: 'Merge',
|
|
73
|
+
canonicalRecord: 'Keep',
|
|
74
|
+
affectedRecords: 'Affected records',
|
|
75
|
+
cleanupLowQuality: 'Low quality',
|
|
76
|
+
cleanupDuplicates: 'Exact duplicates',
|
|
77
|
+
cleanupNoiseCount: 'Noise',
|
|
78
|
+
scannedRecords: 'Scanned',
|
|
79
|
+
comparisonFailures: 'Comparison failures',
|
|
80
|
+
confirmCleanup: 'Execute cleanup',
|
|
81
|
+
confirmConsolidate: 'Merge candidates',
|
|
82
|
+
confirmDeduplicate: 'Resolve duplicates',
|
|
83
|
+
confirmArchive: 'Archive candidates',
|
|
52
84
|
selected: 'selected',
|
|
53
85
|
cancel: 'Cancel',
|
|
54
86
|
deleteSelected: 'Delete Selected',
|
|
@@ -481,6 +513,38 @@ const i18n = {
|
|
|
481
513
|
deleteObs: '删除',
|
|
482
514
|
deleteConfirm: '确认删除观察 #%id%?',
|
|
483
515
|
batchCleanup: '清理',
|
|
516
|
+
selectRecords: '选择',
|
|
517
|
+
memoryMaintenance: '记忆维护',
|
|
518
|
+
previewRequired: '执行前需预览',
|
|
519
|
+
cleanupAction: '清理',
|
|
520
|
+
consolidateAction: '合并',
|
|
521
|
+
deduplicateAction: '智能去重',
|
|
522
|
+
archiveAction: '归档',
|
|
523
|
+
previewAction: '预览',
|
|
524
|
+
executeAction: '执行',
|
|
525
|
+
closeAction: '关闭',
|
|
526
|
+
cleanupNoise: '包含测试和演示噪声',
|
|
527
|
+
maintenancePreview: '维护预览',
|
|
528
|
+
maintenanceEmpty: '没有发现候选项',
|
|
529
|
+
maintenanceUnavailable: '当前操作不可用',
|
|
530
|
+
maintenanceChanged: '记忆已发生变化,请重新预览后再执行。',
|
|
531
|
+
maintenanceSuccess: '维护完成',
|
|
532
|
+
maintenanceFailed: '维护失败',
|
|
533
|
+
willDelete: '删除',
|
|
534
|
+
willArchive: '归档',
|
|
535
|
+
willResolve: '设为已解决',
|
|
536
|
+
willMerge: '合并',
|
|
537
|
+
canonicalRecord: '保留',
|
|
538
|
+
affectedRecords: '受影响记录',
|
|
539
|
+
cleanupLowQuality: '低质量记录',
|
|
540
|
+
cleanupDuplicates: '完全重复',
|
|
541
|
+
cleanupNoiseCount: '噪声',
|
|
542
|
+
scannedRecords: '已扫描',
|
|
543
|
+
comparisonFailures: '比较失败',
|
|
544
|
+
confirmCleanup: '执行清理',
|
|
545
|
+
confirmConsolidate: '合并候选项',
|
|
546
|
+
confirmDeduplicate: '解决重复项',
|
|
547
|
+
confirmArchive: '归档候选项',
|
|
484
548
|
selected: '已选中',
|
|
485
549
|
cancel: '取消',
|
|
486
550
|
deleteSelected: '删除所选',
|
|
@@ -1071,6 +1135,209 @@ async function api(endpoint) {
|
|
|
1071
1135
|
}
|
|
1072
1136
|
}
|
|
1073
1137
|
|
|
1138
|
+
// ============================================================
|
|
1139
|
+
// Maintenance actions
|
|
1140
|
+
// ============================================================
|
|
1141
|
+
|
|
1142
|
+
let activeMaintenancePreview = null;
|
|
1143
|
+
|
|
1144
|
+
const maintenanceMeta = {
|
|
1145
|
+
cleanup: { title: 'cleanupAction', confirm: 'confirmCleanup', icon: 'lucide:eraser' },
|
|
1146
|
+
consolidate: { title: 'consolidateAction', confirm: 'confirmConsolidate', icon: 'lucide:combine' },
|
|
1147
|
+
deduplicate: { title: 'deduplicateAction', confirm: 'confirmDeduplicate', icon: 'lucide:copy-minus' },
|
|
1148
|
+
retention: { title: 'archiveAction', confirm: 'confirmArchive', icon: 'lucide:archive' },
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
async function maintenancePost(action, phase, body = {}) {
|
|
1152
|
+
const suffix = selectedProject ? `?project=${encodeURIComponent(selectedProject)}` : '';
|
|
1153
|
+
const response = await fetch(`/api/maintenance/${action}/${phase}${suffix}`, {
|
|
1154
|
+
method: 'POST',
|
|
1155
|
+
headers: { 'content-type': 'application/json' },
|
|
1156
|
+
body: JSON.stringify(body),
|
|
1157
|
+
});
|
|
1158
|
+
const data = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
|
|
1159
|
+
if (!response.ok) {
|
|
1160
|
+
const error = new Error(data.error || `HTTP ${response.status}`);
|
|
1161
|
+
error.status = response.status;
|
|
1162
|
+
throw error;
|
|
1163
|
+
}
|
|
1164
|
+
return data;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function showDashboardToast(message, tone = 'success') {
|
|
1168
|
+
const toast = document.getElementById('dashboard-toast');
|
|
1169
|
+
if (!toast) return;
|
|
1170
|
+
toast.textContent = message;
|
|
1171
|
+
toast.dataset.tone = tone;
|
|
1172
|
+
toast.classList.add('visible');
|
|
1173
|
+
clearTimeout(showDashboardToast.timer);
|
|
1174
|
+
showDashboardToast.timer = setTimeout(() => toast.classList.remove('visible'), 3200);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function maintenanceSummaryEntries(action, summary = {}) {
|
|
1178
|
+
const mappings = {
|
|
1179
|
+
cleanup: [
|
|
1180
|
+
['delete', 'willDelete'], ['archive', 'willArchive'],
|
|
1181
|
+
['lowQuality', 'cleanupLowQuality'], ['duplicates', 'cleanupDuplicates'], ['noise', 'cleanupNoiseCount'],
|
|
1182
|
+
],
|
|
1183
|
+
consolidate: [['clusters', 'willMerge'], ['observations', 'affectedRecords']],
|
|
1184
|
+
deduplicate: [['resolve', 'willResolve'], ['scanned', 'scannedRecords'], ['failedComparisons', 'comparisonFailures']],
|
|
1185
|
+
retention: [['archive', 'willArchive']],
|
|
1186
|
+
};
|
|
1187
|
+
return (mappings[action] || [])
|
|
1188
|
+
.filter(([key]) => summary[key] !== undefined)
|
|
1189
|
+
.map(([key, label]) => ({ label: t(label), value: summary[key] }));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function maintenanceRecord(record, prefix = '') {
|
|
1193
|
+
return `
|
|
1194
|
+
<div class="maintenance-record">
|
|
1195
|
+
<span class="maintenance-record-id">#${record.id}</span>
|
|
1196
|
+
<span class="maintenance-record-title">${prefix}${escapeHtml(record.title || t('untitled'))}</span>
|
|
1197
|
+
<span class="maintenance-record-meta">${escapeHtml(record.entityName || '')}</span>
|
|
1198
|
+
</div>`;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function maintenanceDetails(action, preview) {
|
|
1202
|
+
if (action === 'cleanup') {
|
|
1203
|
+
const lowQuality = (preview.lowQuality || []).map((item) => maintenanceRecord(item)).join('');
|
|
1204
|
+
const duplicateGroups = (preview.duplicateGroups || []).map((group) => `
|
|
1205
|
+
<div class="maintenance-group">
|
|
1206
|
+
${maintenanceRecord(group.canonical, `${t('canonicalRecord')}: `)}
|
|
1207
|
+
${(group.duplicates || []).map((item) => maintenanceRecord(item, `${t('willDelete')}: `)).join('')}
|
|
1208
|
+
</div>`).join('');
|
|
1209
|
+
const noise = (preview.noise || []).map((item) => maintenanceRecord(item, `${t('willArchive')}: `)).join('');
|
|
1210
|
+
const records = `${lowQuality}${duplicateGroups}${noise}`;
|
|
1211
|
+
return `
|
|
1212
|
+
<label class="maintenance-toggle">
|
|
1213
|
+
<input id="maintenance-noise-toggle" type="checkbox" ${preview.includeNoise ? 'checked' : ''}>
|
|
1214
|
+
<span>${t('cleanupNoise')}</span>
|
|
1215
|
+
</label>
|
|
1216
|
+
${records || `<div class="maintenance-empty">${t('maintenanceEmpty')}</div>`}`;
|
|
1217
|
+
}
|
|
1218
|
+
if (action === 'consolidate') {
|
|
1219
|
+
return (preview.clusters || []).map((cluster, index) => `
|
|
1220
|
+
<div class="maintenance-group">
|
|
1221
|
+
<div class="maintenance-group-title">${t('willMerge')} ${index + 1} · ${escapeHtml(cluster.entityName)} · ${Math.round(cluster.similarity * 100)}%</div>
|
|
1222
|
+
${(cluster.ids || []).map((id, itemIndex) => maintenanceRecord({
|
|
1223
|
+
id,
|
|
1224
|
+
title: (cluster.titles || [])[itemIndex] || '',
|
|
1225
|
+
entityName: cluster.type || '',
|
|
1226
|
+
})).join('')}
|
|
1227
|
+
</div>`).join('');
|
|
1228
|
+
}
|
|
1229
|
+
if (action === 'deduplicate') {
|
|
1230
|
+
return (preview.actions || []).map((item) => `
|
|
1231
|
+
<div class="maintenance-group">
|
|
1232
|
+
${maintenanceRecord({ id: item.keepId, title: item.keepTitle, entityName: item.entityName }, `${t('canonicalRecord')}: `)}
|
|
1233
|
+
${maintenanceRecord({ id: item.resolveId, title: item.resolveTitle, entityName: item.reason }, `${t('willResolve')}: `)}
|
|
1234
|
+
</div>`).join('');
|
|
1235
|
+
}
|
|
1236
|
+
return (preview.candidates || []).map((item) => maintenanceRecord({
|
|
1237
|
+
...item,
|
|
1238
|
+
entityName: `${item.score} · ${item.ageHours}h`,
|
|
1239
|
+
})).join('');
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function renderMaintenanceDialog(action, preview) {
|
|
1243
|
+
const dialog = document.getElementById('maintenance-dialog');
|
|
1244
|
+
const body = document.getElementById('maintenance-dialog-body');
|
|
1245
|
+
const footer = document.getElementById('maintenance-dialog-footer');
|
|
1246
|
+
const meta = maintenanceMeta[action];
|
|
1247
|
+
document.getElementById('maintenance-dialog-kicker').textContent = t('maintenancePreview');
|
|
1248
|
+
document.getElementById('maintenance-dialog-title').textContent = t(meta.title);
|
|
1249
|
+
|
|
1250
|
+
if (preview.available === false) {
|
|
1251
|
+
body.innerHTML = `<div class="maintenance-empty"><strong>${t('maintenanceUnavailable')}</strong><span>${escapeHtml(preview.reason || '')}</span></div>`;
|
|
1252
|
+
footer.innerHTML = `<button class="maintenance-secondary" type="button" data-maintenance-close>${t('closeAction')}</button>`;
|
|
1253
|
+
} else {
|
|
1254
|
+
const summary = maintenanceSummaryEntries(action, preview.summary);
|
|
1255
|
+
const hasChanges = action === 'cleanup'
|
|
1256
|
+
? (preview.summary.delete + preview.summary.archive) > 0
|
|
1257
|
+
: action === 'consolidate'
|
|
1258
|
+
? preview.summary.clusters > 0
|
|
1259
|
+
: action === 'deduplicate'
|
|
1260
|
+
? preview.summary.resolve > 0
|
|
1261
|
+
: preview.summary.archive > 0;
|
|
1262
|
+
body.innerHTML = `
|
|
1263
|
+
<div class="maintenance-summary">
|
|
1264
|
+
${summary.map((item) => `<div class="maintenance-metric"><span>${escapeHtml(item.label)}</span><strong>${item.value}</strong></div>`).join('')}
|
|
1265
|
+
</div>
|
|
1266
|
+
<div class="maintenance-records">
|
|
1267
|
+
${action === 'cleanup' || hasChanges ? maintenanceDetails(action, preview) : `<div class="maintenance-empty">${t('maintenanceEmpty')}</div>`}
|
|
1268
|
+
</div>`;
|
|
1269
|
+
footer.innerHTML = `
|
|
1270
|
+
<button class="maintenance-secondary" type="button" data-maintenance-close>${t('cancel')}</button>
|
|
1271
|
+
<button class="maintenance-primary" id="maintenance-execute" type="button" ${hasChanges ? '' : 'disabled'}>
|
|
1272
|
+
<span class="iconify" data-icon="${meta.icon}"></span>${t(meta.confirm)}
|
|
1273
|
+
</button>`;
|
|
1274
|
+
document.getElementById('maintenance-execute')?.addEventListener('click', executeMaintenancePreview);
|
|
1275
|
+
document.getElementById('maintenance-noise-toggle')?.addEventListener('change', (event) => {
|
|
1276
|
+
openMaintenancePreview('cleanup', { includeNoise: event.target.checked });
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
footer.querySelector('[data-maintenance-close]')?.addEventListener('click', closeMaintenanceDialog);
|
|
1280
|
+
if (!dialog.open) dialog.showModal();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async function openMaintenancePreview(action, options = {}) {
|
|
1284
|
+
const dialog = document.getElementById('maintenance-dialog');
|
|
1285
|
+
const meta = maintenanceMeta[action];
|
|
1286
|
+
document.getElementById('maintenance-dialog-kicker').textContent = t('maintenancePreview');
|
|
1287
|
+
document.getElementById('maintenance-dialog-title').textContent = t(meta.title);
|
|
1288
|
+
document.getElementById('maintenance-dialog-body').innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
1289
|
+
document.getElementById('maintenance-dialog-footer').innerHTML = '';
|
|
1290
|
+
if (!dialog.open) dialog.showModal();
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
const preview = await maintenancePost(action, 'preview', action === 'cleanup' ? { includeNoise: options.includeNoise === true } : {});
|
|
1294
|
+
activeMaintenancePreview = { action, preview };
|
|
1295
|
+
renderMaintenanceDialog(action, preview);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
activeMaintenancePreview = null;
|
|
1298
|
+
document.getElementById('maintenance-dialog-body').innerHTML = `
|
|
1299
|
+
<div class="maintenance-empty"><strong>${t('maintenanceFailed')}</strong><span>${escapeHtml(error.message)}</span></div>`;
|
|
1300
|
+
document.getElementById('maintenance-dialog-footer').innerHTML = `
|
|
1301
|
+
<button class="maintenance-secondary" type="button" data-maintenance-close>${t('closeAction')}</button>`;
|
|
1302
|
+
document.querySelector('[data-maintenance-close]')?.addEventListener('click', closeMaintenanceDialog);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function executeMaintenancePreview() {
|
|
1307
|
+
if (!activeMaintenancePreview) return;
|
|
1308
|
+
const { action, preview } = activeMaintenancePreview;
|
|
1309
|
+
const button = document.getElementById('maintenance-execute');
|
|
1310
|
+
button.disabled = true;
|
|
1311
|
+
button.classList.add('busy');
|
|
1312
|
+
try {
|
|
1313
|
+
await maintenancePost(action, 'execute', { payload: preview.payload, token: preview.token });
|
|
1314
|
+
closeMaintenanceDialog();
|
|
1315
|
+
showDashboardToast(t('maintenanceSuccess'));
|
|
1316
|
+
if (currentPage === 'retention') await loadRetention();
|
|
1317
|
+
else await loadObservations();
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
button.disabled = false;
|
|
1320
|
+
button.classList.remove('busy');
|
|
1321
|
+
const message = error.status === 409 ? t('maintenanceChanged') : `${t('maintenanceFailed')}: ${error.message}`;
|
|
1322
|
+
showDashboardToast(message, 'error');
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function closeMaintenanceDialog() {
|
|
1327
|
+
activeMaintenancePreview = null;
|
|
1328
|
+
document.getElementById('maintenance-dialog')?.close();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
onDomReady(() => {
|
|
1332
|
+
const dialog = document.getElementById('maintenance-dialog');
|
|
1333
|
+
dialog?.querySelector('.maintenance-dialog-close')?.addEventListener('click', closeMaintenanceDialog);
|
|
1334
|
+
dialog?.addEventListener('click', (event) => {
|
|
1335
|
+
if (event.target === dialog) closeMaintenanceDialog();
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
window.openMaintenancePreview = openMaintenancePreview;
|
|
1340
|
+
|
|
1074
1341
|
// ============================================================
|
|
1075
1342
|
// Project Switcher — Custom Dropdown
|
|
1076
1343
|
// ============================================================
|
|
@@ -3214,20 +3481,6 @@ let obsTypeFilter = '';
|
|
|
3214
3481
|
let batchMode = false;
|
|
3215
3482
|
let selectedIds = new Set();
|
|
3216
3483
|
|
|
3217
|
-
// Low quality detection (same patterns as CLI cleanup)
|
|
3218
|
-
const LOW_QUALITY_OBS_PATTERNS = [
|
|
3219
|
-
/^Session activity/i,
|
|
3220
|
-
/^Updated \S+\.\w+$/i,
|
|
3221
|
-
/^Created \S+\.\w+$/i,
|
|
3222
|
-
/^Deleted \S+\.\w+$/i,
|
|
3223
|
-
/^Modified \S+\.\w+$/i,
|
|
3224
|
-
/^Ran command:/i,
|
|
3225
|
-
/^Read file:/i,
|
|
3226
|
-
];
|
|
3227
|
-
function isLowQualityObs(title) {
|
|
3228
|
-
return LOW_QUALITY_OBS_PATTERNS.some(p => p.test(title.trim()));
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
3484
|
function renderBatchToolbar() {
|
|
3232
3485
|
const slot = document.getElementById('batch-toolbar-slot');
|
|
3233
3486
|
if (!slot) return;
|
|
@@ -3314,8 +3567,8 @@ async function loadObservations() {
|
|
|
3314
3567
|
<p class="page-subtitle">${allObservations.length} ${t('observationsStored')}</p>
|
|
3315
3568
|
</div>
|
|
3316
3569
|
<div style="display:flex;gap:8px;">
|
|
3317
|
-
<button class="export-btn" id="btn-batch-
|
|
3318
|
-
|
|
3570
|
+
<button class="export-btn" id="btn-batch-select" title="${t('selectRecords')}">
|
|
3571
|
+
<span class="iconify" data-icon="lucide:list-checks"></span> ${t('selectRecords')}
|
|
3319
3572
|
</button>
|
|
3320
3573
|
<button class="export-btn" id="btn-export" title="${t('exportData')}">
|
|
3321
3574
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 7l4 4 4-4M2 12v2h12v-2"/></svg>
|
|
@@ -3324,6 +3577,18 @@ async function loadObservations() {
|
|
|
3324
3577
|
</div>
|
|
3325
3578
|
</div>
|
|
3326
3579
|
|
|
3580
|
+
<section class="maintenance-strip" aria-labelledby="memory-maintenance-title">
|
|
3581
|
+
<div class="maintenance-strip-heading">
|
|
3582
|
+
<span id="memory-maintenance-title">${t('memoryMaintenance')}</span>
|
|
3583
|
+
<small>${t('previewRequired')}</small>
|
|
3584
|
+
</div>
|
|
3585
|
+
<div class="maintenance-actions">
|
|
3586
|
+
<button type="button" onclick="openMaintenancePreview('cleanup')"><span class="iconify" data-icon="lucide:eraser"></span>${t('cleanupAction')}</button>
|
|
3587
|
+
<button type="button" onclick="openMaintenancePreview('consolidate')"><span class="iconify" data-icon="lucide:combine"></span>${t('consolidateAction')}</button>
|
|
3588
|
+
<button type="button" onclick="openMaintenancePreview('deduplicate')"><span class="iconify" data-icon="lucide:copy-minus"></span>${t('deduplicateAction')}</button>
|
|
3589
|
+
</div>
|
|
3590
|
+
</section>
|
|
3591
|
+
|
|
3327
3592
|
<div id="batch-toolbar-slot"></div>
|
|
3328
3593
|
|
|
3329
3594
|
<div class="search-bar">
|
|
@@ -3341,18 +3606,10 @@ async function loadObservations() {
|
|
|
3341
3606
|
window.open(`/api/export${sep}`, '_blank');
|
|
3342
3607
|
});
|
|
3343
3608
|
|
|
3344
|
-
//
|
|
3345
|
-
document.getElementById('btn-batch-
|
|
3609
|
+
// Manual selection remains separate from the shared cleanup pipeline.
|
|
3610
|
+
document.getElementById('btn-batch-select').addEventListener('click', () => {
|
|
3346
3611
|
batchMode = !batchMode;
|
|
3347
|
-
|
|
3348
|
-
// Auto-select low quality ones
|
|
3349
|
-
selectedIds.clear();
|
|
3350
|
-
allObservations.forEach(obs => {
|
|
3351
|
-
if (isLowQualityObs(obs.title || '')) selectedIds.add(obs.id);
|
|
3352
|
-
});
|
|
3353
|
-
} else {
|
|
3354
|
-
selectedIds.clear();
|
|
3355
|
-
}
|
|
3612
|
+
selectedIds.clear();
|
|
3356
3613
|
renderObsList();
|
|
3357
3614
|
renderBatchToolbar();
|
|
3358
3615
|
});
|
|
@@ -3405,18 +3662,16 @@ function renderObsList() {
|
|
|
3405
3662
|
}
|
|
3406
3663
|
|
|
3407
3664
|
list.innerHTML = filtered.map(obs => {
|
|
3408
|
-
const isLow = isLowQualityObs(obs.title || '');
|
|
3409
3665
|
const isSelected = selectedIds.has(obs.id);
|
|
3410
3666
|
const hl = (text) => obsFilter ? escapeHtml(text).replace(new RegExp(`(${escapeHtml(obsFilter).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>') : escapeHtml(text);
|
|
3411
3667
|
return `
|
|
3412
|
-
<div class="obs-card
|
|
3668
|
+
<div class="obs-card" data-obs-id="${obs.id}" onclick="toggleObsDetail(${obs.id})" style="cursor:pointer;">
|
|
3413
3669
|
<div class="obs-card-header">
|
|
3414
3670
|
${batchMode ? `<input type="checkbox" class="obs-checkbox" ${isSelected ? 'checked' : ''} onclick="event.stopPropagation(); toggleObsSelect(${obs.id});" />` : ''}
|
|
3415
3671
|
<span class="obs-card-id">#${obs.id}</span>
|
|
3416
3672
|
<span class="type-badge" data-type="${obs.type || 'unknown'}">
|
|
3417
3673
|
${typeIcons[obs.type] || '[UNKNOWN]'} ${obs.type || t('unknown')}
|
|
3418
3674
|
</span>
|
|
3419
|
-
${isLow ? '<span class="low-quality-badge">' + t('lowQuality') + '</span>' : ''}
|
|
3420
3675
|
<span class="obs-card-title">${hl(obs.title || t('untitled'))}</span>
|
|
3421
3676
|
<span class="obs-expand-icon">▼</span>
|
|
3422
3677
|
</div>
|
|
@@ -3461,9 +3716,14 @@ async function loadRetention() {
|
|
|
3461
3716
|
const { summary, items } = data;
|
|
3462
3717
|
|
|
3463
3718
|
container.innerHTML = `
|
|
3464
|
-
<div class="page-header">
|
|
3465
|
-
<
|
|
3466
|
-
|
|
3719
|
+
<div class="page-header page-header-actions">
|
|
3720
|
+
<div>
|
|
3721
|
+
<h1 class="page-title">${t('memoryRetention')}</h1>
|
|
3722
|
+
<p class="page-subtitle">${t('retentionSubtitle')}</p>
|
|
3723
|
+
</div>
|
|
3724
|
+
<button class="export-btn" type="button" onclick="openMaintenancePreview('retention')" ${summary.archive > 0 ? '' : 'disabled'}>
|
|
3725
|
+
<span class="iconify" data-icon="lucide:archive"></span>${t('archiveAction')}
|
|
3726
|
+
</button>
|
|
3467
3727
|
</div>
|
|
3468
3728
|
|
|
3469
3729
|
<div class="retention-summary">
|
|
@@ -182,6 +182,23 @@
|
|
|
182
182
|
<div id="page-team" class="page"></div>
|
|
183
183
|
</main>
|
|
184
184
|
|
|
185
|
+
<dialog id="maintenance-dialog" class="maintenance-dialog" aria-labelledby="maintenance-dialog-title">
|
|
186
|
+
<div class="maintenance-dialog-shell">
|
|
187
|
+
<header class="maintenance-dialog-header">
|
|
188
|
+
<div>
|
|
189
|
+
<div class="maintenance-dialog-kicker" id="maintenance-dialog-kicker"></div>
|
|
190
|
+
<h2 id="maintenance-dialog-title"></h2>
|
|
191
|
+
</div>
|
|
192
|
+
<button class="icon-btn maintenance-dialog-close" type="button" aria-label="Close" title="Close">
|
|
193
|
+
<span class="iconify" data-icon="lucide:x"></span>
|
|
194
|
+
</button>
|
|
195
|
+
</header>
|
|
196
|
+
<div id="maintenance-dialog-body" class="maintenance-dialog-body"></div>
|
|
197
|
+
<footer id="maintenance-dialog-footer" class="maintenance-dialog-footer"></footer>
|
|
198
|
+
</div>
|
|
199
|
+
</dialog>
|
|
200
|
+
<div id="dashboard-toast" class="dashboard-toast" role="status" aria-live="polite"></div>
|
|
201
|
+
|
|
185
202
|
<!-- Apache ECharts 5 — Knowledge Graph renderer -->
|
|
186
203
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
|
187
204
|
<!-- Cytoscape.js + Dagre — legacy entity-graph fallback -->
|