superlocalmemory 3.8.14 → 4.0.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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -137
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +32 -5
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -424
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/{M034_scene_fact_members.py → M039_scene_fact_members.py} +15 -5
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -4
- package/src/superlocalmemory/storage/schema.py +10 -2
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -261,9 +261,13 @@ function openMemoryDetail(mem, source) {
|
|
|
261
261
|
forgetBtn.className = 'btn btn-outline-warning btn-sm';
|
|
262
262
|
forgetBtn.innerHTML = '<i class="bi bi-archive"></i> Forget';
|
|
263
263
|
forgetBtn.title = 'Archive this memory — hidden from recall but recoverable';
|
|
264
|
-
forgetBtn.onclick = function() {
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
forgetBtn.onclick = async function() {
|
|
265
|
+
var confirmed = await confirmDestructive({
|
|
266
|
+
title: 'Forget memory',
|
|
267
|
+
target: mem.content ? mem.content.slice(0, 80) : 'Memory #' + mem.id,
|
|
268
|
+
consequence: 'Archived — hidden from recall but recoverable.',
|
|
269
|
+
});
|
|
270
|
+
if (!confirmed) return;
|
|
267
271
|
forgetBtn.disabled = true;
|
|
268
272
|
fetch('/api/memories/' + encodeURIComponent(mem.id) + '/forget',
|
|
269
273
|
{method: 'POST'})
|
|
@@ -323,13 +327,147 @@ function openMemoryDetail(mem, source) {
|
|
|
323
327
|
};
|
|
324
328
|
actionsDiv.appendChild(mergeBtn);
|
|
325
329
|
|
|
330
|
+
// Task F: Set Scope — PATCH /api/memories/{fact_id}/scope
|
|
331
|
+
// Lets the user change personal → shared → global visibility.
|
|
332
|
+
var scopeBtn = document.createElement('button');
|
|
333
|
+
scopeBtn.className = 'btn btn-outline-secondary btn-sm';
|
|
334
|
+
scopeBtn.innerHTML = '<i class="bi bi-globe"></i> Set Scope…';
|
|
335
|
+
scopeBtn.title = 'Change memory visibility: personal, shared, or global';
|
|
336
|
+
(function() {
|
|
337
|
+
var scopeFormEl = null;
|
|
338
|
+
scopeBtn.addEventListener('click', function() {
|
|
339
|
+
// Toggle the inline scope form
|
|
340
|
+
if (scopeFormEl) {
|
|
341
|
+
scopeFormEl.remove();
|
|
342
|
+
scopeFormEl = null;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
scopeFormEl = document.createElement('div');
|
|
346
|
+
scopeFormEl.className = 'mt-2 p-2 border rounded';
|
|
347
|
+
scopeFormEl.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;width:100%';
|
|
348
|
+
var scopeSel = document.createElement('select');
|
|
349
|
+
scopeSel.className = 'form-select form-select-sm';
|
|
350
|
+
scopeSel.style.width = 'auto';
|
|
351
|
+
['personal', 'shared', 'global'].forEach(function(s) {
|
|
352
|
+
var opt = document.createElement('option');
|
|
353
|
+
opt.value = s;
|
|
354
|
+
opt.textContent = s;
|
|
355
|
+
if (s === (mem.scope || 'personal')) opt.selected = true;
|
|
356
|
+
scopeSel.appendChild(opt);
|
|
357
|
+
});
|
|
358
|
+
var sharedInput = document.createElement('input');
|
|
359
|
+
sharedInput.className = 'form-control form-control-sm';
|
|
360
|
+
sharedInput.placeholder = 'shared_with (profile1,profile2)';
|
|
361
|
+
sharedInput.style.flex = '1';
|
|
362
|
+
sharedInput.style.display = scopeSel.value === 'shared' ? '' : 'none';
|
|
363
|
+
if (mem.shared_with) {
|
|
364
|
+
try {
|
|
365
|
+
var sw = typeof mem.shared_with === 'string'
|
|
366
|
+
? JSON.parse(mem.shared_with) : mem.shared_with;
|
|
367
|
+
sharedInput.value = Array.isArray(sw) ? sw.join(',') : String(sw);
|
|
368
|
+
} catch(e) { sharedInput.value = String(mem.shared_with || ''); }
|
|
369
|
+
}
|
|
370
|
+
scopeSel.addEventListener('change', function() {
|
|
371
|
+
sharedInput.style.display = scopeSel.value === 'shared' ? '' : 'none';
|
|
372
|
+
});
|
|
373
|
+
var saveBtn = document.createElement('button');
|
|
374
|
+
saveBtn.className = 'btn btn-sm btn-primary';
|
|
375
|
+
saveBtn.textContent = 'Save';
|
|
376
|
+
saveBtn.addEventListener('click', function() {
|
|
377
|
+
var scope = scopeSel.value;
|
|
378
|
+
var sharedWith = scope === 'shared' ? sharedInput.value : '';
|
|
379
|
+
saveBtn.disabled = true;
|
|
380
|
+
saveBtn.textContent = 'Saving…';
|
|
381
|
+
fetch('/api/memories/' + encodeURIComponent(mem.fact_id || mem.id) + '/scope', {
|
|
382
|
+
method: 'PATCH',
|
|
383
|
+
headers: {'Content-Type': 'application/json'},
|
|
384
|
+
body: JSON.stringify({scope: scope, shared_with: sharedWith}),
|
|
385
|
+
}).then(function(r) { return r.json(); })
|
|
386
|
+
.then(function(d) {
|
|
387
|
+
if (d.success) {
|
|
388
|
+
mem.scope = scope;
|
|
389
|
+
if (typeof showToast === 'function') showToast('Scope set to ' + scope);
|
|
390
|
+
scopeFormEl.remove(); scopeFormEl = null;
|
|
391
|
+
if (typeof loadMemories === 'function') setTimeout(loadMemories, 300);
|
|
392
|
+
} else {
|
|
393
|
+
if (typeof showToast === 'function') showToast('Scope update failed: ' + (d.detail || d.error || 'unknown'));
|
|
394
|
+
saveBtn.disabled = false; saveBtn.textContent = 'Save';
|
|
395
|
+
}
|
|
396
|
+
}).catch(function() {
|
|
397
|
+
if (typeof showToast === 'function') showToast('Network error setting scope.');
|
|
398
|
+
saveBtn.disabled = false; saveBtn.textContent = 'Save';
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
scopeFormEl.appendChild(scopeSel);
|
|
402
|
+
scopeFormEl.appendChild(sharedInput);
|
|
403
|
+
scopeFormEl.appendChild(saveBtn);
|
|
404
|
+
actionsDiv.insertAdjacentElement('afterend', scopeFormEl);
|
|
405
|
+
});
|
|
406
|
+
}());
|
|
407
|
+
actionsDiv.appendChild(scopeBtn);
|
|
408
|
+
|
|
409
|
+
// Task G: Pin — POST /api/tiers/pin — keep this fact in the active tier forever
|
|
410
|
+
var factIdForTier = mem.fact_id || mem.id;
|
|
411
|
+
var pinBtn = document.createElement('button');
|
|
412
|
+
pinBtn.className = 'btn btn-outline-success btn-sm';
|
|
413
|
+
pinBtn.innerHTML = '<i class="bi bi-pin-fill"></i> Pin';
|
|
414
|
+
pinBtn.title = 'Pin to active tier — this fact will not be demoted by lifecycle';
|
|
415
|
+
pinBtn.addEventListener('click', function() {
|
|
416
|
+
pinBtn.disabled = true;
|
|
417
|
+
fetch('/api/tiers/pin', {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: {'Content-Type': 'application/json'},
|
|
420
|
+
body: JSON.stringify({fact_id: factIdForTier, reason: 'pinned from dashboard'}),
|
|
421
|
+
}).then(function(r) { return r.json(); })
|
|
422
|
+
.then(function(d) {
|
|
423
|
+
pinBtn.disabled = false;
|
|
424
|
+
if (typeof showToast === 'function') {
|
|
425
|
+
showToast(d && d.success ? 'Fact pinned to active tier.' : 'Pin failed: ' + (d && (d.detail || d.error) || 'unknown'));
|
|
426
|
+
}
|
|
427
|
+
}).catch(function() {
|
|
428
|
+
pinBtn.disabled = false;
|
|
429
|
+
if (typeof showToast === 'function') showToast('Network error pinning fact.');
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
actionsDiv.appendChild(pinBtn);
|
|
433
|
+
|
|
434
|
+
// Task G: Unpin — POST /api/tiers/unpin — allows normal tier demotion again
|
|
435
|
+
var unpinBtn = document.createElement('button');
|
|
436
|
+
unpinBtn.className = 'btn btn-outline-warning btn-sm';
|
|
437
|
+
unpinBtn.innerHTML = '<i class="bi bi-pin-angle"></i> Unpin';
|
|
438
|
+
unpinBtn.title = 'Unpin — allow normal lifecycle tier demotion';
|
|
439
|
+
unpinBtn.addEventListener('click', function() {
|
|
440
|
+
unpinBtn.disabled = true;
|
|
441
|
+
fetch('/api/tiers/unpin', {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: {'Content-Type': 'application/json'},
|
|
444
|
+
body: JSON.stringify({fact_id: factIdForTier, reason: ''}),
|
|
445
|
+
}).then(function(r) { return r.json(); })
|
|
446
|
+
.then(function(d) {
|
|
447
|
+
unpinBtn.disabled = false;
|
|
448
|
+
if (typeof showToast === 'function') {
|
|
449
|
+
showToast(d && d.success ? 'Fact unpinned — will age normally.' : 'Unpin failed: ' + (d && (d.detail || d.error) || 'unknown'));
|
|
450
|
+
}
|
|
451
|
+
}).catch(function() {
|
|
452
|
+
unpinBtn.disabled = false;
|
|
453
|
+
if (typeof showToast === 'function') showToast('Network error unpinning fact.');
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
actionsDiv.appendChild(unpinBtn);
|
|
457
|
+
|
|
326
458
|
// Delete button — always available (hard delete, irreversible)
|
|
327
459
|
var deleteBtn = document.createElement('button');
|
|
328
460
|
deleteBtn.className = 'btn btn-outline-danger btn-sm';
|
|
329
461
|
deleteBtn.innerHTML = '<i class="bi bi-trash"></i> Delete';
|
|
330
462
|
deleteBtn.title = 'Permanently delete (cannot be undone) — prefer Forget';
|
|
331
|
-
deleteBtn.onclick = function() {
|
|
332
|
-
|
|
463
|
+
deleteBtn.onclick = async function() {
|
|
464
|
+
var confirmed = await confirmDestructive({
|
|
465
|
+
title: 'Delete memory',
|
|
466
|
+
target: mem.content ? mem.content.slice(0, 80) : 'Memory #' + mem.id,
|
|
467
|
+
consequence: 'Permanently deleted — this cannot be undone.',
|
|
468
|
+
confirmLabel: 'Delete',
|
|
469
|
+
});
|
|
470
|
+
if (!confirmed) return;
|
|
333
471
|
fetch('/api/memories/' + encodeURIComponent(mem.id), {method: 'DELETE'})
|
|
334
472
|
.then(function(r) { return r.json(); })
|
|
335
473
|
.then(function(d) {
|
|
@@ -432,6 +570,135 @@ function addDetailTagsRow(parent, label, tags) {
|
|
|
432
570
|
parent.appendChild(dd);
|
|
433
571
|
}
|
|
434
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Show a shared confirmation modal for destructive dashboard actions.
|
|
575
|
+
* Creates the modal element on first call; reuses it on subsequent calls.
|
|
576
|
+
*
|
|
577
|
+
* @param {object} opts
|
|
578
|
+
* @param {string} opts.title Short header, e.g. "Delete profile"
|
|
579
|
+
* @param {string} opts.target Exact item being acted on, e.g. "my-project"
|
|
580
|
+
* @param {string} opts.consequence What happens, e.g. "Memories moved to default profile"
|
|
581
|
+
* @param {string} [opts.confirmLabel] Confirm button text (default: "Confirm")
|
|
582
|
+
* @param {string} [opts.confirmationText] Exact text required to unlock confirmation
|
|
583
|
+
* @returns {Promise<boolean>} Resolves true when confirmed, false when cancelled
|
|
584
|
+
*/
|
|
585
|
+
var activeDestructiveConfirmation = null;
|
|
586
|
+
|
|
587
|
+
function confirmDestructive(opts) {
|
|
588
|
+
return new Promise(function(resolve) {
|
|
589
|
+
if (activeDestructiveConfirmation) {
|
|
590
|
+
activeDestructiveConfirmation.cancel();
|
|
591
|
+
}
|
|
592
|
+
var settled = false;
|
|
593
|
+
var MODAL_ID = 'slm-confirm-destructive-modal';
|
|
594
|
+
var modalEl = document.getElementById(MODAL_ID);
|
|
595
|
+
|
|
596
|
+
if (!modalEl) {
|
|
597
|
+
modalEl = document.createElement('div');
|
|
598
|
+
modalEl.id = MODAL_ID;
|
|
599
|
+
modalEl.className = 'modal fade';
|
|
600
|
+
modalEl.setAttribute('tabindex', '-1');
|
|
601
|
+
modalEl.setAttribute('aria-modal', 'true');
|
|
602
|
+
modalEl.setAttribute('role', 'dialog');
|
|
603
|
+
modalEl.innerHTML =
|
|
604
|
+
'<div class="modal-dialog modal-dialog-centered">' +
|
|
605
|
+
'<div class="modal-content">' +
|
|
606
|
+
'<div class="modal-header border-0 pb-1">' +
|
|
607
|
+
'<h5 class="modal-title slm-cd-title text-danger"></h5>' +
|
|
608
|
+
'<button type="button" class="btn-close"' +
|
|
609
|
+
' data-slm-cd-action="cancel" aria-label="Close"></button>' +
|
|
610
|
+
'</div>' +
|
|
611
|
+
'<div class="modal-body pt-1">' +
|
|
612
|
+
'<p class="slm-cd-target fw-semibold mb-1"></p>' +
|
|
613
|
+
'<p class="slm-cd-consequence text-muted small mb-2"></p>' +
|
|
614
|
+
'<label class="form-label small mb-1" for="slm-cd-challenge">' +
|
|
615
|
+
'Type <code class="slm-cd-confirmation-text"></code> to continue</label>' +
|
|
616
|
+
'<input id="slm-cd-challenge" class="form-control form-control-sm slm-cd-challenge"' +
|
|
617
|
+
' type="text" autocomplete="off" spellcheck="false">' +
|
|
618
|
+
'</div>' +
|
|
619
|
+
'<div class="modal-footer border-0 pt-0">' +
|
|
620
|
+
'<button type="button" class="btn btn-secondary btn-sm"' +
|
|
621
|
+
' data-slm-cd-action="cancel">Cancel</button>' +
|
|
622
|
+
'<button type="button" class="btn btn-danger btn-sm"' +
|
|
623
|
+
' data-slm-cd-action="confirm">Confirm</button>' +
|
|
624
|
+
'</div>' +
|
|
625
|
+
'</div>' +
|
|
626
|
+
'</div>';
|
|
627
|
+
document.body.appendChild(modalEl);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
var titleEl = modalEl.querySelector('.slm-cd-title');
|
|
631
|
+
var targetEl = modalEl.querySelector('.slm-cd-target');
|
|
632
|
+
var consequenceEl = modalEl.querySelector('.slm-cd-consequence');
|
|
633
|
+
var confirmationTextEl = modalEl.querySelector('.slm-cd-confirmation-text');
|
|
634
|
+
var challengeInput = modalEl.querySelector('.slm-cd-challenge');
|
|
635
|
+
var confirmBtn = modalEl.querySelector('[data-slm-cd-action="confirm"]');
|
|
636
|
+
var confirmationText = opts.confirmationText || opts.target || 'CONFIRM';
|
|
637
|
+
|
|
638
|
+
if (titleEl) titleEl.textContent = opts.title || 'Confirm action';
|
|
639
|
+
if (targetEl) targetEl.textContent = opts.target || '';
|
|
640
|
+
if (consequenceEl) consequenceEl.textContent = opts.consequence || '';
|
|
641
|
+
if (confirmationTextEl) confirmationTextEl.textContent = confirmationText;
|
|
642
|
+
if (challengeInput) challengeInput.value = '';
|
|
643
|
+
if (confirmBtn) {
|
|
644
|
+
confirmBtn.textContent = opts.confirmLabel || 'Confirm';
|
|
645
|
+
confirmBtn.disabled = true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
var bsModal = null;
|
|
649
|
+
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
|
650
|
+
bsModal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function settle(result, hideModal) {
|
|
654
|
+
if (settled) return;
|
|
655
|
+
settled = true;
|
|
656
|
+
if (activeDestructiveConfirmation === confirmationSession) {
|
|
657
|
+
activeDestructiveConfirmation = null;
|
|
658
|
+
}
|
|
659
|
+
modalEl.removeEventListener('click', onAction);
|
|
660
|
+
if (challengeInput) challengeInput.removeEventListener('input', onChallengeInput);
|
|
661
|
+
if (bsModal) {
|
|
662
|
+
modalEl.removeEventListener('hidden.bs.modal', onHide);
|
|
663
|
+
modalEl.removeEventListener('shown.bs.modal', onShown);
|
|
664
|
+
if (hideModal !== false) bsModal.hide();
|
|
665
|
+
}
|
|
666
|
+
resolve(result);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function onAction(e) {
|
|
670
|
+
var actionEl = e.target.closest('[data-slm-cd-action]');
|
|
671
|
+
if (!actionEl) return;
|
|
672
|
+
if (actionEl.getAttribute('data-slm-cd-action') === 'confirm' &&
|
|
673
|
+
(!challengeInput || challengeInput.value !== confirmationText)) return;
|
|
674
|
+
settle(actionEl.getAttribute('data-slm-cd-action') === 'confirm');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function onChallengeInput() {
|
|
678
|
+
if (confirmBtn) confirmBtn.disabled = challengeInput.value !== confirmationText;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function onShown() {
|
|
682
|
+
if (challengeInput) challengeInput.focus();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function onHide() { settle(false); }
|
|
686
|
+
|
|
687
|
+
var confirmationSession = {
|
|
688
|
+
cancel: function() { settle(false, false); }
|
|
689
|
+
};
|
|
690
|
+
activeDestructiveConfirmation = confirmationSession;
|
|
691
|
+
|
|
692
|
+
modalEl.addEventListener('click', onAction);
|
|
693
|
+
if (challengeInput) challengeInput.addEventListener('input', onChallengeInput);
|
|
694
|
+
if (bsModal) {
|
|
695
|
+
modalEl.addEventListener('hidden.bs.modal', onHide, { once: true });
|
|
696
|
+
modalEl.addEventListener('shown.bs.modal', onShown, { once: true });
|
|
697
|
+
bsModal.show();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
435
702
|
function copyMemoryToClipboard() {
|
|
436
703
|
if (!currentMemoryDetail) return;
|
|
437
704
|
var text = currentMemoryDetail.content || currentMemoryDetail.summary || '';
|
|
@@ -642,14 +642,21 @@
|
|
|
642
642
|
}
|
|
643
643
|
|
|
644
644
|
function doDisconnect(root, destId) {
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
window.confirmDestructive({
|
|
646
|
+
title: 'Disconnect cloud destination',
|
|
647
|
+
target: destId,
|
|
648
|
+
consequence: 'Existing backups are not deleted.',
|
|
649
|
+
confirmLabel: 'Disconnect',
|
|
650
|
+
}).then(function(confirmed) {
|
|
651
|
+
if (!confirmed) return;
|
|
652
|
+
authMutation('/api/backup/disconnect/' + encodeURIComponent(destId), 'DELETE')
|
|
647
653
|
.then(function (r) { return r.json(); })
|
|
648
654
|
.then(function (d) {
|
|
649
655
|
toast(d.success ? 'Disconnected' : 'Disconnect failed: ' + esc(d.error || ''), !d.success);
|
|
650
656
|
loadDestinations(root);
|
|
651
657
|
})
|
|
652
658
|
.catch(function () { toast('Disconnect failed', true); });
|
|
659
|
+
});
|
|
653
660
|
}
|
|
654
661
|
|
|
655
662
|
function openOAuth(root, oauthPath, providerLabel) {
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// od-compliance-ext.js — Compliance tab extensions v1.0
|
|
2
|
+
// Extends the OD Operations compliance pane with:
|
|
3
|
+
// Task A: Entity-level GDPR erasure (POST /api/compliance/gdpr/erase-entity)
|
|
4
|
+
// Task B: Erasure Receipts panel (GET /api/compliance/receipts + verify)
|
|
5
|
+
//
|
|
6
|
+
// This module hooks into odRenderOperations by patching window.odRenderOperations
|
|
7
|
+
// to inject the new panels into the compliance tab after the base render.
|
|
8
|
+
//
|
|
9
|
+
// Security: all user-supplied strings go through esc() before innerHTML insertion.
|
|
10
|
+
// Confirm guard on entity erase (typed confirmation + confirmDestructive).
|
|
11
|
+
//
|
|
12
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
|
|
13
|
+
|
|
14
|
+
/* global window, document, fetch, Promise */
|
|
15
|
+
(function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function esc(s) {
|
|
21
|
+
return String(s == null ? '' : s)
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fmtTime(ts) {
|
|
30
|
+
if (!ts) return '—';
|
|
31
|
+
try {
|
|
32
|
+
var d = typeof ts === 'number' ? new Date(ts * 1000) : new Date(ts);
|
|
33
|
+
return d.toLocaleString();
|
|
34
|
+
} catch (e) { return String(ts); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Task A: Entity Erase Card ────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
var ENTITY_ERASE_ID = 'od-entity-erase-card';
|
|
40
|
+
|
|
41
|
+
function buildEntityEraseCard() {
|
|
42
|
+
var card = document.createElement('div');
|
|
43
|
+
card.id = ENTITY_ERASE_ID;
|
|
44
|
+
card.className = 'card';
|
|
45
|
+
card.style.marginTop = '16px';
|
|
46
|
+
// Using textContent / controlled HTML (only static strings + esc'd values below)
|
|
47
|
+
card.innerHTML =
|
|
48
|
+
'<div class="card-head">' +
|
|
49
|
+
'<h3>Entity-level GDPR erasure</h3>' +
|
|
50
|
+
'</div>' +
|
|
51
|
+
'<div class="card-pad">' +
|
|
52
|
+
'<p class="muted" style="font-size:13px" id="od-entity-erase-summary">' +
|
|
53
|
+
'Erase all facts mentioning a specific named entity (GDPR Art. 17). ' +
|
|
54
|
+
'Enter the entity name exactly; you will need to confirm it. This is IRREVERSIBLE.' +
|
|
55
|
+
'</p>' +
|
|
56
|
+
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px">' +
|
|
57
|
+
'<input id="od-entity-erase-input" type="text" maxlength="200" ' +
|
|
58
|
+
'placeholder="Entity name…" ' +
|
|
59
|
+
'style="flex:1;min-width:180px;padding:7px 10px;border-radius:8px;' +
|
|
60
|
+
'border:1px solid var(--border);background:var(--card);color:var(--fg);font-size:13px">' +
|
|
61
|
+
'<button id="od-entity-erase-btn" class="btn sm" ' +
|
|
62
|
+
'style="border-color:#ff6b6b;color:#ff6b6b">Erase entity…</button>' +
|
|
63
|
+
'</div>' +
|
|
64
|
+
'</div>';
|
|
65
|
+
return card;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wireEntityEraseCard() {
|
|
69
|
+
var btn = document.getElementById('od-entity-erase-btn');
|
|
70
|
+
var input = document.getElementById('od-entity-erase-input');
|
|
71
|
+
var summary = document.getElementById('od-entity-erase-summary');
|
|
72
|
+
if (!btn || !input) return;
|
|
73
|
+
|
|
74
|
+
btn.addEventListener('click', function () {
|
|
75
|
+
var entityName = (input.value || '').trim();
|
|
76
|
+
if (!entityName) {
|
|
77
|
+
if (summary) summary.textContent = 'Enter an entity name first.';
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Step 1: typed-confirmation prompt (same guard as profile erase)
|
|
81
|
+
var typed = window.prompt(
|
|
82
|
+
'This PERMANENTLY erases all facts mentioning entity "' + entityName + '".\n' +
|
|
83
|
+
'This cannot be undone.\n\nType the entity name to confirm:'
|
|
84
|
+
);
|
|
85
|
+
if (typed === null) return; // cancelled
|
|
86
|
+
if (typed !== entityName) {
|
|
87
|
+
if (summary) summary.textContent = 'Erase cancelled — name did not match.';
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Step 2: confirmDestructive modal
|
|
91
|
+
window.confirmDestructive({
|
|
92
|
+
title: 'Erase entity',
|
|
93
|
+
target: entityName,
|
|
94
|
+
consequence: 'All facts mentioning this entity will be permanently erased.',
|
|
95
|
+
confirmLabel: 'Erase entity',
|
|
96
|
+
}).then(function (confirmed) {
|
|
97
|
+
if (!confirmed) return;
|
|
98
|
+
btn.disabled = true;
|
|
99
|
+
btn.textContent = 'Erasing…';
|
|
100
|
+
if (summary) summary.textContent = 'Erasing entity "' + esc(entityName) + '"…';
|
|
101
|
+
fetch('/api/compliance/gdpr/erase-entity', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ entity_name: entityName, confirm: entityName }),
|
|
105
|
+
})
|
|
106
|
+
.then(function (r) { return r.json(); })
|
|
107
|
+
.then(function (d) {
|
|
108
|
+
if (!summary) return;
|
|
109
|
+
if (d && d.success) {
|
|
110
|
+
var parts = [];
|
|
111
|
+
if (d.facts != null) parts.push('facts erased: ' + d.facts);
|
|
112
|
+
if (d.entity != null) parts.push('entity: ' + esc(String(d.entity)));
|
|
113
|
+
summary.textContent = 'Entity erased — ' + (parts.join(', ') || 'done') + '.';
|
|
114
|
+
input.value = '';
|
|
115
|
+
// Reload receipts panel to reflect new receipt
|
|
116
|
+
loadReceiptsPanel();
|
|
117
|
+
} else {
|
|
118
|
+
summary.textContent = 'Erase failed: ' + esc(String((d && d.error) || 'unknown'));
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.catch(function () {
|
|
122
|
+
if (summary) summary.textContent = 'Erase request failed. Check console.';
|
|
123
|
+
})
|
|
124
|
+
.finally(function () {
|
|
125
|
+
btn.disabled = false;
|
|
126
|
+
btn.textContent = 'Erase entity…';
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Task B: Erasure Receipts Panel ───────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
var RECEIPTS_ID = 'od-receipts-card';
|
|
135
|
+
|
|
136
|
+
function buildReceiptsCard() {
|
|
137
|
+
var card = document.createElement('div');
|
|
138
|
+
card.id = RECEIPTS_ID;
|
|
139
|
+
card.className = 'card';
|
|
140
|
+
card.style.marginTop = '16px';
|
|
141
|
+
card.innerHTML =
|
|
142
|
+
'<div class="card-head">' +
|
|
143
|
+
'<h3>Erasure receipts</h3>' +
|
|
144
|
+
'<div class="spacer"></div>' +
|
|
145
|
+
'<button class="btn sm" id="od-receipts-refresh-btn">Refresh</button>' +
|
|
146
|
+
'</div>' +
|
|
147
|
+
'<div class="card-pad">' +
|
|
148
|
+
'<table class="tbl" id="od-receipts-tbl">' +
|
|
149
|
+
'<thead><tr>' +
|
|
150
|
+
'<th>ID</th>' +
|
|
151
|
+
'<th>Type</th>' +
|
|
152
|
+
'<th>Subject</th>' +
|
|
153
|
+
'<th>State</th>' +
|
|
154
|
+
'<th>Facts</th>' +
|
|
155
|
+
'<th>Requested</th>' +
|
|
156
|
+
'<th></th>' +
|
|
157
|
+
'</tr></thead>' +
|
|
158
|
+
'<tbody id="od-receipts-body">' +
|
|
159
|
+
'<tr><td colspan="7" class="dim" style="text-align:center;padding:20px">Loading…</td></tr>' +
|
|
160
|
+
'</tbody>' +
|
|
161
|
+
'</table>' +
|
|
162
|
+
'</div>';
|
|
163
|
+
return card;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function loadReceiptsPanel() {
|
|
167
|
+
var tbody = document.getElementById('od-receipts-body');
|
|
168
|
+
if (!tbody) return;
|
|
169
|
+
tbody.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">Loading…</td></tr>';
|
|
170
|
+
fetch('/api/compliance/receipts?limit=50')
|
|
171
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
172
|
+
.then(function (d) {
|
|
173
|
+
if (!d || !d.available) {
|
|
174
|
+
tbody.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">Receipts unavailable.</td></tr>';
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
var receipts = d.receipts || [];
|
|
178
|
+
if (receipts.length === 0) {
|
|
179
|
+
tbody.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">No erasure receipts yet.</td></tr>';
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
tbody.innerHTML = receipts.map(function (r) {
|
|
183
|
+
var shortId = esc(String(r.erasure_id || '').slice(0, 8)) + '…';
|
|
184
|
+
var stateClass = r.state === 'COMPLETE' ? 'ok' : 'danger';
|
|
185
|
+
var allOk = r.all_erased ? '<span class="badge ok">all erased</span>' : '<span class="badge warn">partial</span>';
|
|
186
|
+
return (
|
|
187
|
+
'<tr>' +
|
|
188
|
+
'<td class="mono dim" style="font-size:11px" title="' + esc(r.erasure_id || '') + '">' + shortId + '</td>' +
|
|
189
|
+
'<td><span class="badge neutral">' + esc(r.subject_type || '') + '</span></td>' +
|
|
190
|
+
'<td class="dim" style="font-size:12px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
|
|
191
|
+
esc(r.subject_id || '') +
|
|
192
|
+
'</td>' +
|
|
193
|
+
'<td><span class="badge ' + stateClass + '">' + esc(r.state || '') + '</span></td>' +
|
|
194
|
+
'<td class="num dim">' + esc(String(r.fact_count || 0)) + '</td>' +
|
|
195
|
+
'<td class="dim" style="font-size:11px">' + esc(fmtTime(r.requested_at)) + '</td>' +
|
|
196
|
+
'<td><button class="btn sm ghost" data-verify-id="' + esc(r.erasure_id || '') + '">Verify</button></td>' +
|
|
197
|
+
'</tr>'
|
|
198
|
+
);
|
|
199
|
+
}).join('');
|
|
200
|
+
// Wire verify buttons
|
|
201
|
+
tbody.querySelectorAll('[data-verify-id]').forEach(function (btn) {
|
|
202
|
+
btn.addEventListener('click', function () {
|
|
203
|
+
var eid = btn.getAttribute('data-verify-id');
|
|
204
|
+
btn.disabled = true;
|
|
205
|
+
btn.textContent = '…';
|
|
206
|
+
fetch('/api/compliance/receipts/' + encodeURIComponent(eid) + '/verify')
|
|
207
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
208
|
+
.then(function (d) {
|
|
209
|
+
if (d && d.verified === true) {
|
|
210
|
+
btn.textContent = 'Verified ✓';
|
|
211
|
+
btn.style.color = 'var(--ok)';
|
|
212
|
+
} else if (d && d.verified === false) {
|
|
213
|
+
btn.textContent = 'Tampered!';
|
|
214
|
+
btn.style.color = 'var(--danger)';
|
|
215
|
+
} else {
|
|
216
|
+
btn.textContent = 'Error';
|
|
217
|
+
}
|
|
218
|
+
btn.disabled = false;
|
|
219
|
+
})
|
|
220
|
+
.catch(function () { btn.textContent = 'Error'; btn.disabled = false; });
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
})
|
|
224
|
+
.catch(function () {
|
|
225
|
+
var tb = document.getElementById('od-receipts-body');
|
|
226
|
+
if (tb) tb.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">Failed to load receipts.</td></tr>';
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function wireReceiptsPanel() {
|
|
231
|
+
var refreshBtn = document.getElementById('od-receipts-refresh-btn');
|
|
232
|
+
if (refreshBtn) {
|
|
233
|
+
refreshBtn.addEventListener('click', loadReceiptsPanel);
|
|
234
|
+
}
|
|
235
|
+
loadReceiptsPanel();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Injection Hook ───────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Inject the two new panels into the compliance tab section.
|
|
242
|
+
* Called after odRenderOperations() has built and populated the DOM.
|
|
243
|
+
*/
|
|
244
|
+
function injectCompliancePanels(container) {
|
|
245
|
+
// Find the compliance tabpane
|
|
246
|
+
var pane = container && container.querySelector('[data-od-pane="compliance"]');
|
|
247
|
+
if (!pane) return;
|
|
248
|
+
|
|
249
|
+
// Remove any stale injected panels from a previous render
|
|
250
|
+
var oldErase = pane.querySelector('#' + ENTITY_ERASE_ID);
|
|
251
|
+
if (oldErase) oldErase.remove();
|
|
252
|
+
var oldReceipts = pane.querySelector('#' + RECEIPTS_ID);
|
|
253
|
+
if (oldReceipts) oldReceipts.remove();
|
|
254
|
+
|
|
255
|
+
// Append the two new cards at the bottom of the compliance tab
|
|
256
|
+
pane.appendChild(buildEntityEraseCard());
|
|
257
|
+
pane.appendChild(buildReceiptsCard());
|
|
258
|
+
|
|
259
|
+
// Wire interactions
|
|
260
|
+
wireEntityEraseCard();
|
|
261
|
+
wireReceiptsPanel();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── Patch odRenderOperations ─────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Wait for od-operations.js to define window.odRenderOperations, then wrap it
|
|
268
|
+
* so our panels are injected after every render. Uses polling with a cap —
|
|
269
|
+
* no unbounded loops.
|
|
270
|
+
*/
|
|
271
|
+
function patchOdRenderOperations() {
|
|
272
|
+
var base = window.odRenderOperations;
|
|
273
|
+
if (typeof base !== 'function') return false;
|
|
274
|
+
window.odRenderOperations = function (container) {
|
|
275
|
+
base(container);
|
|
276
|
+
// od-operations.js renders asynchronously (Promise.all inside).
|
|
277
|
+
// Give it time to populate the DOM before injecting our panels.
|
|
278
|
+
setTimeout(function () { injectCompliancePanels(container); }, 600);
|
|
279
|
+
};
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Retry for up to 3s in case od-operations.js hasn't defined the function yet.
|
|
284
|
+
var _attempts = 0;
|
|
285
|
+
function tryPatch() {
|
|
286
|
+
if (patchOdRenderOperations()) return;
|
|
287
|
+
_attempts += 1;
|
|
288
|
+
if (_attempts < 30) setTimeout(tryPatch, 100);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Also inject into the currently active operations pane on DOMContentLoaded
|
|
292
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
293
|
+
tryPatch();
|
|
294
|
+
// If the operations pane is already in the DOM and populated, inject now
|
|
295
|
+
var pane = document.getElementById('operations-pane');
|
|
296
|
+
if (pane && pane.querySelector('[data-od-pane="compliance"]')) {
|
|
297
|
+
injectCompliancePanels(pane);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
}());
|