superlocalmemory 3.7.7 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- 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 +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -0,0 +1,1250 @@
|
|
|
1
|
+
// od-operations.js — OD Operations pane renderer v2 (design-faithful)
|
|
2
|
+
// Renders into window.odRenderOperations(container).
|
|
3
|
+
//
|
|
4
|
+
// Endpoints (confirmed with curl against daemon :8765):
|
|
5
|
+
// GET /api/lifecycle/status → {available, active_profile, total_memories,
|
|
6
|
+
// states: {archive, cold, warm},
|
|
7
|
+
// recent_transitions: [],
|
|
8
|
+
// age_stats: {warm: {avg_days, min_days, max_days}, ...}}
|
|
9
|
+
// POST /api/lifecycle/compact → {status: "not_implemented"} — wired to button only
|
|
10
|
+
// GET /api/trust/stats → {avg_trust_score, total_signals, enforcement, by_signal_type}
|
|
11
|
+
// GET /api/compliance/status → {available, audit_events_count, recent_audit_events,
|
|
12
|
+
// retention_policies, abac_policies_count}
|
|
13
|
+
//
|
|
14
|
+
// NOTE on lifecycle states: API returns keys 'archive', 'cold', 'warm' (not 'active'/'tombstoned').
|
|
15
|
+
// Display maps: archive → Archived. Missing keys default to 0.
|
|
16
|
+
//
|
|
17
|
+
// CRIT fixes applied:
|
|
18
|
+
// (a) Lifecycle counts always from /api/lifecycle/status — no hardcoded state numbers.
|
|
19
|
+
// (b) Compact buttons wired ONLY to click via addEventListener — never auto-fired.
|
|
20
|
+
// (c) All API strings (policy names, actors, actions, targets) pass through esc() for XSS safety.
|
|
21
|
+
//
|
|
22
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
|
|
23
|
+
|
|
24
|
+
/* global window, document, fetch, Promise */
|
|
25
|
+
(function () {
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** HTML-escape a value before inserting via innerHTML. */
|
|
31
|
+
function esc(s) {
|
|
32
|
+
return String(s == null ? '' : s)
|
|
33
|
+
.replace(/&/g, '&')
|
|
34
|
+
.replace(/</g, '<')
|
|
35
|
+
.replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
.replace(/'/g, ''');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format a Unix-seconds float or ISO string as "Xm ago". */
|
|
41
|
+
function timeAgo(ts) {
|
|
42
|
+
if (!ts) return '—';
|
|
43
|
+
var ms = (typeof ts === 'number') ? ts * 1000 : new Date(ts).getTime();
|
|
44
|
+
var d = (Date.now() - ms) / 1000;
|
|
45
|
+
if (d < 0) d = 0;
|
|
46
|
+
if (d < 60) return Math.floor(d) + 's ago';
|
|
47
|
+
if (d < 3600) return Math.floor(d / 60) + 'm ago';
|
|
48
|
+
if (d < 86400) return Math.floor(d / 3600) + 'h ago';
|
|
49
|
+
return Math.floor(d / 86400) + 'd ago';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Format a number with locale separators. */
|
|
53
|
+
function fmtNum(n) { return Number(n || 0).toLocaleString(); }
|
|
54
|
+
|
|
55
|
+
/** Get an inline SVG icon from the shell registry; fail-safe if shell absent. */
|
|
56
|
+
function safeIcon(name) {
|
|
57
|
+
return (typeof window.slmIcon === 'function') ? window.slmIcon(name) : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── .ctl style injection ────────────────────────────────────────────────────
|
|
61
|
+
// Design defines .ctl via <style> block. Inject once so :last-child border rule works.
|
|
62
|
+
var _ctlStyleInjected = false;
|
|
63
|
+
function injectCtlStyle() {
|
|
64
|
+
if (_ctlStyleInjected || document.getElementById('od-ops-ctl-style')) return;
|
|
65
|
+
var s = document.createElement('style');
|
|
66
|
+
s.id = 'od-ops-ctl-style';
|
|
67
|
+
s.textContent =
|
|
68
|
+
'.od-ops-ctl{display:flex;align-items:center;justify-content:space-between;' +
|
|
69
|
+
'gap:16px;padding:14px 0;border-bottom:1px solid var(--border)}' +
|
|
70
|
+
'.od-ops-ctl:last-child{border-bottom:0}';
|
|
71
|
+
document.head.appendChild(s);
|
|
72
|
+
_ctlStyleInjected = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Skeleton HTML ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Full static HTML for the operations pane — zero API data inside.
|
|
79
|
+
* IDs use od-* prefix to avoid colliding with legacy Bootstrap IDs.
|
|
80
|
+
* Tab switching uses data-od-tab / data-od-pane to isolate from shell nav.
|
|
81
|
+
*/
|
|
82
|
+
function buildSkeleton() {
|
|
83
|
+
return (
|
|
84
|
+
// Page header
|
|
85
|
+
'<div class="page-head">' +
|
|
86
|
+
'<h2>Governance</h2>' +
|
|
87
|
+
'<p>Who can access this workspace, how memory ages, trust, and ' +
|
|
88
|
+
'compliance — with the controls to act on them. Nothing here is ' +
|
|
89
|
+
'read-only.</p>' +
|
|
90
|
+
'</div>' +
|
|
91
|
+
|
|
92
|
+
// Tab bar
|
|
93
|
+
'<div class="tabs" id="od-ops-tabs">' +
|
|
94
|
+
'<button class="tab active" data-od-tab="lifecycle">Lifecycle</button>' +
|
|
95
|
+
'<button class="tab" data-od-tab="access">Access & Users</button>' +
|
|
96
|
+
'<button class="tab" data-od-tab="trust">Trust</button>' +
|
|
97
|
+
'<button class="tab" data-od-tab="compliance">Compliance</button>' +
|
|
98
|
+
'<button class="tab" data-od-tab="loops">Bounded Loops</button>' +
|
|
99
|
+
'</div>' +
|
|
100
|
+
|
|
101
|
+
// ══════ LIFECYCLE TAB ══════════════════════════════════════════════
|
|
102
|
+
'<section class="tabpane active" data-od-pane="lifecycle">' +
|
|
103
|
+
|
|
104
|
+
// State KPI strip: 5 lifecycle buckets
|
|
105
|
+
'<div class="kpi-strip" ' +
|
|
106
|
+
'style="margin-bottom:16px;grid-template-columns:repeat(5,1fr)" ' +
|
|
107
|
+
'id="od-lc-states">' +
|
|
108
|
+
'<div class="dim" style="padding:12px">Loading…</div>' +
|
|
109
|
+
'</div>' +
|
|
110
|
+
|
|
111
|
+
// Two-column: age table + compaction preview
|
|
112
|
+
'<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start">' +
|
|
113
|
+
|
|
114
|
+
'<div class="card">' +
|
|
115
|
+
'<div class="card-head"><h3>Average memory age by state</h3></div>' +
|
|
116
|
+
'<div class="card-pad">' +
|
|
117
|
+
'<table class="tbl" id="od-lc-ages">' +
|
|
118
|
+
'<thead><tr>' +
|
|
119
|
+
'<th>State</th>' +
|
|
120
|
+
'<th>Avg age (d)</th>' +
|
|
121
|
+
'<th>Newest (d)</th>' +
|
|
122
|
+
'<th>Oldest (d)</th>' +
|
|
123
|
+
'</tr></thead>' +
|
|
124
|
+
'<tbody>' +
|
|
125
|
+
'<tr><td colspan="4" class="dim" style="text-align:center;padding:20px">Loading…</td></tr>' +
|
|
126
|
+
'</tbody>' +
|
|
127
|
+
'</table>' +
|
|
128
|
+
'</div>' +
|
|
129
|
+
'</div>' +
|
|
130
|
+
|
|
131
|
+
'<div class="card">' +
|
|
132
|
+
'<div class="card-head">' +
|
|
133
|
+
'<h3>Compaction preview</h3>' +
|
|
134
|
+
'<span class="sub">dry-run · no changes applied</span>' +
|
|
135
|
+
'</div>' +
|
|
136
|
+
'<div class="card-pad">' +
|
|
137
|
+
'<p class="muted" style="font-size:13px" id="od-compact-summary">' +
|
|
138
|
+
'Click "Re-run dry-run" to preview which memories would be transitioned.' +
|
|
139
|
+
'</p>' +
|
|
140
|
+
'<div id="od-compact-detail" style="margin:14px 0"></div>' +
|
|
141
|
+
'<div style="display:flex;gap:8px">' +
|
|
142
|
+
// CRIT(b): wired via addEventListener in wireCompactBtns(), never auto-fired
|
|
143
|
+
'<button class="btn sm" id="od-compact-preview-btn">Re-run dry-run</button>' +
|
|
144
|
+
'<button class="btn sm primary" id="od-compact-apply-btn">Apply compaction</button>' +
|
|
145
|
+
'</div>' +
|
|
146
|
+
'</div>' +
|
|
147
|
+
'</div>' +
|
|
148
|
+
|
|
149
|
+
'</div>' + // end two-column grid
|
|
150
|
+
|
|
151
|
+
// Recent transitions table
|
|
152
|
+
'<div class="card" style="margin-top:16px">' +
|
|
153
|
+
'<div class="card-head"><h3>Recent transitions</h3></div>' +
|
|
154
|
+
'<div class="card-pad">' +
|
|
155
|
+
'<table class="tbl" id="od-lc-trans">' +
|
|
156
|
+
'<thead><tr>' +
|
|
157
|
+
'<th>Memory</th><th>Transition</th><th>Reason</th><th>When</th>' +
|
|
158
|
+
'</tr></thead>' +
|
|
159
|
+
'<tbody>' +
|
|
160
|
+
'<tr><td colspan="4" class="dim" style="text-align:center;padding:20px">Loading…</td></tr>' +
|
|
161
|
+
'</tbody>' +
|
|
162
|
+
'</table>' +
|
|
163
|
+
'</div>' +
|
|
164
|
+
'</div>' +
|
|
165
|
+
|
|
166
|
+
// System control — restart the daemon without a terminal
|
|
167
|
+
'<div class="card" style="margin-top:16px;max-width:640px">' +
|
|
168
|
+
'<div class="card-head"><h3>System control</h3></div>' +
|
|
169
|
+
'<div class="card-pad">' +
|
|
170
|
+
'<p class="muted" style="font-size:13px" id="od-restart-summary">' +
|
|
171
|
+
'Restart the memory daemon — applies pending config changes and ' +
|
|
172
|
+
'clears transient state. Memories are never affected.</p>' +
|
|
173
|
+
'<button class="btn sm" id="od-restart-daemon-btn" style="margin-top:10px">' +
|
|
174
|
+
'Restart daemon</button>' +
|
|
175
|
+
'</div>' +
|
|
176
|
+
'</div>' +
|
|
177
|
+
|
|
178
|
+
'</section>' +
|
|
179
|
+
|
|
180
|
+
// ══════ TRUST TAB ═════════════════════════════════════════════════
|
|
181
|
+
'<section class="tabpane" data-od-pane="trust">' +
|
|
182
|
+
|
|
183
|
+
'<div class="card" style="max-width:640px">' +
|
|
184
|
+
'<div class="card-head"><h3>Trust enforcement</h3></div>' +
|
|
185
|
+
'<div class="card-pad" id="od-trust-ctl">' +
|
|
186
|
+
'<div class="dim" style="padding:20px;text-align:center">Loading trust data…</div>' +
|
|
187
|
+
'</div>' +
|
|
188
|
+
'</div>' +
|
|
189
|
+
|
|
190
|
+
'</section>' +
|
|
191
|
+
|
|
192
|
+
// ══════ ACCESS & USERS TAB ════════════════════════════════════════
|
|
193
|
+
// First-class governance surface for non-technical admins: who can use
|
|
194
|
+
// this workspace and at what role. The card is populated by od-team.js
|
|
195
|
+
// (window.odRenderTeam) into #od-team-mount during odRenderOperations().
|
|
196
|
+
'<section class="tabpane" data-od-pane="access">' +
|
|
197
|
+
'<div class="card" id="od-team-mount">' +
|
|
198
|
+
'<div class="card-head"><h3>Team & access</h3></div>' +
|
|
199
|
+
'<div class="card-pad"><p class="muted">Loading…</p></div>' +
|
|
200
|
+
'</div>' +
|
|
201
|
+
'</section>' +
|
|
202
|
+
|
|
203
|
+
// ══════ BOUNDED LOOPS TAB ════════════════════════════════════════
|
|
204
|
+
// Static info card — no backend calls.
|
|
205
|
+
// A bounded loop advances only when an INDEPENDENT gate passes, not
|
|
206
|
+
// when the agent claims success. Laps are persisted as SLM memory
|
|
207
|
+
// (tag loop:<name>) so the full run history is queryable.
|
|
208
|
+
'<section class="tabpane" data-od-pane="loops">' +
|
|
209
|
+
|
|
210
|
+
'<div class="page-head" style="margin-bottom:16px">' +
|
|
211
|
+
'<h2 style="font-size:20px;margin-bottom:6px">Bounded Loops</h2>' +
|
|
212
|
+
'<p style="font-size:13.5px">An iteration control pattern for agentic ' +
|
|
213
|
+
'frameworks — the loop advances only when an <strong>independent ' +
|
|
214
|
+
'gate</strong> passes, not when the agent claims success. Prevents ' +
|
|
215
|
+
'rationalisation: the model cannot self-certify completion.</p>' +
|
|
216
|
+
'</div>' +
|
|
217
|
+
|
|
218
|
+
// Two-column: concept + CLI reference
|
|
219
|
+
'<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start;margin-bottom:16px">' +
|
|
220
|
+
|
|
221
|
+
'<div class="card">' +
|
|
222
|
+
'<div class="card-head"><h3>How it works</h3></div>' +
|
|
223
|
+
'<div class="card-pad">' +
|
|
224
|
+
'<p style="font-size:13px;line-height:1.6;margin-bottom:12px">' +
|
|
225
|
+
'Standard agentic loops let the model declare itself done — ' +
|
|
226
|
+
'a known failure mode when the model rationalises instead of verifying. ' +
|
|
227
|
+
'A bounded loop separates <em>execution</em> (the agent) from ' +
|
|
228
|
+
'<em>verification</em> (an independent gate such as a test suite, ' +
|
|
229
|
+
'linter, or judge LLM).' +
|
|
230
|
+
'</p>' +
|
|
231
|
+
'<p style="font-size:13px;line-height:1.6;margin-bottom:12px">' +
|
|
232
|
+
'The loop terminates only when the gate returns <code>DONE</code>, ' +
|
|
233
|
+
'or when a hard lap cap is reached (<code>HALT</code>). Each lap is ' +
|
|
234
|
+
'persisted as a queryable SLM memory tagged ' +
|
|
235
|
+
'<code>loop:<name></code>.' +
|
|
236
|
+
'</p>' +
|
|
237
|
+
'<div style="background:var(--card-2);border-radius:var(--r-md);' +
|
|
238
|
+
'padding:10px 14px;font-size:12.5px;line-height:1.7">' +
|
|
239
|
+
'<div><span class="badge ok" style="margin-right:8px">DONE</span>' +
|
|
240
|
+
'Gate passed — loop succeeded cleanly</div>' +
|
|
241
|
+
'<div style="margin-top:6px"><span class="badge warn" style="margin-right:8px">HALT</span>' +
|
|
242
|
+
'Lap cap reached — hard stop applied</div>' +
|
|
243
|
+
'<div style="margin-top:6px"><span class="badge cyan" style="margin-right:8px">PAUSE</span>' +
|
|
244
|
+
'Awaiting external input or approval</div>' +
|
|
245
|
+
'<div style="margin-top:6px"><span class="badge danger" style="margin-right:8px">KILLED</span>' +
|
|
246
|
+
'Manually stopped by the operator</div>' +
|
|
247
|
+
'<div style="margin-top:6px"><span class="badge neutral" style="margin-right:8px">ERROR</span>' +
|
|
248
|
+
'Unrecoverable failure during a lap</div>' +
|
|
249
|
+
'</div>' +
|
|
250
|
+
'</div>' +
|
|
251
|
+
'</div>' +
|
|
252
|
+
|
|
253
|
+
'<div class="card">' +
|
|
254
|
+
'<div class="card-head"><h3>Run it: CLI · command · MCP</h3></div>' +
|
|
255
|
+
'<div class="card-pad">' +
|
|
256
|
+
'<p style="font-size:13px;margin-bottom:14px">' +
|
|
257
|
+
'Bounded loops ship on three surfaces — the same engine and ' +
|
|
258
|
+
'the same queryable ledger behind each.' +
|
|
259
|
+
'</p>' +
|
|
260
|
+
|
|
261
|
+
'<div style="font-size:12px;font-weight:600;color:var(--fg-2);margin-bottom:6px">CLI</div>' +
|
|
262
|
+
'<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px">' +
|
|
263
|
+
'<div class="list-row">' +
|
|
264
|
+
'<span class="mono" style="min-width:190px;font-size:13px">slm loop demo</span>' +
|
|
265
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">Run a live demo bounded loop</span>' +
|
|
266
|
+
'</div>' +
|
|
267
|
+
'<div class="list-row">' +
|
|
268
|
+
'<span class="mono" style="min-width:190px;font-size:13px">slm loop history</span>' +
|
|
269
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">List loop runs for this profile</span>' +
|
|
270
|
+
'</div>' +
|
|
271
|
+
'<div class="list-row">' +
|
|
272
|
+
'<span class="mono" style="min-width:190px;font-size:13px">slm loop show <run_id></span>' +
|
|
273
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">Inspect a run lap-by-lap</span>' +
|
|
274
|
+
'</div>' +
|
|
275
|
+
'</div>' +
|
|
276
|
+
|
|
277
|
+
'<div style="font-size:12px;font-weight:600;color:var(--fg-2);margin-bottom:6px">Command (Claude Code / Codex)</div>' +
|
|
278
|
+
'<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px">' +
|
|
279
|
+
'<div class="list-row">' +
|
|
280
|
+
'<span class="mono" style="min-width:190px;font-size:13px">/slm-loop</span>' +
|
|
281
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">Slash command bound to the slm-loop skill + runner agent</span>' +
|
|
282
|
+
'</div>' +
|
|
283
|
+
'</div>' +
|
|
284
|
+
|
|
285
|
+
'<div style="font-size:12px;font-weight:600;color:var(--fg-2);margin-bottom:6px">MCP tools (code / full / power profiles)</div>' +
|
|
286
|
+
'<div style="display:flex;flex-direction:column;gap:8px">' +
|
|
287
|
+
'<div class="list-row">' +
|
|
288
|
+
'<span class="mono" style="min-width:190px;font-size:13px">slm_loop_run</span>' +
|
|
289
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">Run a gated loop — the gate is an independent SLM recall</span>' +
|
|
290
|
+
'</div>' +
|
|
291
|
+
'<div class="list-row">' +
|
|
292
|
+
'<span class="mono" style="min-width:190px;font-size:13px">slm_loop_history</span>' +
|
|
293
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">List runs (read-only)</span>' +
|
|
294
|
+
'</div>' +
|
|
295
|
+
'<div class="list-row">' +
|
|
296
|
+
'<span class="mono" style="min-width:190px;font-size:13px">slm_loop_show</span>' +
|
|
297
|
+
'<span style="font-size:12.5px;color:var(--fg-2)">Show a run lap-by-lap (read-only)</span>' +
|
|
298
|
+
'</div>' +
|
|
299
|
+
'</div>' +
|
|
300
|
+
'<div style="margin-top:18px;padding:10px 14px;background:var(--card-2);' +
|
|
301
|
+
'border-radius:var(--r-md);font-size:12.5px;line-height:1.6">' +
|
|
302
|
+
'<b>Memory tagging:</b> each lap is stored with tag ' +
|
|
303
|
+
'<code>loop:<name></code>. Recall the full history with ' +
|
|
304
|
+
'<br><code>slm recall --tag loop:my-loop-name</code>' +
|
|
305
|
+
'</div>' +
|
|
306
|
+
'</div>' +
|
|
307
|
+
'</div>' +
|
|
308
|
+
|
|
309
|
+
'</div>' + // end two-column grid
|
|
310
|
+
|
|
311
|
+
// Framework adapters note
|
|
312
|
+
'<div class="card">' +
|
|
313
|
+
'<div class="card-head"><h3>Framework adapters</h3></div>' +
|
|
314
|
+
'<div class="card-pad">' +
|
|
315
|
+
'<p style="font-size:13px;line-height:1.6;margin-bottom:14px">' +
|
|
316
|
+
'Bounded loops integrate with any agentic framework that supports ' +
|
|
317
|
+
'tool-call round-trips. The gate is an ordinary SLM memory check — ' +
|
|
318
|
+
'no special framework wiring required. Each framework stamps ' +
|
|
319
|
+
'<code>SLM_AGENT_ID</code> so lap history is attribution-aware.' +
|
|
320
|
+
'</p>' +
|
|
321
|
+
'<div style="display:flex;flex-wrap:wrap;gap:8px">' +
|
|
322
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
323
|
+
'background:var(--card-2);color:var(--fg-2)">CrewAI</span>' +
|
|
324
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
325
|
+
'background:var(--card-2);color:var(--fg-2)">LangChain</span>' +
|
|
326
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
327
|
+
'background:var(--card-2);color:var(--fg-2)">LangGraph</span>' +
|
|
328
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
329
|
+
'background:var(--card-2);color:var(--fg-2)">Semantic Kernel</span>' +
|
|
330
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
331
|
+
'background:var(--card-2);color:var(--fg-2)">LlamaIndex</span>' +
|
|
332
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
333
|
+
'background:var(--card-2);color:var(--fg-2)">Microsoft Agent Framework</span>' +
|
|
334
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
335
|
+
'background:var(--card-2);color:var(--fg-2)">AutoGen</span>' +
|
|
336
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
337
|
+
'background:var(--card-2);color:var(--fg-2)">Google ADK</span>' +
|
|
338
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
339
|
+
'background:var(--card-2);color:var(--fg-2)">OpenAI Agents</span>' +
|
|
340
|
+
'<span style="padding:4px 12px;border-radius:99px;font-size:12px;' +
|
|
341
|
+
'background:var(--card-2);color:var(--fg-2)">Any MCP-compatible agent</span>' +
|
|
342
|
+
'</div>' +
|
|
343
|
+
'<p style="margin-top:12px;font-size:12.5px;color:var(--fg-2)">' +
|
|
344
|
+
'Learn the full pattern: run <code>/slm-loop</code> (the slm-loop skill) ' +
|
|
345
|
+
'inside Claude Code for an interactive walkthrough with live examples.' +
|
|
346
|
+
'</p>' +
|
|
347
|
+
'</div>' +
|
|
348
|
+
'</div>' +
|
|
349
|
+
|
|
350
|
+
'</section>' +
|
|
351
|
+
|
|
352
|
+
// ══════ COMPLIANCE TAB ════════════════════════════════════════════
|
|
353
|
+
'<section class="tabpane" data-od-pane="compliance">' +
|
|
354
|
+
|
|
355
|
+
// KPI strip: 3 cards with icons
|
|
356
|
+
'<div class="kpi-strip" ' +
|
|
357
|
+
'style="margin-bottom:16px;grid-template-columns:repeat(3,1fr)" ' +
|
|
358
|
+
'id="od-comp-kpi">' +
|
|
359
|
+
'<div class="dim" style="padding:12px">Loading…</div>' +
|
|
360
|
+
'</div>' +
|
|
361
|
+
|
|
362
|
+
// Retention policies table with New policy button
|
|
363
|
+
'<div class="card">' +
|
|
364
|
+
'<div class="card-head">' +
|
|
365
|
+
'<h3>Active retention policies</h3>' +
|
|
366
|
+
'<div class="spacer"></div>' +
|
|
367
|
+
// New policy button — shows alert since write endpoint is not yet exposed
|
|
368
|
+
'<button class="btn sm" id="od-new-policy-btn">' +
|
|
369
|
+
safeIcon('plus') + ' New policy' +
|
|
370
|
+
'</button>' +
|
|
371
|
+
'</div>' +
|
|
372
|
+
'<div class="card-pad">' +
|
|
373
|
+
'<table class="tbl" id="od-ret">' +
|
|
374
|
+
'<thead><tr>' +
|
|
375
|
+
'<th>Policy</th>' +
|
|
376
|
+
'<th>Retention (days)</th>' +
|
|
377
|
+
'<th>Category</th>' +
|
|
378
|
+
'<th>Action</th>' +
|
|
379
|
+
'<th></th>' +
|
|
380
|
+
'</tr></thead>' +
|
|
381
|
+
'<tbody>' +
|
|
382
|
+
'<tr><td colspan="5" class="dim" style="text-align:center;padding:20px">Loading…</td></tr>' +
|
|
383
|
+
'</tbody>' +
|
|
384
|
+
'</table>' +
|
|
385
|
+
'</div>' +
|
|
386
|
+
'</div>' +
|
|
387
|
+
|
|
388
|
+
// Data privacy & GDPR — export, run retention, erase (dashboard-operable)
|
|
389
|
+
'<div class="card" style="margin-top:16px">' +
|
|
390
|
+
'<div class="card-head"><h3>Data privacy & GDPR</h3></div>' +
|
|
391
|
+
'<div class="card-pad">' +
|
|
392
|
+
'<p class="muted" style="font-size:13px" id="od-gdpr-summary">' +
|
|
393
|
+
'Manage this profile\'s data under GDPR — export a full copy, run ' +
|
|
394
|
+
'retention now, or erase everything for this profile.</p>' +
|
|
395
|
+
'<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">' +
|
|
396
|
+
'<button class="btn sm" id="od-gdpr-export-btn">' + safeIcon('download') + ' Export my data</button>' +
|
|
397
|
+
'<button class="btn sm" id="od-retention-run-btn">Run retention now</button>' +
|
|
398
|
+
'<button class="btn sm" id="od-gdpr-erase-btn" style="border-color:#ff6b6b;color:#ff6b6b">Erase this profile…</button>' +
|
|
399
|
+
'</div>' +
|
|
400
|
+
'</div>' +
|
|
401
|
+
'</div>' +
|
|
402
|
+
|
|
403
|
+
// Audit trail with All/Denied segment filter
|
|
404
|
+
'<div class="card" style="margin-top:16px">' +
|
|
405
|
+
'<div class="card-head">' +
|
|
406
|
+
'<h3>Audit trail</h3>' +
|
|
407
|
+
'<div class="spacer"></div>' +
|
|
408
|
+
'<div class="seg" id="od-audit-filter">' +
|
|
409
|
+
'<button class="active" data-filter="all">All</button>' +
|
|
410
|
+
'<button data-filter="denied">Denied</button>' +
|
|
411
|
+
'</div>' +
|
|
412
|
+
'</div>' +
|
|
413
|
+
'<div class="card-pad">' +
|
|
414
|
+
'<table class="tbl" id="od-audit">' +
|
|
415
|
+
'<thead><tr>' +
|
|
416
|
+
'<th>Timestamp</th>' +
|
|
417
|
+
'<th>Event</th>' +
|
|
418
|
+
'<th>Actor</th>' +
|
|
419
|
+
'<th>Action</th>' +
|
|
420
|
+
'<th>Target</th>' +
|
|
421
|
+
'<th>Result</th>' +
|
|
422
|
+
'</tr></thead>' +
|
|
423
|
+
'<tbody id="od-audit-body">' +
|
|
424
|
+
'<tr><td colspan="6" class="dim" style="text-align:center;padding:20px">Loading…</td></tr>' +
|
|
425
|
+
'</tbody>' +
|
|
426
|
+
'</table>' +
|
|
427
|
+
'</div>' +
|
|
428
|
+
'</div>' +
|
|
429
|
+
|
|
430
|
+
'</section>'
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── Tab switching ────────────────────────────────────────────────────────
|
|
435
|
+
/**
|
|
436
|
+
* Wire the internal tab bar.
|
|
437
|
+
* Uses data-od-tab on buttons and data-od-pane on sections.
|
|
438
|
+
* No onclick attributes — CSP-safe addEventListener only.
|
|
439
|
+
*/
|
|
440
|
+
function initTabs(root) {
|
|
441
|
+
var buttons = root.querySelectorAll('[data-od-tab]');
|
|
442
|
+
var panes = root.querySelectorAll('[data-od-pane]');
|
|
443
|
+
|
|
444
|
+
buttons.forEach(function (btn) {
|
|
445
|
+
btn.addEventListener('click', function () {
|
|
446
|
+
var target = btn.getAttribute('data-od-tab');
|
|
447
|
+
buttons.forEach(function (b) { b.classList.remove('active'); });
|
|
448
|
+
panes.forEach(function (p) {
|
|
449
|
+
p.classList.toggle('active', p.getAttribute('data-od-pane') === target);
|
|
450
|
+
});
|
|
451
|
+
btn.classList.add('active');
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
457
|
+
/**
|
|
458
|
+
* Populate the lifecycle KPI strip.
|
|
459
|
+
* API returns states: {archive, cold, warm}. 'active' and 'tombstoned' not in API.
|
|
460
|
+
* Missing keys default to 0.
|
|
461
|
+
*/
|
|
462
|
+
function populateLifecycleStates(lc) {
|
|
463
|
+
var statesEl = document.getElementById('od-lc-states');
|
|
464
|
+
if (!statesEl) return;
|
|
465
|
+
|
|
466
|
+
var rawStates = (lc && lc.states) || {};
|
|
467
|
+
|
|
468
|
+
// Labels are lowercase to match the approved design (design uses 'active' not 'Active').
|
|
469
|
+
var STATE_MAP = [
|
|
470
|
+
{ key: 'active', label: 'active', cls: 'ok' },
|
|
471
|
+
{ key: 'warm', label: 'warm', cls: 'warn' },
|
|
472
|
+
{ key: 'cold', label: 'cold', cls: 'cyan' },
|
|
473
|
+
{ key: 'archive', label: 'archived', cls: 'neutral' },
|
|
474
|
+
{ key: 'tombstoned', label: 'tombstoned', cls: 'danger' },
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
statesEl.innerHTML = STATE_MAP.map(function (s) {
|
|
478
|
+
var count = rawStates[s.key] != null ? rawStates[s.key] : 0;
|
|
479
|
+
return (
|
|
480
|
+
'<div class="card kpi">' +
|
|
481
|
+
'<div class="label">' +
|
|
482
|
+
'<span class="badge ' + s.cls + '" style="padding:1px 8px">' +
|
|
483
|
+
'<span class="dot"></span>' + esc(s.label) +
|
|
484
|
+
'</span>' +
|
|
485
|
+
'</div>' +
|
|
486
|
+
'<div class="value num" style="font-size:24px">' + fmtNum(count) + '</div>' +
|
|
487
|
+
'</div>'
|
|
488
|
+
);
|
|
489
|
+
}).join('');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Populate the age-by-state table from lifecycle age_stats.
|
|
494
|
+
* Maps API key 'archive' to display label 'Archived'.
|
|
495
|
+
*/
|
|
496
|
+
function populateLifecycleAges(lc) {
|
|
497
|
+
var ageTbody = document.querySelector('#od-lc-ages tbody');
|
|
498
|
+
if (!ageTbody) return;
|
|
499
|
+
|
|
500
|
+
var ageStats = (lc && lc.age_stats) || {};
|
|
501
|
+
var ageKeys = Object.keys(ageStats);
|
|
502
|
+
|
|
503
|
+
if (ageKeys.length === 0) {
|
|
504
|
+
ageTbody.innerHTML =
|
|
505
|
+
'<tr><td colspan="4" class="dim" style="text-align:center;padding:16px">' +
|
|
506
|
+
'No age data available yet.</td></tr>';
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
ageTbody.innerHTML = ageKeys.map(function (k) {
|
|
511
|
+
var ag = ageStats[k] || {};
|
|
512
|
+
// Lowercase to match design badge style (design shows 'archived' not 'Archived')
|
|
513
|
+
var displayLabel = (k === 'archive') ? 'archived' : esc(k);
|
|
514
|
+
return (
|
|
515
|
+
'<tr>' +
|
|
516
|
+
'<td><span class="badge neutral">' + displayLabel + '</span></td>' +
|
|
517
|
+
'<td class="num">' + esc(String(Number(ag.avg_days || 0).toFixed(1))) + '</td>' +
|
|
518
|
+
'<td class="num dim">' + esc(String(Number(ag.min_days || 0).toFixed(1))) + '</td>' +
|
|
519
|
+
'<td class="num dim">' + esc(String(Number(ag.max_days || 0).toFixed(1))) + '</td>' +
|
|
520
|
+
'</tr>'
|
|
521
|
+
);
|
|
522
|
+
}).join('');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Populate the recent transitions table.
|
|
527
|
+
* API returns recent_transitions: [] in current live daemon.
|
|
528
|
+
* Shows honest empty state; displays up to 20 rows when present.
|
|
529
|
+
*/
|
|
530
|
+
function populateLifecycleTransitions(lc) {
|
|
531
|
+
var transTbody = document.querySelector('#od-lc-trans tbody');
|
|
532
|
+
if (!transTbody) return;
|
|
533
|
+
|
|
534
|
+
var trans = (lc && lc.recent_transitions) || [];
|
|
535
|
+
|
|
536
|
+
if (trans.length === 0) {
|
|
537
|
+
transTbody.innerHTML =
|
|
538
|
+
'<tr><td colspan="4" class="dim" style="text-align:center;padding:16px">' +
|
|
539
|
+
'No recent transitions. Transitions appear here when memories change lifecycle state.' +
|
|
540
|
+
'</td></tr>';
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
transTbody.innerHTML = trans.slice(0, 20).map(function (t) {
|
|
545
|
+
var idLabel = t.memory_id ? '#' + esc(String(t.memory_id)) : '—';
|
|
546
|
+
var transition = t.transition ? esc(String(t.transition)) : '—';
|
|
547
|
+
var reason = t.reason ? esc(String(t.reason)) : '—';
|
|
548
|
+
var when = t.timestamp ? esc(timeAgo(t.timestamp)) : '—';
|
|
549
|
+
return (
|
|
550
|
+
'<tr>' +
|
|
551
|
+
'<td class="mono dim">' + idLabel + '</td>' +
|
|
552
|
+
'<td><span class="badge neutral">' + transition + '</span></td>' +
|
|
553
|
+
'<td class="dim">' + reason + '</td>' +
|
|
554
|
+
'<td class="dim">' + when + '</td>' +
|
|
555
|
+
'</tr>'
|
|
556
|
+
);
|
|
557
|
+
}).join('');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Populate all lifecycle sections.
|
|
562
|
+
* Calls populateLifecycleStates, populateLifecycleAges, populateLifecycleTransitions.
|
|
563
|
+
*/
|
|
564
|
+
function populateLifecycle(lc) {
|
|
565
|
+
if (!lc) {
|
|
566
|
+
var stEl = document.getElementById('od-lc-states');
|
|
567
|
+
if (stEl) stEl.innerHTML = '<p class="muted">Lifecycle data unavailable.</p>';
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
populateLifecycleStates(lc);
|
|
571
|
+
populateLifecycleAges(lc);
|
|
572
|
+
populateLifecycleTransitions(lc);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ─── Compact buttons ──────────────────────────────────────────────────────
|
|
576
|
+
/**
|
|
577
|
+
* Wire the compaction buttons via addEventListener.
|
|
578
|
+
* CRIT(b): preview-btn and apply-btn only fire on explicit user click.
|
|
579
|
+
* Current API returns {status:"not_implemented"} for POST /api/lifecycle/compact.
|
|
580
|
+
*/
|
|
581
|
+
function wireCompactBtns() {
|
|
582
|
+
var previewBtn = document.getElementById('od-compact-preview-btn');
|
|
583
|
+
var applyBtn = document.getElementById('od-compact-apply-btn');
|
|
584
|
+
var summaryEl = document.getElementById('od-compact-summary');
|
|
585
|
+
var detailEl = document.getElementById('od-compact-detail');
|
|
586
|
+
|
|
587
|
+
if (previewBtn) {
|
|
588
|
+
previewBtn.addEventListener('click', function () {
|
|
589
|
+
previewBtn.disabled = true;
|
|
590
|
+
previewBtn.textContent = 'Running…';
|
|
591
|
+
|
|
592
|
+
fetch('/api/lifecycle/compact', {
|
|
593
|
+
method: 'POST',
|
|
594
|
+
headers: { 'Content-Type': 'application/json' },
|
|
595
|
+
body: JSON.stringify({ dry_run: true }),
|
|
596
|
+
})
|
|
597
|
+
.then(function (r) { return r.json(); })
|
|
598
|
+
.then(function (data) {
|
|
599
|
+
if (data && data.success === false) {
|
|
600
|
+
if (summaryEl) summaryEl.innerHTML =
|
|
601
|
+
'<span class="dim">' + esc(data.error || 'Compaction unavailable.') + '</span>';
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
var recs = Number(data.candidates || 0);
|
|
605
|
+
if (summaryEl) {
|
|
606
|
+
summaryEl.innerHTML =
|
|
607
|
+
'Compaction preview: <b style="color:var(--fg)">' + fmtNum(recs) +
|
|
608
|
+
'</b> of ' + fmtNum(data.total_facts || 0) +
|
|
609
|
+
' memories would be transitioned.';
|
|
610
|
+
}
|
|
611
|
+
if (detailEl) {
|
|
612
|
+
var details = (data.transitions || []).slice(0, 10);
|
|
613
|
+
if (details.length === 0) {
|
|
614
|
+
detailEl.innerHTML =
|
|
615
|
+
'<p class="muted" style="font-size:13px">' +
|
|
616
|
+
'No compaction needed — all memories in optimal states.</p>';
|
|
617
|
+
} else {
|
|
618
|
+
detailEl.innerHTML = details.map(function (d) {
|
|
619
|
+
return (
|
|
620
|
+
'<div style="display:flex;justify-content:space-between;' +
|
|
621
|
+
'align-items:center;padding:8px 0;' +
|
|
622
|
+
'border-bottom:1px solid var(--border)">' +
|
|
623
|
+
'<span class="mono" style="font-size:12.5px">' +
|
|
624
|
+
esc(String(d.fact_id || '')) +
|
|
625
|
+
'</span>' +
|
|
626
|
+
'<span class="badge neutral">' +
|
|
627
|
+
esc(d.current || '') + ' → ' + esc(d.proposed || '') +
|
|
628
|
+
'</span>' +
|
|
629
|
+
'</div>'
|
|
630
|
+
);
|
|
631
|
+
}).join('');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
.catch(function (err) {
|
|
636
|
+
if (summaryEl) summaryEl.textContent = 'Preview failed. Check console.';
|
|
637
|
+
if (typeof console !== 'undefined') {
|
|
638
|
+
console.error('[od-ops] compact preview error:', err);
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
.finally(function () {
|
|
642
|
+
previewBtn.disabled = false;
|
|
643
|
+
previewBtn.textContent = 'Re-run dry-run';
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (applyBtn) {
|
|
649
|
+
applyBtn.addEventListener('click', function () {
|
|
650
|
+
if (!window.confirm('This will transition memories to lower lifecycle states. Continue?')) return;
|
|
651
|
+
applyBtn.disabled = true;
|
|
652
|
+
applyBtn.textContent = 'Running…';
|
|
653
|
+
|
|
654
|
+
fetch('/api/lifecycle/compact', {
|
|
655
|
+
method: 'POST',
|
|
656
|
+
headers: { 'Content-Type': 'application/json' },
|
|
657
|
+
body: JSON.stringify({ dry_run: false }),
|
|
658
|
+
})
|
|
659
|
+
.then(function (r) { return r.json(); })
|
|
660
|
+
.then(function (data) {
|
|
661
|
+
if (data && data.success === false) {
|
|
662
|
+
if (summaryEl) summaryEl.innerHTML =
|
|
663
|
+
'<span class="dim">' + esc(data.error || 'Compaction failed.') + '</span>';
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
var n = Number(data.applied || 0);
|
|
667
|
+
if (summaryEl) {
|
|
668
|
+
summaryEl.innerHTML =
|
|
669
|
+
'Compaction complete: <b style="color:var(--ok)">' +
|
|
670
|
+
fmtNum(n) + '</b> memories transitioned.';
|
|
671
|
+
}
|
|
672
|
+
})
|
|
673
|
+
.catch(function (err) {
|
|
674
|
+
if (summaryEl) summaryEl.textContent = 'Compaction failed. Check console.';
|
|
675
|
+
if (typeof console !== 'undefined') {
|
|
676
|
+
console.error('[od-ops] compact apply error:', err);
|
|
677
|
+
}
|
|
678
|
+
})
|
|
679
|
+
.finally(function () {
|
|
680
|
+
applyBtn.disabled = false;
|
|
681
|
+
applyBtn.textContent = 'Apply compaction';
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Wire the "Restart daemon" button. POSTs /api/daemon/restart (which spawns a
|
|
689
|
+
* detached `slm restart`), then polls /health until the fresh daemon is back
|
|
690
|
+
* and reloads the dashboard. Wired via addEventListener — never auto-fired.
|
|
691
|
+
*/
|
|
692
|
+
function wireRestartBtn() {
|
|
693
|
+
var btn = document.getElementById('od-restart-daemon-btn');
|
|
694
|
+
var summary = document.getElementById('od-restart-summary');
|
|
695
|
+
if (!btn) return;
|
|
696
|
+
btn.addEventListener('click', function () {
|
|
697
|
+
if (!window.confirm(
|
|
698
|
+
'Restart the memory daemon now? It will be unavailable for a few ' +
|
|
699
|
+
'seconds. Your memories are not affected.')) return;
|
|
700
|
+
btn.disabled = true;
|
|
701
|
+
btn.textContent = 'Restarting…';
|
|
702
|
+
if (summary) summary.textContent = 'Restart requested — waiting for the daemon to come back…';
|
|
703
|
+
|
|
704
|
+
fetch('/api/daemon/restart', {
|
|
705
|
+
method: 'POST',
|
|
706
|
+
headers: { 'Content-Type': 'application/json' },
|
|
707
|
+
body: '{}',
|
|
708
|
+
})
|
|
709
|
+
.then(function (r) { return r.json().catch(function () { return {}; }); })
|
|
710
|
+
.then(function (data) {
|
|
711
|
+
if (data && data.success === false) {
|
|
712
|
+
if (summary) summary.textContent = 'Restart failed: ' + (data.error || 'unknown error');
|
|
713
|
+
btn.disabled = false; btn.textContent = 'Restart daemon';
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// Poll /health until the fresh daemon reports ready, then reload.
|
|
717
|
+
var tries = 0;
|
|
718
|
+
var poll = setInterval(function () {
|
|
719
|
+
tries += 1;
|
|
720
|
+
fetch('/health', { cache: 'no-store' })
|
|
721
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
722
|
+
.then(function (h) {
|
|
723
|
+
if (h && (h.ready === true || h.engine === 'initialized')) {
|
|
724
|
+
clearInterval(poll);
|
|
725
|
+
if (summary) summary.textContent = 'Daemon back online — reloading…';
|
|
726
|
+
setTimeout(function () { window.location.reload(); }, 600);
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
.catch(function () { /* daemon still down mid-restart — keep polling */ });
|
|
730
|
+
if (tries > 30) { // ~30s ceiling
|
|
731
|
+
clearInterval(poll);
|
|
732
|
+
if (summary) summary.textContent =
|
|
733
|
+
'Daemon is taking longer than expected. Refresh the page in a moment.';
|
|
734
|
+
btn.disabled = false; btn.textContent = 'Restart daemon';
|
|
735
|
+
}
|
|
736
|
+
}, 1000);
|
|
737
|
+
})
|
|
738
|
+
.catch(function () {
|
|
739
|
+
// The connection often drops as the daemon stops — that's expected;
|
|
740
|
+
// start polling for it to come back.
|
|
741
|
+
if (summary) summary.textContent = 'Daemon restarting — waiting for it to come back…';
|
|
742
|
+
var tries = 0;
|
|
743
|
+
var poll = setInterval(function () {
|
|
744
|
+
tries += 1;
|
|
745
|
+
fetch('/health', { cache: 'no-store' })
|
|
746
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
747
|
+
.then(function (h) {
|
|
748
|
+
if (h && (h.ready === true || h.engine === 'initialized')) {
|
|
749
|
+
clearInterval(poll);
|
|
750
|
+
window.location.reload();
|
|
751
|
+
}
|
|
752
|
+
})
|
|
753
|
+
.catch(function () {});
|
|
754
|
+
if (tries > 30) { clearInterval(poll); btn.disabled = false; btn.textContent = 'Restart daemon'; }
|
|
755
|
+
}, 1000);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ─── Trust ────────────────────────────────────────────────────────────────
|
|
761
|
+
/**
|
|
762
|
+
* Populate trust enforcement section from /api/trust/stats.
|
|
763
|
+
* Toggle controls are read-only display — no write endpoint exists yet.
|
|
764
|
+
* TODO: wire toggles when /api/trust/settings PATCH is exposed.
|
|
765
|
+
*/
|
|
766
|
+
function populateTrust(trustStats) {
|
|
767
|
+
var ctlEl = document.getElementById('od-trust-ctl');
|
|
768
|
+
if (!ctlEl) return;
|
|
769
|
+
|
|
770
|
+
if (!trustStats) {
|
|
771
|
+
ctlEl.innerHTML = '<p class="muted">Trust data unavailable.</p>';
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
var enforcement = String(trustStats.enforcement || 'unknown');
|
|
776
|
+
var isEnforced = enforcement.toLowerCase() !== 'silent collection' &&
|
|
777
|
+
enforcement.toLowerCase() !== 'off' &&
|
|
778
|
+
enforcement.toLowerCase() !== 'disabled';
|
|
779
|
+
|
|
780
|
+
// Helper: a control row using .od-ops-ctl class (injectCtlStyle injects .ctl rules).
|
|
781
|
+
// :last-child removes border-bottom on the final row — matches design's <style> block.
|
|
782
|
+
function ctlRow(label, sublabel, control) {
|
|
783
|
+
return (
|
|
784
|
+
'<div class="od-ops-ctl">' +
|
|
785
|
+
'<div>' +
|
|
786
|
+
'<b>' + esc(label) + '</b>' +
|
|
787
|
+
'<div class="dim" style="font-size:12.5px">' + esc(sublabel) + '</div>' +
|
|
788
|
+
'</div>' +
|
|
789
|
+
control +
|
|
790
|
+
'</div>'
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Switch is display-only (no write endpoint) — disabled + lower opacity.
|
|
795
|
+
// enforcement: if mode is "silent collection" / off / disabled → switch is OFF.
|
|
796
|
+
var enfSwitch = '<button class="switch' + (isEnforced ? ' on' : '') +
|
|
797
|
+
'" disabled aria-label="read-only" style="cursor:not-allowed;opacity:0.7"></button>';
|
|
798
|
+
|
|
799
|
+
// Design shows only 4 ctl rows — no stats summary block above them.
|
|
800
|
+
ctlEl.innerHTML = (
|
|
801
|
+
ctlRow(
|
|
802
|
+
'Enforcement mode',
|
|
803
|
+
'Block writes from agents below the trust floor',
|
|
804
|
+
enfSwitch
|
|
805
|
+
) +
|
|
806
|
+
// TODO: trust floor — no read or write endpoint in current API
|
|
807
|
+
ctlRow(
|
|
808
|
+
'Trust floor',
|
|
809
|
+
'Minimum trust score to write',
|
|
810
|
+
'<span class="mono">0.30</span>' // TODO: read from /api/trust/config when available
|
|
811
|
+
) +
|
|
812
|
+
// TODO: quick-delete penalty — no read/write endpoint yet
|
|
813
|
+
ctlRow(
|
|
814
|
+
'Quick-delete penalty',
|
|
815
|
+
'Lower trust when writes are deleted < 60s',
|
|
816
|
+
'<button class="switch on" disabled style="cursor:not-allowed;opacity:0.7"></button>'
|
|
817
|
+
) +
|
|
818
|
+
// TODO: recall reward — no read/write endpoint yet
|
|
819
|
+
ctlRow(
|
|
820
|
+
'Recall reward',
|
|
821
|
+
'Raise trust when a memory is recalled',
|
|
822
|
+
'<button class="switch on" disabled style="cursor:not-allowed;opacity:0.7"></button>'
|
|
823
|
+
)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// ─── Compliance ───────────────────────────────────────────────────────────
|
|
828
|
+
/**
|
|
829
|
+
* Populate the compliance KPI strip — 3 cards with icons.
|
|
830
|
+
* Icons via window.slmIcon() so they work in dynamically-injected content.
|
|
831
|
+
*/
|
|
832
|
+
function populateComplianceKpi(comp) {
|
|
833
|
+
var kpiEl = document.getElementById('od-comp-kpi');
|
|
834
|
+
if (!kpiEl) return;
|
|
835
|
+
|
|
836
|
+
var auditCount = comp ? Number(comp.audit_events_count || 0) : 0;
|
|
837
|
+
var retCount = comp && comp.retention_policies ? comp.retention_policies.length : 0;
|
|
838
|
+
var abacCount = comp ? Number(comp.abac_policies_count || 0) : 0;
|
|
839
|
+
|
|
840
|
+
kpiEl.innerHTML = (
|
|
841
|
+
'<div class="card kpi">' +
|
|
842
|
+
// Design uses data-ic="shield". Shield is not in the icon registry (od-shell.js).
|
|
843
|
+
// safeIcon('health') (pulse/waveform) is the closest semantic match for audit monitoring.
|
|
844
|
+
'<div class="label">' + safeIcon('health') + ' Audit events</div>' +
|
|
845
|
+
'<div class="value num">' + fmtNum(auditCount) + '</div>' +
|
|
846
|
+
'</div>' +
|
|
847
|
+
'<div class="card kpi">' +
|
|
848
|
+
'<div class="label">' + safeIcon('clock') + ' Retention policies</div>' +
|
|
849
|
+
'<div class="value num">' + fmtNum(retCount) + '</div>' +
|
|
850
|
+
'</div>' +
|
|
851
|
+
'<div class="card kpi">' +
|
|
852
|
+
'<div class="label">' + safeIcon('lock') + ' ABAC rules</div>' +
|
|
853
|
+
'<div class="value num">' + fmtNum(abacCount) + '</div>' +
|
|
854
|
+
'</div>'
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Populate the retention policies table.
|
|
859
|
+
* CRIT(c): policy name, category, action go through esc().
|
|
860
|
+
* Empty state explains how to add policies via MCP tool.
|
|
861
|
+
*/
|
|
862
|
+
function populateRetentionPolicies(comp) {
|
|
863
|
+
var retTbody = document.querySelector('#od-ret tbody');
|
|
864
|
+
if (!retTbody) return;
|
|
865
|
+
|
|
866
|
+
var policies = (comp && comp.retention_policies) || [];
|
|
867
|
+
|
|
868
|
+
if (policies.length === 0) {
|
|
869
|
+
retTbody.innerHTML =
|
|
870
|
+
'<tr><td colspan="5" class="dim" style="text-align:center;padding:20px">' +
|
|
871
|
+
'No retention policies configured. Use the set_retention_policy MCP tool to add one.' +
|
|
872
|
+
'</td></tr>';
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
var ACTION_CLS = { archive: 'neutral', tombstone: 'danger', notify: 'warn' };
|
|
877
|
+
|
|
878
|
+
retTbody.innerHTML = policies.map(function (p) {
|
|
879
|
+
var ac = ACTION_CLS[p.action] || 'neutral';
|
|
880
|
+
// CRIT(c): name, category, action through esc()
|
|
881
|
+
return (
|
|
882
|
+
'<tr>' +
|
|
883
|
+
'<td><b>' + esc(p.name || '') + '</b></td>' +
|
|
884
|
+
'<td class="num">' + esc(String(p.retention_days || '—')) + '</td>' +
|
|
885
|
+
'<td><span class="badge neutral">' + esc(p.category || 'all') + '</span></td>' +
|
|
886
|
+
'<td><span class="badge ' + ac + '">' + esc(p.action || '') + '</span></td>' +
|
|
887
|
+
'<td style="text-align:right">' +
|
|
888
|
+
'<button class="btn sm ghost" disabled>Edit</button>' +
|
|
889
|
+
// TODO: wire edit button to /api/compliance/retention-policy PATCH when available
|
|
890
|
+
'</td>' +
|
|
891
|
+
'</tr>'
|
|
892
|
+
);
|
|
893
|
+
}).join('');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Cache of all audit rows (for client-side All/Denied filter)
|
|
897
|
+
var _allAuditRows = [];
|
|
898
|
+
/**
|
|
899
|
+
* Populate the audit trail table.
|
|
900
|
+
* CRIT(c): actor, action, target, result all through esc().
|
|
901
|
+
*/
|
|
902
|
+
function populateAuditTrail(comp) {
|
|
903
|
+
var auditTbody = document.getElementById('od-audit-body');
|
|
904
|
+
if (!auditTbody) return;
|
|
905
|
+
|
|
906
|
+
var events = (comp && comp.recent_audit_events) || [];
|
|
907
|
+
_allAuditRows = events;
|
|
908
|
+
|
|
909
|
+
renderAuditRows(auditTbody, events);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** Render audit table rows into tbody, filtering by mode ('all' or 'denied'). */
|
|
913
|
+
function renderAuditRows(tbody, events) {
|
|
914
|
+
if (events.length === 0) {
|
|
915
|
+
tbody.innerHTML =
|
|
916
|
+
'<tr><td colspan="6" class="dim" style="text-align:center;padding:20px">' +
|
|
917
|
+
'No audit events recorded yet.</td></tr>';
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
var EV_CLS = {
|
|
922
|
+
recall: 'cyan',
|
|
923
|
+
remember: 'ok',
|
|
924
|
+
delete: 'danger',
|
|
925
|
+
lifecycle_transition: 'warn',
|
|
926
|
+
access_denied: 'danger',
|
|
927
|
+
retention_enforced: 'warn',
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
tbody.innerHTML = events.slice(0, 50).map(function (ev) {
|
|
931
|
+
var evc = EV_CLS[ev.event_type] || 'neutral';
|
|
932
|
+
var isOk = ev.result === 'success' || ev.result === 'allowed';
|
|
933
|
+
var isDenied = ev.result === 'denied' || ev.result === 'error';
|
|
934
|
+
var resultCls = isOk ? 'ok' : isDenied ? 'danger' : 'neutral';
|
|
935
|
+
// CRIT(c): all user-sourced strings through esc()
|
|
936
|
+
return (
|
|
937
|
+
'<tr>' +
|
|
938
|
+
'<td class="mono dim" style="font-size:12px">' + esc(ev.timestamp || '') + '</td>' +
|
|
939
|
+
'<td><span class="badge ' + evc + '">' + esc(ev.event_type || '') + '</span></td>' +
|
|
940
|
+
'<td class="mono" style="font-size:12px">' + esc(ev.actor || '') + '</td>' +
|
|
941
|
+
'<td>' + esc(ev.action || '') + '</td>' +
|
|
942
|
+
'<td class="dim">' + esc(ev.target || '') + '</td>' +
|
|
943
|
+
'<td><span class="badge ' + resultCls + '">' + esc(ev.result || '') + '</span></td>' +
|
|
944
|
+
'</tr>'
|
|
945
|
+
);
|
|
946
|
+
}).join('');
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Wire the All/Denied segment filter in the audit trail header.
|
|
950
|
+
* Client-side filter over already-fetched data — no extra network call.
|
|
951
|
+
*/
|
|
952
|
+
function wireAuditFilter() {
|
|
953
|
+
var filterEl = document.getElementById('od-audit-filter');
|
|
954
|
+
var tbody = document.getElementById('od-audit-body');
|
|
955
|
+
if (!filterEl || !tbody) return;
|
|
956
|
+
|
|
957
|
+
filterEl.querySelectorAll('button').forEach(function (btn) {
|
|
958
|
+
btn.addEventListener('click', function () {
|
|
959
|
+
var mode = btn.getAttribute('data-filter');
|
|
960
|
+
filterEl.querySelectorAll('button').forEach(function (b) {
|
|
961
|
+
b.classList.remove('active');
|
|
962
|
+
});
|
|
963
|
+
btn.classList.add('active');
|
|
964
|
+
|
|
965
|
+
var rows = (mode === 'denied')
|
|
966
|
+
? _allAuditRows.filter(function (ev) {
|
|
967
|
+
return ev.result === 'denied' || ev.result === 'error' ||
|
|
968
|
+
(ev.event_type && ev.event_type.indexOf('denied') >= 0);
|
|
969
|
+
})
|
|
970
|
+
: _allAuditRows;
|
|
971
|
+
|
|
972
|
+
renderAuditRows(tbody, rows);
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Wire the "New policy" button.
|
|
979
|
+
* Informs user that the write endpoint is not yet exposed.
|
|
980
|
+
*/
|
|
981
|
+
function wireNewPolicyBtn() {
|
|
982
|
+
var btn = document.getElementById('od-new-policy-btn');
|
|
983
|
+
if (!btn) return;
|
|
984
|
+
btn.addEventListener('click', openRetentionPolicyModal);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Wire the GDPR data-privacy controls: export (download), run retention now,
|
|
989
|
+
* and erase-this-profile (typed confirmation). All dashboard-operable.
|
|
990
|
+
*/
|
|
991
|
+
function wireGdprBtns() {
|
|
992
|
+
var summary = document.getElementById('od-gdpr-summary');
|
|
993
|
+
var exportBtn = document.getElementById('od-gdpr-export-btn');
|
|
994
|
+
var runBtn = document.getElementById('od-retention-run-btn');
|
|
995
|
+
var eraseBtn = document.getElementById('od-gdpr-erase-btn');
|
|
996
|
+
|
|
997
|
+
if (exportBtn) {
|
|
998
|
+
exportBtn.addEventListener('click', function () {
|
|
999
|
+
// Stream the JSON attachment to a download via a temporary anchor.
|
|
1000
|
+
var a = document.createElement('a');
|
|
1001
|
+
a.href = '/api/compliance/gdpr/export';
|
|
1002
|
+
a.download = '';
|
|
1003
|
+
document.body.appendChild(a);
|
|
1004
|
+
a.click();
|
|
1005
|
+
document.body.removeChild(a);
|
|
1006
|
+
if (summary) summary.textContent = 'Export started — check your downloads.';
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (runBtn) {
|
|
1011
|
+
runBtn.addEventListener('click', function () {
|
|
1012
|
+
runBtn.disabled = true; runBtn.textContent = 'Running…';
|
|
1013
|
+
fetch('/api/compliance/retention/enforce', {
|
|
1014
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
|
|
1015
|
+
})
|
|
1016
|
+
.then(function (r) { return r.json(); })
|
|
1017
|
+
.then(function (d) {
|
|
1018
|
+
if (summary) {
|
|
1019
|
+
summary.textContent = d && d.success
|
|
1020
|
+
? ('Retention applied — archived ' + (d.archived || 0) +
|
|
1021
|
+
', tombstoned ' + (d.tombstoned || 0) + ', flagged ' + (d.notified || 0) + '.')
|
|
1022
|
+
: ('Retention failed: ' + ((d && d.error) || 'unknown'));
|
|
1023
|
+
}
|
|
1024
|
+
})
|
|
1025
|
+
.catch(function () { if (summary) summary.textContent = 'Retention run failed. Check console.'; })
|
|
1026
|
+
.finally(function () { runBtn.disabled = false; runBtn.textContent = 'Run retention now'; });
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (eraseBtn) {
|
|
1031
|
+
eraseBtn.addEventListener('click', async function () {
|
|
1032
|
+
// Resolve the active profile so the confirmation is unambiguous.
|
|
1033
|
+
var profile = 'default';
|
|
1034
|
+
try {
|
|
1035
|
+
var s = await fetch('/status', { cache: 'no-store' }).then(function (r) { return r.json(); });
|
|
1036
|
+
profile = s.profile || s.active_profile || 'default';
|
|
1037
|
+
} catch (e) {}
|
|
1038
|
+
if (profile === 'default') {
|
|
1039
|
+
window.alert('The default profile cannot be erased.');
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
var typed = window.prompt(
|
|
1043
|
+
'This PERMANENTLY erases ALL data for profile "' + profile + '".\n' +
|
|
1044
|
+
'This cannot be undone.\n\nType the profile name to confirm:');
|
|
1045
|
+
if (typed !== profile) {
|
|
1046
|
+
if (typed !== null && summary) summary.textContent = 'Erase cancelled — name did not match.';
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
eraseBtn.disabled = true; eraseBtn.textContent = 'Erasing…';
|
|
1050
|
+
fetch('/api/compliance/gdpr/erase', {
|
|
1051
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1052
|
+
body: JSON.stringify({ confirm: profile }),
|
|
1053
|
+
})
|
|
1054
|
+
.then(function (r) { return r.json(); })
|
|
1055
|
+
.then(function (d) {
|
|
1056
|
+
if (summary) {
|
|
1057
|
+
summary.textContent = d && d.success
|
|
1058
|
+
? ('Profile "' + profile + '" erased. Reloading…')
|
|
1059
|
+
: ('Erase failed: ' + ((d && d.error) || 'unknown'));
|
|
1060
|
+
}
|
|
1061
|
+
if (d && d.success) setTimeout(function () { window.location.reload(); }, 900);
|
|
1062
|
+
})
|
|
1063
|
+
.catch(function () { if (summary) summary.textContent = 'Erase failed. Check console.'; })
|
|
1064
|
+
.finally(function () { eraseBtn.disabled = false; eraseBtn.textContent = 'Erase this profile…'; });
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* OD-styled "New retention policy" modal → POST /api/compliance/retention-policy.
|
|
1071
|
+
* Non-technical users define GDPR/retention rules without a CLI. On success
|
|
1072
|
+
* the Compliance tab reloads so the new policy appears immediately.
|
|
1073
|
+
*/
|
|
1074
|
+
function openRetentionPolicyModal() {
|
|
1075
|
+
var existing = document.getElementById('od-retention-overlay');
|
|
1076
|
+
if (existing) existing.remove();
|
|
1077
|
+
|
|
1078
|
+
var overlay = document.createElement('div');
|
|
1079
|
+
overlay.id = 'od-retention-overlay';
|
|
1080
|
+
overlay.setAttribute('role', 'dialog');
|
|
1081
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
1082
|
+
overlay.style.cssText =
|
|
1083
|
+
'position:fixed;inset:0;z-index:11000;display:flex;align-items:center;' +
|
|
1084
|
+
'justify-content:center;background:rgba(0,0,0,0.45);backdrop-filter:blur(2px)';
|
|
1085
|
+
|
|
1086
|
+
var card = document.createElement('div');
|
|
1087
|
+
card.style.cssText =
|
|
1088
|
+
'width:min(460px,93vw);background:var(--card,#1a1f2e);color:var(--fg,#e8ecf3);' +
|
|
1089
|
+
'border:1px solid var(--border,rgba(255,255,255,0.1));border-radius:14px;' +
|
|
1090
|
+
'box-shadow:0 20px 60px rgba(0,0,0,0.4);padding:22px 22px 18px';
|
|
1091
|
+
|
|
1092
|
+
var fieldStyle =
|
|
1093
|
+
'width:100%;box-sizing:border-box;padding:9px 11px;font-size:0.9rem;margin-top:4px;' +
|
|
1094
|
+
'background:var(--page,rgba(255,255,255,0.04));color:var(--fg,#e8ecf3);' +
|
|
1095
|
+
'border:1px solid var(--border,rgba(255,255,255,0.14));border-radius:8px;outline:none';
|
|
1096
|
+
var labelStyle = 'display:block;margin-top:12px;font-size:0.8rem;color:var(--fg-3,#8b93a7)';
|
|
1097
|
+
|
|
1098
|
+
card.innerHTML =
|
|
1099
|
+
'<h3 style="margin:0 0 4px;font-size:1.05rem;font-weight:600">New retention policy</h3>' +
|
|
1100
|
+
'<p style="margin:0 0 8px;font-size:0.8125rem;color:var(--fg-3,#8b93a7)">' +
|
|
1101
|
+
'Automatically age out memories older than a set period — GDPR-style retention.</p>' +
|
|
1102
|
+
'<label style="' + labelStyle + '">Policy name' +
|
|
1103
|
+
'<input id="od-rp-name" type="text" maxlength="60" placeholder="e.g. GDPR 90-day" style="' + fieldStyle + '"></label>' +
|
|
1104
|
+
'<label style="' + labelStyle + '">Retain for (days)' +
|
|
1105
|
+
'<input id="od-rp-days" type="number" min="1" value="90" style="' + fieldStyle + '"></label>' +
|
|
1106
|
+
'<label style="' + labelStyle + '">Framework' +
|
|
1107
|
+
'<select id="od-rp-cat" style="' + fieldStyle + '">' +
|
|
1108
|
+
'<option value="gdpr">GDPR</option><option value="hipaa">HIPAA</option>' +
|
|
1109
|
+
'<option value="sox">SOX</option><option value="custom">Custom</option>' +
|
|
1110
|
+
'</select></label>' +
|
|
1111
|
+
'<label style="' + labelStyle + '">When a memory expires' +
|
|
1112
|
+
'<select id="od-rp-action" style="' + fieldStyle + '">' +
|
|
1113
|
+
'<option value="archive">Archive (keep, hide from recall)</option>' +
|
|
1114
|
+
'<option value="tombstone">Tombstone (mark deleted)</option>' +
|
|
1115
|
+
'<option value="notify">Notify only (no change)</option>' +
|
|
1116
|
+
'</select></label>' +
|
|
1117
|
+
'<div id="od-rp-err" style="min-height:18px;margin:8px 2px 0;font-size:0.75rem;color:#ff6b6b"></div>' +
|
|
1118
|
+
'<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:8px">' +
|
|
1119
|
+
'<button type="button" id="od-rp-cancel" style="padding:8px 14px;font-size:0.85rem;border-radius:8px;cursor:pointer;background:transparent;color:var(--fg-3,#8b93a7);border:1px solid var(--border,rgba(255,255,255,0.14))">Cancel</button>' +
|
|
1120
|
+
'<button type="button" id="od-rp-create" style="padding:8px 16px;font-size:0.85rem;border-radius:8px;cursor:pointer;background:var(--violet,#7c5cff);color:#fff;border:1px solid transparent;font-weight:600">Create policy</button>' +
|
|
1121
|
+
'</div>';
|
|
1122
|
+
|
|
1123
|
+
overlay.appendChild(card);
|
|
1124
|
+
document.body.appendChild(overlay);
|
|
1125
|
+
setTimeout(function () { var n = document.getElementById('od-rp-name'); if (n) n.focus(); }, 30);
|
|
1126
|
+
|
|
1127
|
+
function close() { document.removeEventListener('keydown', onKey); overlay.remove(); }
|
|
1128
|
+
function onKey(e) { if (e.key === 'Escape') close(); }
|
|
1129
|
+
var errEl = document.getElementById('od-rp-err');
|
|
1130
|
+
var createBtn = document.getElementById('od-rp-create');
|
|
1131
|
+
|
|
1132
|
+
async function submit() {
|
|
1133
|
+
var name = (document.getElementById('od-rp-name').value || '').trim();
|
|
1134
|
+
var days = parseInt(document.getElementById('od-rp-days').value, 10);
|
|
1135
|
+
var cat = document.getElementById('od-rp-cat').value;
|
|
1136
|
+
var action = document.getElementById('od-rp-action').value;
|
|
1137
|
+
if (!name) { errEl.textContent = 'Please enter a policy name.'; return; }
|
|
1138
|
+
if (!(days >= 1)) { errEl.textContent = 'Retention days must be a positive number.'; return; }
|
|
1139
|
+
createBtn.disabled = true; createBtn.textContent = 'Creating…'; errEl.textContent = '';
|
|
1140
|
+
try {
|
|
1141
|
+
var r = await fetch('/api/compliance/retention-policy', {
|
|
1142
|
+
method: 'POST',
|
|
1143
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1144
|
+
body: JSON.stringify({ name: name, retention_days: days, category: cat, action: action }),
|
|
1145
|
+
});
|
|
1146
|
+
var data = await r.json().catch(function () { return {}; });
|
|
1147
|
+
if (!r.ok || data.success === false) {
|
|
1148
|
+
errEl.textContent = (data && data.error) || 'Failed to create policy.';
|
|
1149
|
+
createBtn.disabled = false; createBtn.textContent = 'Create policy';
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
close();
|
|
1153
|
+
if (typeof window.odRenderOperations === 'function') {
|
|
1154
|
+
// Reload the Operations pane so the new policy shows in the table.
|
|
1155
|
+
var pane = document.getElementById('operations-pane');
|
|
1156
|
+
if (pane) window.odRenderOperations(pane);
|
|
1157
|
+
}
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
errEl.textContent = 'Network error creating policy.';
|
|
1160
|
+
createBtn.disabled = false; createBtn.textContent = 'Create policy';
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
document.getElementById('od-rp-cancel').addEventListener('click', close);
|
|
1165
|
+
createBtn.addEventListener('click', submit);
|
|
1166
|
+
overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
|
|
1167
|
+
document.addEventListener('keydown', onKey);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// ─── Main entry point ─────────────────────────────────────────────────────
|
|
1171
|
+
/**
|
|
1172
|
+
* Fetch all operations endpoints, inject the OD skeleton, and populate with real data.
|
|
1173
|
+
* Idempotent: each call clears and re-renders the container.
|
|
1174
|
+
*
|
|
1175
|
+
* @param {HTMLElement} container Usually document.getElementById("operations-pane").
|
|
1176
|
+
*/
|
|
1177
|
+
function odRenderOperations(container) {
|
|
1178
|
+
if (!container) return;
|
|
1179
|
+
|
|
1180
|
+
container.innerHTML =
|
|
1181
|
+
'<div style="color:var(--fg-2);text-align:center;padding:48px 0">' +
|
|
1182
|
+
'Loading operations data…</div>';
|
|
1183
|
+
|
|
1184
|
+
// Reset audit row cache on each render
|
|
1185
|
+
_allAuditRows = [];
|
|
1186
|
+
|
|
1187
|
+
Promise.all([
|
|
1188
|
+
fetch('/api/lifecycle/status').then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }),
|
|
1189
|
+
fetch('/api/trust/stats').then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }),
|
|
1190
|
+
fetch('/api/compliance/status').then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }),
|
|
1191
|
+
]).then(function (results) {
|
|
1192
|
+
var lc = results[0];
|
|
1193
|
+
var trustStats = results[1];
|
|
1194
|
+
var comp = results[2];
|
|
1195
|
+
|
|
1196
|
+
// Inject .ctl class styles (used by Trust section control rows)
|
|
1197
|
+
injectCtlStyle();
|
|
1198
|
+
|
|
1199
|
+
// Inject static skeleton — no API data inside
|
|
1200
|
+
container.innerHTML = buildSkeleton();
|
|
1201
|
+
|
|
1202
|
+
// Wire tab switching (CSP-safe)
|
|
1203
|
+
initTabs(container);
|
|
1204
|
+
|
|
1205
|
+
// Populate lifecycle
|
|
1206
|
+
populateLifecycle(lc);
|
|
1207
|
+
// CRIT(b): compact buttons wired here — never auto-fired on render
|
|
1208
|
+
wireCompactBtns();
|
|
1209
|
+
// Restart-daemon button — wired via addEventListener, never auto-fired
|
|
1210
|
+
wireRestartBtn();
|
|
1211
|
+
|
|
1212
|
+
// Populate trust
|
|
1213
|
+
populateTrust(trustStats);
|
|
1214
|
+
|
|
1215
|
+
// Populate compliance
|
|
1216
|
+
populateComplianceKpi(comp);
|
|
1217
|
+
populateRetentionPolicies(comp);
|
|
1218
|
+
populateAuditTrail(comp);
|
|
1219
|
+
|
|
1220
|
+
// Wire audit filter and new-policy button (listeners on injected DOM)
|
|
1221
|
+
wireAuditFilter();
|
|
1222
|
+
wireNewPolicyBtn();
|
|
1223
|
+
wireGdprBtns();
|
|
1224
|
+
|
|
1225
|
+
// Team & access (RBAC / C3) — rendered by od-team.js
|
|
1226
|
+
if (typeof window.odRenderTeam === 'function') {
|
|
1227
|
+
window.odRenderTeam(document.getElementById('od-team-mount'));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
}).catch(function (err) {
|
|
1231
|
+
container.innerHTML =
|
|
1232
|
+
'<div style="color:var(--danger);text-align:center;padding:48px 0">' +
|
|
1233
|
+
'Failed to load operations data. Check the daemon is running on port 8765.</div>';
|
|
1234
|
+
if (typeof console !== 'undefined') {
|
|
1235
|
+
console.error('[od-ops] render error:', err);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
1241
|
+
|
|
1242
|
+
window.odRenderOperations = odRenderOperations;
|
|
1243
|
+
|
|
1244
|
+
// Auto-run: if the operations pane is already in the DOM at script load, render it.
|
|
1245
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
1246
|
+
var pane = document.getElementById('operations-pane');
|
|
1247
|
+
if (pane) odRenderOperations(pane);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
}());
|