superlocalmemory 3.7.8 → 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 +69 -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 -1
- 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 +94 -10
- 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/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 +8 -1
- 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 +4 -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 +10 -5
- 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 +182 -57
- 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 +183 -69
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +384 -56
- 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_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,539 @@
|
|
|
1
|
+
// SuperLocalMemory — od-mesh.js v2.0
|
|
2
|
+
// OD-styled Mesh Peers pane. Self-contained, CSP-safe.
|
|
3
|
+
// Renders into the "mesh-pane" tab container via the shell's triggerTabLoad.
|
|
4
|
+
// Exposes window.odRenderMesh(container).
|
|
5
|
+
//
|
|
6
|
+
// HTML structure mirrors approved design (mesh-peers.html <main class="content">).
|
|
7
|
+
// Design-system CSS classes used throughout — no invented inline colours.
|
|
8
|
+
//
|
|
9
|
+
// Endpoints (all via meshFetch → X-Install-Token):
|
|
10
|
+
// GET /mesh/status → {broker_up, peer_count, uptime_s}
|
|
11
|
+
// GET /mesh/peers → {peers:[{peer_id, session_id, status, last_heartbeat,
|
|
12
|
+
// summary, host, port, agent_type, project_path}]}
|
|
13
|
+
// POST /mesh/send → body:{from_peer, to_peer, content, type} → {ok:true}
|
|
14
|
+
//
|
|
15
|
+
// JSON keys mapped:
|
|
16
|
+
// status : broker_up(bool), peer_count(int), uptime_s(int)
|
|
17
|
+
// peer : peer_id, session_id, summary, status, host, port,
|
|
18
|
+
// agent_type, last_heartbeat, project_path
|
|
19
|
+
// send OK : ok(bool) | send ERR: detail(str)
|
|
20
|
+
//
|
|
21
|
+
// CRIT fixes baked in:
|
|
22
|
+
// C1 — send guarded: button disabled when no peer selected or peer is not active
|
|
23
|
+
// C2 — fetch errors surface in conversation log; no silent broken state
|
|
24
|
+
// C3 — all API strings through escapeHtml() before innerHTML assignment
|
|
25
|
+
//
|
|
26
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
|
|
27
|
+
|
|
28
|
+
(function () {
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
var REFRESH_MS = 5000;
|
|
32
|
+
var SEND_FROM = 'dashboard';
|
|
33
|
+
|
|
34
|
+
// ── Token helpers (mirrors ng-mesh.js; never stored in sessionStorage) ────
|
|
35
|
+
var _tokenCache = null;
|
|
36
|
+
|
|
37
|
+
function fetchTokenFromServer() {
|
|
38
|
+
return fetch('/internal/token', { credentials: 'same-origin' })
|
|
39
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
40
|
+
.then(function (d) {
|
|
41
|
+
var tok = d && typeof d.token === 'string' ? d.token.trim() : '';
|
|
42
|
+
if (tok) _tokenCache = tok;
|
|
43
|
+
return tok || null;
|
|
44
|
+
})
|
|
45
|
+
.catch(function () { return null; });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureToken() {
|
|
49
|
+
return _tokenCache ? Promise.resolve(_tokenCache) : fetchTokenFromServer();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Authenticated fetch; retries once on 401 (stale token).
|
|
53
|
+
function meshFetch(url, opts) {
|
|
54
|
+
return ensureToken().then(function (token) {
|
|
55
|
+
var base = Object.assign({ credentials: 'same-origin' }, opts || {});
|
|
56
|
+
if (token) {
|
|
57
|
+
base.headers = Object.assign({ 'X-Install-Token': token }, base.headers || {});
|
|
58
|
+
}
|
|
59
|
+
return fetch(url, base).then(function (r) {
|
|
60
|
+
if (r.status !== 401) return r;
|
|
61
|
+
return fetchTokenFromServer().then(function (fresh) {
|
|
62
|
+
var retry = Object.assign({ credentials: 'same-origin' }, opts || {});
|
|
63
|
+
if (fresh) {
|
|
64
|
+
retry.headers = Object.assign({ 'X-Install-Token': fresh }, retry.headers || {});
|
|
65
|
+
}
|
|
66
|
+
return fetch(url, retry);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Module state ──────────────────────────────────────────────────────────
|
|
73
|
+
var _peers = []; // last-fetched peer list
|
|
74
|
+
var _selectedId = null; // peer_id currently shown in conversation panel
|
|
75
|
+
var _convLogs = {}; // peer_id → [{role:'q'|'a', text}]
|
|
76
|
+
var _container = null;
|
|
77
|
+
var _timer = null;
|
|
78
|
+
var _observer = null; // MutationObserver — re-starts refresh on tab activate
|
|
79
|
+
|
|
80
|
+
// ── HTML helpers ──────────────────────────────────────────────────────────
|
|
81
|
+
// CRIT C3: every API-derived string MUST pass through this.
|
|
82
|
+
function escapeHtml(s) {
|
|
83
|
+
return String(s == null ? '' : s)
|
|
84
|
+
.replace(/&/g, '&')
|
|
85
|
+
.replace(/</g, '<')
|
|
86
|
+
.replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"')
|
|
88
|
+
.replace(/'/g, ''');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function badgeCls(st) {
|
|
92
|
+
return st === 'active' ? 'ok' : st === 'stale' ? 'warn' : 'danger';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function timeAgo(iso) {
|
|
96
|
+
if (!iso) return 'unknown';
|
|
97
|
+
var d = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
98
|
+
if (d < 5) return 'now';
|
|
99
|
+
if (d < 60) return Math.floor(d) + 's';
|
|
100
|
+
if (d < 3600) return Math.floor(d / 60) + 'm';
|
|
101
|
+
if (d < 86400) return Math.floor(d / 3600) + 'h';
|
|
102
|
+
return Math.floor(d / 86400) + 'd';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fmtUptime(sec) {
|
|
106
|
+
if (!sec) return '—';
|
|
107
|
+
var h = Math.floor(sec / 3600);
|
|
108
|
+
if (h > 24) return Math.floor(h / 24) + 'd';
|
|
109
|
+
var m = Math.floor((sec % 3600) / 60);
|
|
110
|
+
return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sendSvg() {
|
|
114
|
+
// Use shell's slmIcon if available; inline fallback for safety.
|
|
115
|
+
if (typeof window.slmIcon === 'function') return window.slmIcon('send');
|
|
116
|
+
return '<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor"' +
|
|
117
|
+
' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
118
|
+
'<line x1="22" y1="2" x2="11" y2="13"/>' +
|
|
119
|
+
'<polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Layout HTML ───────────────────────────────────────────────────────────
|
|
123
|
+
// Mirrors <main class="content"> from approved design (mesh-peers.html).
|
|
124
|
+
function buildHTML() {
|
|
125
|
+
return (
|
|
126
|
+
'<div id="od-mesh-root">' +
|
|
127
|
+
|
|
128
|
+
// Page head — description updated live from /mesh/status
|
|
129
|
+
'<div class="page-head">' +
|
|
130
|
+
'<h2>Mesh peers</h2>' +
|
|
131
|
+
'<p id="od-mesh-page-desc">Loading mesh status…</p>' +
|
|
132
|
+
'</div>' +
|
|
133
|
+
|
|
134
|
+
// Two-column: peer cards (left) + conversation panel (right)
|
|
135
|
+
'<div style="display:grid;grid-template-columns:1.15fr 1fr;gap:20px;align-items:start">' +
|
|
136
|
+
|
|
137
|
+
// LEFT — peer card grid
|
|
138
|
+
'<div>' +
|
|
139
|
+
'<div class="launch-grid" id="od-mesh-peers-grid"' +
|
|
140
|
+
' style="grid-template-columns:1fr 1fr">' +
|
|
141
|
+
'<div id="od-mesh-peers-ph"' +
|
|
142
|
+
' style="grid-column:1/-1;text-align:center;padding:40px;color:var(--fg-2)">' +
|
|
143
|
+
'<div style="font-size:15px;margin-bottom:6px">Loading peers…</div>' +
|
|
144
|
+
'</div>' +
|
|
145
|
+
'</div>' +
|
|
146
|
+
'</div>' +
|
|
147
|
+
|
|
148
|
+
// RIGHT — conversation panel (sticky, full-height card)
|
|
149
|
+
'<div class="card"' +
|
|
150
|
+
' style="position:sticky;top:86px;display:flex;flex-direction:column;' +
|
|
151
|
+
'height:calc(100vh - 120px)">' +
|
|
152
|
+
|
|
153
|
+
// Card header: selected peer info
|
|
154
|
+
'<div class="card-head">' +
|
|
155
|
+
'<span class="avatar" id="od-mesh-cv-avatar"' +
|
|
156
|
+
' style="width:30px;height:30px">?</span>' +
|
|
157
|
+
'<div>' +
|
|
158
|
+
'<h3 id="od-mesh-cv-name">No peer selected</h3>' +
|
|
159
|
+
'<span class="sub" id="od-mesh-cv-meta">' +
|
|
160
|
+
'Click a peer card to start messaging' +
|
|
161
|
+
'</span>' +
|
|
162
|
+
'</div>' +
|
|
163
|
+
'<div class="spacer"></div>' +
|
|
164
|
+
'<span class="badge neutral" id="od-mesh-cv-badge">—</span>' +
|
|
165
|
+
'</div>' +
|
|
166
|
+
|
|
167
|
+
// Conversation log
|
|
168
|
+
'<div class="ask-log" id="od-mesh-cv-log"' +
|
|
169
|
+
' style="flex:1;max-height:none;padding:16px;display:flex;flex-direction:column;gap:10px">' +
|
|
170
|
+
'<div style="text-align:center;color:var(--fg-3);font-size:13px;margin-top:30px">' +
|
|
171
|
+
'Select a peer to start messaging' +
|
|
172
|
+
'</div>' +
|
|
173
|
+
'</div>' +
|
|
174
|
+
|
|
175
|
+
// Input row — C1: disabled until active peer selected
|
|
176
|
+
'<div class="ask-input" style="border-top:1px solid var(--border)">' +
|
|
177
|
+
'<input id="od-mesh-cv-input" disabled autocomplete="off"' +
|
|
178
|
+
' placeholder="Select a peer to start messaging">' +
|
|
179
|
+
'<button id="od-mesh-cv-send" disabled aria-label="Send message">' +
|
|
180
|
+
sendSvg() +
|
|
181
|
+
'</button>' +
|
|
182
|
+
'</div>' +
|
|
183
|
+
|
|
184
|
+
'</div>' + // end conversation panel
|
|
185
|
+
|
|
186
|
+
'</div>' + // end two-column
|
|
187
|
+
'</div>' // od-mesh-root
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Render: page-head description from /mesh/status ───────────────────────
|
|
192
|
+
function renderPageDesc(data) {
|
|
193
|
+
var el = document.getElementById('od-mesh-page-desc');
|
|
194
|
+
if (!el) return;
|
|
195
|
+
if (!data) {
|
|
196
|
+
el.textContent = 'Agents sharing this memory mesh on your machine and LAN.' +
|
|
197
|
+
' Peers go stale after 5 min without a heartbeat, dead after 30.';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
var up = data.broker_up === true ? 'Broker up' : 'Broker offline';
|
|
201
|
+
var cnt = data.peer_count != null
|
|
202
|
+
? data.peer_count + ' peer' + (data.peer_count === 1 ? '' : 's')
|
|
203
|
+
: '';
|
|
204
|
+
var upt = data.uptime_s != null ? 'uptime ' + fmtUptime(data.uptime_s) : '';
|
|
205
|
+
var parts = [up];
|
|
206
|
+
if (cnt) parts.push(cnt);
|
|
207
|
+
if (upt) parts.push(upt);
|
|
208
|
+
el.textContent = 'Agents sharing this memory mesh on your machine and LAN. ' +
|
|
209
|
+
parts.join(' · ') + '. Peers go stale after 5 min without a heartbeat, dead after 30.';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Render: peer cards ────────────────────────────────────────────────────
|
|
213
|
+
function meshEmptyIcon() {
|
|
214
|
+
if (typeof window.slmIcon === 'function') return window.slmIcon('mesh');
|
|
215
|
+
return '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor"' +
|
|
216
|
+
' stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
|
|
217
|
+
'<circle cx="12" cy="12" r="2.5"/><circle cx="5" cy="5" r="2"/>' +
|
|
218
|
+
'<circle cx="19" cy="5" r="2"/><circle cx="5" cy="19" r="2"/>' +
|
|
219
|
+
'<circle cx="19" cy="19" r="2"/>' +
|
|
220
|
+
'<path d="M10 10L6.5 6.5M14 10l3.5-3.5M10 14l-3.5 3.5M14 14l3.5 3.5"/></svg>';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderPeers(peers) {
|
|
224
|
+
var grid = document.getElementById('od-mesh-peers-grid');
|
|
225
|
+
if (!grid) return;
|
|
226
|
+
|
|
227
|
+
if (!peers || peers.length === 0) {
|
|
228
|
+
// Design pattern: proper launch-card in the grid, not bare centered text
|
|
229
|
+
grid.innerHTML =
|
|
230
|
+
'<div class="card launch-card"' +
|
|
231
|
+
' style="grid-column:1/-1;display:flex;flex-direction:column;' +
|
|
232
|
+
'align-items:center;justify-content:center;' +
|
|
233
|
+
'padding:48px 24px;text-align:center;cursor:default">' +
|
|
234
|
+
'<div style="width:44px;height:44px;border-radius:12px;' +
|
|
235
|
+
'background:var(--card-2);display:flex;align-items:center;' +
|
|
236
|
+
'justify-content:center;margin-bottom:16px;color:var(--fg-3)">' +
|
|
237
|
+
meshEmptyIcon() +
|
|
238
|
+
'</div>' +
|
|
239
|
+
'<h3 style="font-size:15px;margin-bottom:6px">No peers connected</h3>' +
|
|
240
|
+
'<p style="font-size:12.5px;color:var(--fg-2);' +
|
|
241
|
+
'max-width:30ch;line-height:1.5">' +
|
|
242
|
+
'Start another agent session. Peers register via the' +
|
|
243
|
+
' <code>mesh_summary</code> MCP tool.' +
|
|
244
|
+
'</p>' +
|
|
245
|
+
'</div>';
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
var html = '';
|
|
250
|
+
peers.forEach(function (p) {
|
|
251
|
+
var st = p.status || 'unknown';
|
|
252
|
+
var name = escapeHtml(p.session_id || p.peer_id || 'Unknown');
|
|
253
|
+
var pidFrag = escapeHtml((p.peer_id || '').slice(0, 12));
|
|
254
|
+
var summary = escapeHtml(p.summary || 'No summary');
|
|
255
|
+
var atype = escapeHtml(p.agent_type || 'unknown');
|
|
256
|
+
var ago = escapeHtml(timeAgo(p.last_heartbeat));
|
|
257
|
+
var proj = escapeHtml(p.project_path || '~');
|
|
258
|
+
var port = p.port ? ' · :' + p.port : '';
|
|
259
|
+
var letter = (p.session_id || p.peer_id || '?').charAt(0).toUpperCase();
|
|
260
|
+
var avatarBg = p.agent_type === 'mcp' ? 'var(--violet)' : 'var(--cyan)';
|
|
261
|
+
var pid = escapeHtml(p.peer_id || '');
|
|
262
|
+
var toolCnt = Number(p.tool_count || 0);
|
|
263
|
+
var memCnt = Number(p.memory_count || 0);
|
|
264
|
+
// Latency — API may return latency_ms; dead peers always show '—'
|
|
265
|
+
var latencyStr = p.latency_ms != null
|
|
266
|
+
? escapeHtml(String(p.latency_ms)) + ' ms'
|
|
267
|
+
: (st === 'dead' ? '—' : '—');
|
|
268
|
+
|
|
269
|
+
html +=
|
|
270
|
+
'<div class="card launch-card" data-peer-id="' + pid + '"' +
|
|
271
|
+
' role="button" tabindex="0" aria-label="Select peer ' + name + '">' +
|
|
272
|
+
'<div style="display:flex;align-items:center;gap:11px;margin-bottom:12px">' +
|
|
273
|
+
'<span class="avatar" style="background:' + avatarBg + '">' + letter + '</span>' +
|
|
274
|
+
'<div style="flex:1;min-width:0">' +
|
|
275
|
+
'<h3 style="font-size:15px;white-space:nowrap;overflow:hidden;' +
|
|
276
|
+
'text-overflow:ellipsis">' + name + '</h3>' +
|
|
277
|
+
'<span class="mono dim" style="font-size:11px">' + pidFrag + '</span>' +
|
|
278
|
+
'</div>' +
|
|
279
|
+
'<span class="badge ' + badgeCls(st) + '">' +
|
|
280
|
+
'<span class="dot"></span>' + escapeHtml(st) +
|
|
281
|
+
'</span>' +
|
|
282
|
+
'</div>' +
|
|
283
|
+
'<p style="font-size:12.5px;color:var(--fg-2);margin-bottom:12px;' +
|
|
284
|
+
'overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;' +
|
|
285
|
+
'-webkit-box-orient:vertical">' + summary + '</p>' +
|
|
286
|
+
// Design: proto / latency / last (three items)
|
|
287
|
+
'<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px" class="mono">' +
|
|
288
|
+
'<span><span class="dim">proto</span> ' + atype + '</span>' +
|
|
289
|
+
'<span><span class="dim">memories</span> ' + memCnt + '</span>' +
|
|
290
|
+
'<span><span class="dim">tools</span> ' + toolCnt + '</span>' +
|
|
291
|
+
'<span><span class="dim">last</span> ' + ago + '</span>' +
|
|
292
|
+
'</div>' +
|
|
293
|
+
'<div class="mono dim" style="font-size:11px;margin-top:8px">' +
|
|
294
|
+
proj + port +
|
|
295
|
+
'</div>' +
|
|
296
|
+
'</div>';
|
|
297
|
+
});
|
|
298
|
+
grid.innerHTML = html;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Peer selection: update conversation panel header + input state ─────────
|
|
302
|
+
function selectPeerById(pid) {
|
|
303
|
+
_selectedId = pid || null;
|
|
304
|
+
var peer = pid ? _peers.find(function (p) { return p.peer_id === pid; }) : null;
|
|
305
|
+
|
|
306
|
+
var avatarEl = document.getElementById('od-mesh-cv-avatar');
|
|
307
|
+
var nameEl = document.getElementById('od-mesh-cv-name');
|
|
308
|
+
var metaEl = document.getElementById('od-mesh-cv-meta');
|
|
309
|
+
var badgeEl = document.getElementById('od-mesh-cv-badge');
|
|
310
|
+
var inputEl = document.getElementById('od-mesh-cv-input');
|
|
311
|
+
var sendEl = document.getElementById('od-mesh-cv-send');
|
|
312
|
+
|
|
313
|
+
if (!peer) {
|
|
314
|
+
if (avatarEl) { avatarEl.textContent = '?'; avatarEl.style.background = ''; }
|
|
315
|
+
if (nameEl) nameEl.textContent = 'No peer selected';
|
|
316
|
+
if (metaEl) metaEl.textContent = 'Click a peer card to start messaging';
|
|
317
|
+
if (badgeEl) { badgeEl.className = 'badge neutral'; badgeEl.textContent = '—'; }
|
|
318
|
+
// CRIT C1: disable when no peer
|
|
319
|
+
if (inputEl) { inputEl.disabled = true; inputEl.placeholder = 'Select a peer to start messaging'; }
|
|
320
|
+
if (sendEl) { sendEl.disabled = true; sendEl.style.opacity = '0.4'; }
|
|
321
|
+
renderConv();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
var st = peer.status || 'unknown';
|
|
326
|
+
// CRIT C1: dead peers are gone from the broker — disable send.
|
|
327
|
+
// Stale peers are still registered and can queue messages (matches design behaviour).
|
|
328
|
+
var canSend = (st !== 'dead');
|
|
329
|
+
var atype = peer.agent_type || 'unknown';
|
|
330
|
+
var letter = (peer.session_id || peer.peer_id || '?').charAt(0).toUpperCase();
|
|
331
|
+
var avatarBg = atype === 'mcp' ? 'var(--violet)' : 'var(--cyan)';
|
|
332
|
+
|
|
333
|
+
if (avatarEl) { avatarEl.textContent = letter; avatarEl.style.background = avatarBg; }
|
|
334
|
+
if (nameEl) nameEl.textContent = peer.session_id || peer.peer_id || 'Unknown';
|
|
335
|
+
if (metaEl) metaEl.textContent =
|
|
336
|
+
atype + ' · ' + st +
|
|
337
|
+
(peer.host && peer.port ? ' · ' + peer.host + ':' + peer.port : '');
|
|
338
|
+
if (badgeEl) {
|
|
339
|
+
badgeEl.className = 'badge ' + badgeCls(st);
|
|
340
|
+
badgeEl.innerHTML = '<span class="dot"></span> ' + escapeHtml(st);
|
|
341
|
+
}
|
|
342
|
+
if (inputEl) {
|
|
343
|
+
inputEl.disabled = !canSend;
|
|
344
|
+
inputEl.placeholder = st === 'dead'
|
|
345
|
+
? 'Peer is unreachable'
|
|
346
|
+
: 'Message ' + escapeHtml(peer.session_id || 'peer') + '…';
|
|
347
|
+
}
|
|
348
|
+
if (sendEl) {
|
|
349
|
+
sendEl.disabled = !canSend;
|
|
350
|
+
sendEl.style.opacity = canSend ? '1' : '0.4';
|
|
351
|
+
}
|
|
352
|
+
renderConv();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Render: conversation log ──────────────────────────────────────────────
|
|
356
|
+
function renderConv() {
|
|
357
|
+
var log = document.getElementById('od-mesh-cv-log');
|
|
358
|
+
if (!log) return;
|
|
359
|
+
var msgs = _selectedId ? (_convLogs[_selectedId] || []) : [];
|
|
360
|
+
if (msgs.length === 0) {
|
|
361
|
+
log.innerHTML =
|
|
362
|
+
'<div style="text-align:center;color:var(--fg-3);font-size:13px;margin-top:30px">' +
|
|
363
|
+
(_selectedId
|
|
364
|
+
? 'No messages yet — type below to send'
|
|
365
|
+
: 'Select a peer to start messaging') +
|
|
366
|
+
'</div>';
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
var html = '';
|
|
370
|
+
msgs.forEach(function (m) {
|
|
371
|
+
// .msg.q and .msg.a are defined in design-system.css
|
|
372
|
+
html += '<div class="msg ' + m.role + '">' + escapeHtml(m.text) + '</div>';
|
|
373
|
+
});
|
|
374
|
+
log.innerHTML = html;
|
|
375
|
+
log.scrollTop = log.scrollHeight;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Send message to peer via POST /mesh/send ──────────────────────────────
|
|
379
|
+
function sendMessage() {
|
|
380
|
+
// CRIT C1: bail immediately if no peer selected
|
|
381
|
+
if (!_selectedId) return;
|
|
382
|
+
var inputEl = document.getElementById('od-mesh-cv-input');
|
|
383
|
+
if (!inputEl) return;
|
|
384
|
+
var text = inputEl.value.trim();
|
|
385
|
+
if (!text) return;
|
|
386
|
+
|
|
387
|
+
inputEl.value = '';
|
|
388
|
+
|
|
389
|
+
// Optimistic echo
|
|
390
|
+
if (!_convLogs[_selectedId]) _convLogs[_selectedId] = [];
|
|
391
|
+
_convLogs[_selectedId].push({ role: 'q', text: text });
|
|
392
|
+
renderConv();
|
|
393
|
+
|
|
394
|
+
var peerId = _selectedId; // capture before any async re-assign
|
|
395
|
+
meshFetch('/mesh/send', {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
from_peer: SEND_FROM,
|
|
400
|
+
to_peer: peerId,
|
|
401
|
+
content: text,
|
|
402
|
+
type: 'text'
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
.then(function (r) {
|
|
406
|
+
return r.json().then(function (d) { return { ok: r.ok, data: d }; });
|
|
407
|
+
})
|
|
408
|
+
.then(function (res) {
|
|
409
|
+
// CRIT C2: always surface errors — never silent
|
|
410
|
+
var ack = (res.ok && res.data && res.data.ok)
|
|
411
|
+
? 'Delivered — queued in mesh (TTL 48h).'
|
|
412
|
+
: 'Error: ' + escapeHtml(
|
|
413
|
+
(res.data && res.data.detail) ? String(res.data.detail) : 'unknown error'
|
|
414
|
+
);
|
|
415
|
+
_convLogs[peerId].push({ role: 'a', text: ack });
|
|
416
|
+
renderConv();
|
|
417
|
+
})
|
|
418
|
+
.catch(function () {
|
|
419
|
+
// CRIT C2: network failure visible in log
|
|
420
|
+
_convLogs[peerId].push({ role: 'a', text: 'Network error — could not reach broker.' });
|
|
421
|
+
renderConv();
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Data loaders ──────────────────────────────────────────────────────────
|
|
426
|
+
function loadAll() {
|
|
427
|
+
meshFetch('/mesh/status')
|
|
428
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
429
|
+
.catch(function () { return null; })
|
|
430
|
+
.then(renderPageDesc);
|
|
431
|
+
|
|
432
|
+
meshFetch('/mesh/peers')
|
|
433
|
+
.then(function (r) { return r.ok ? r.json() : { peers: [] }; })
|
|
434
|
+
.catch(function () { return { peers: [] }; })
|
|
435
|
+
.then(function (d) {
|
|
436
|
+
_peers = Array.isArray(d && d.peers) ? d.peers : [];
|
|
437
|
+
renderPeers(_peers);
|
|
438
|
+
// If the currently selected peer has expired, deselect
|
|
439
|
+
if (_selectedId && !_peers.find(function (p) { return p.peer_id === _selectedId; })) {
|
|
440
|
+
selectPeerById(null);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Event wiring (CSP-safe delegation — no onXxx= attributes) ─────────────
|
|
446
|
+
function wireEvents(container) {
|
|
447
|
+
// Peer card click → select for conversation
|
|
448
|
+
var grid = container.querySelector('#od-mesh-peers-grid');
|
|
449
|
+
if (grid) {
|
|
450
|
+
grid.addEventListener('click', function (e) {
|
|
451
|
+
var card = e.target.closest('[data-peer-id]');
|
|
452
|
+
if (!card) return;
|
|
453
|
+
var pid = card.getAttribute('data-peer-id');
|
|
454
|
+
if (pid) selectPeerById(pid);
|
|
455
|
+
});
|
|
456
|
+
grid.addEventListener('keydown', function (e) {
|
|
457
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
458
|
+
var card = e.target.closest('[data-peer-id]');
|
|
459
|
+
if (!card) return;
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
var pid = card.getAttribute('data-peer-id');
|
|
462
|
+
if (pid) selectPeerById(pid);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Send button
|
|
467
|
+
var sendBtn = container.querySelector('#od-mesh-cv-send');
|
|
468
|
+
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
|
|
469
|
+
|
|
470
|
+
// Enter key in input
|
|
471
|
+
var inputEl = container.querySelector('#od-mesh-cv-input');
|
|
472
|
+
if (inputEl) {
|
|
473
|
+
inputEl.addEventListener('keydown', function (e) {
|
|
474
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── Refresh management: MutationObserver re-starts on tab activate ────────
|
|
480
|
+
function startRefreshObserver() {
|
|
481
|
+
var pane = document.getElementById('mesh-pane');
|
|
482
|
+
if (!pane || _observer) return;
|
|
483
|
+
_observer = new MutationObserver(function (mutations) {
|
|
484
|
+
mutations.forEach(function (m) {
|
|
485
|
+
if (m.type !== 'attributes' || m.attributeName !== 'class') return;
|
|
486
|
+
var t = m.target;
|
|
487
|
+
if (t.classList.contains('active') && t.classList.contains('show')) {
|
|
488
|
+
loadAll();
|
|
489
|
+
if (_timer) clearInterval(_timer);
|
|
490
|
+
_timer = setInterval(function () {
|
|
491
|
+
if (t.classList.contains('active')) {
|
|
492
|
+
loadAll();
|
|
493
|
+
} else {
|
|
494
|
+
clearInterval(_timer);
|
|
495
|
+
_timer = null;
|
|
496
|
+
}
|
|
497
|
+
}, REFRESH_MS);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
_observer.observe(pane, { attributes: true });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
505
|
+
window.odRenderMesh = function (container) {
|
|
506
|
+
if (!container) return;
|
|
507
|
+
_container = container;
|
|
508
|
+
|
|
509
|
+
// Idempotent: if already rendered, only reload data
|
|
510
|
+
if (container.querySelector('#od-mesh-root')) {
|
|
511
|
+
loadAll();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
container.innerHTML = buildHTML();
|
|
516
|
+
wireEvents(container);
|
|
517
|
+
loadAll();
|
|
518
|
+
startRefreshObserver();
|
|
519
|
+
|
|
520
|
+
// Initial poll timer (self-stops when pane becomes inactive)
|
|
521
|
+
if (_timer) clearInterval(_timer);
|
|
522
|
+
_timer = setInterval(function () {
|
|
523
|
+
var pane = document.getElementById('mesh-pane');
|
|
524
|
+
if (pane && pane.classList.contains('active')) {
|
|
525
|
+
loadAll();
|
|
526
|
+
} else {
|
|
527
|
+
clearInterval(_timer);
|
|
528
|
+
_timer = null;
|
|
529
|
+
}
|
|
530
|
+
}, REFRESH_MS);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// Auto-run at DOMContentLoaded if the pane already exists in DOM
|
|
534
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
535
|
+
var pane = document.getElementById('mesh-pane');
|
|
536
|
+
if (pane) window.odRenderMesh(pane);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
}());
|