prism-mcp-server 20.21.19 → 20.21.21

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.
@@ -13,6 +13,7 @@
13
13
  * - Responsive grid layout
14
14
  * ═══════════════════════════════════════════════════════════════════
15
15
  */
16
+ import { ANALYTICS_DATE_PATTERN, ANALYTICS_METRIC_IDS, DASHBOARD_READ_MESSAGES } from './readMessages.js';
16
17
  export function renderDashboardLocalOpenHTML() {
17
18
  return `<!DOCTYPE html>
18
19
  <html lang="en">
@@ -191,6 +192,7 @@ export function renderDashboardHTML(version) {
191
192
 
192
193
  /* ─── Glass Cards ─── */
193
194
  .card {
195
+ min-width: 0; overflow-wrap: anywhere;
194
196
  background: var(--bg-glass); backdrop-filter: blur(16px);
195
197
  border: 1px solid var(--border-glass); border-radius: var(--radius);
196
198
  padding: 1.5rem; transition: border-color 0.3s ease, box-shadow 0.3s ease;
@@ -206,8 +208,10 @@ export function renderDashboardHTML(version) {
206
208
  }
207
209
 
208
210
  /* ─── Grid Layout ─── */
209
- .grid { display: grid; gap: 1.5rem; }
210
- .grid-main { grid-template-columns: 1fr 2fr; }
211
+ .grid { display: grid; gap: 1.5rem; min-width: 0; }
212
+ .grid-main { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
213
+ .graph-filters select { color: var(--text-primary); background: var(--bg-secondary); border: 1px solid var(--border-glass); border-radius: 6px; max-width: 100%; }
214
+ .graph-filters option { color: var(--text-primary); background: var(--bg-secondary); }
211
215
  @media (max-width: 900px) { .grid-main { grid-template-columns: 1fr; } }
212
216
 
213
217
  /* ─── PWA Mobile Overrides (v5.4) ─── */
@@ -864,8 +868,9 @@ export function renderDashboardHTML(version) {
864
868
  <!-- Brain Health (v2.2.0) -->
865
869
  <div class="card" id="healthCard" style="display:none">
866
870
  <div class="card-title">
867
- <span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺
868
- <button class="cleanup-btn" id="cleanupBtn" onclick="cleanupIssues()" style="display:none">🧹 Fix Issues</button>
871
+ <span class="dot" style="background:var(--accent-green)"></span> ${DASHBOARD_READ_MESSAGES.memoryScanTitle}
872
+ <button class="refresh-btn" id="healthRefreshBtn" type="button" onclick="refreshHealthScan()">${DASHBOARD_READ_MESSAGES.refreshScan}</button>
873
+ <button class="cleanup-btn" id="cleanupBtn" onclick="cleanupIssues()" style="display:none" title="${DASHBOARD_READ_MESSAGES.repairCoverage}">🧹 Fix Issues</button>
869
874
  </div>
870
875
  <div class="health-status">
871
876
  <div class="health-dot unknown" id="healthDot"></div>
@@ -875,6 +880,8 @@ export function renderDashboardHTML(version) {
875
880
  </div>
876
881
  </div>
877
882
  <div class="health-issues" id="healthIssues"></div>
883
+ <p id="healthCoverageNote" style="color:var(--text-muted);font-size:0.75rem"></p>
884
+ <p id="healthRepairNote" style="color:var(--text-muted);font-size:0.75rem">${DASHBOARD_READ_MESSAGES.repairCoverage}</p>
878
885
  <!-- Repair progress bar (v6.1.4) -->
879
886
  <div class="health-progress-wrap" id="healthProgressWrap">
880
887
  <div class="health-progress-header">
@@ -892,13 +899,15 @@ export function renderDashboardHTML(version) {
892
899
  <div class="card-title">
893
900
  <span class="dot" style="background:var(--accent-purple)"></span>
894
901
  Memory Analytics 📊
902
+ <button class="refresh-btn" id="analyticsRetryBtn" type="button" onclick="loadAnalytics(document.getElementById('projectSelect').value)">${DASHBOARD_READ_MESSAGES.retry}</button>
895
903
  </div>
904
+ <div id="analyticsReadStatus" role="status" style="display:none;color:var(--accent-rose);font-size:0.85rem;margin-bottom:0.75rem"></div>
896
905
  <div class="sparkline" id="sparkline" title="Sessions per day (last 14 days)"></div>
897
906
  <div style="font-size:0.68rem;color:var(--text-muted);text-align:right">Sessions / day (14d)</div>
898
907
  <div class="analytics-stats">
899
- <div class="astat"><div class="astat-val" id="astat-entries">—</div><div class="astat-label">Active sessions</div></div>
908
+ <div class="astat"><div class="astat-val" id="astat-entries">—</div><div class="astat-label">${DASHBOARD_READ_MESSAGES.activeEntries}</div></div>
900
909
  <div class="astat"><div class="astat-val" id="astat-rollups">—</div><div class="astat-label">Rollups</div></div>
901
- <div class="astat"><div class="astat-val" id="astat-savings">—</div><div class="astat-label">Entries saved</div></div>
910
+ <div class="astat"><div class="astat-val" id="astat-savings">—</div><div class="astat-label">${DASHBOARD_READ_MESSAGES.consolidatedEntries}</div></div>
902
911
  <div class="astat"><div class="astat-val" id="astat-avglen">—</div><div class="astat-label">Avg summary chars</div></div>
903
912
  </div>
904
913
  </div>
@@ -1017,7 +1026,7 @@ export function renderDashboardHTML(version) {
1017
1026
  </div>
1018
1027
 
1019
1028
  <!-- v5.1 Graph Filters -->
1020
- <div style="display:flex; gap:0.5rem; margin-bottom:1rem; flex-wrap:wrap; align-items:center;">
1029
+ <div class="graph-filters" style="display:flex; gap:0.5rem; margin-bottom:1rem; flex-wrap:wrap; align-items:center;">
1021
1030
  <select id="graphProjectFilter" class="input-modern" style="min-width:120px; font-size:0.75rem; padding:0.3rem 0.5rem" onchange="loadGraph()">
1022
1031
  <option value="">All Projects</option>
1023
1032
  </select>
@@ -1131,7 +1140,7 @@ export function renderDashboardHTML(version) {
1131
1140
  <div class="card" style="margin-top:1rem">
1132
1141
  <div class="card-title" style="display:flex;align-items:center;gap:0.5rem">
1133
1142
  <span class="dot" style="background:var(--accent-blue)"></span>
1134
- Graph Health 📊
1143
+ ${DASHBOARD_READ_MESSAGES.graphActivityTitle}
1135
1144
  <div style="flex:1"></div>
1136
1145
  <span id="graphHealthWarnings" style="display:inline-flex;gap:0.3rem"></span>
1137
1146
  <button onclick="loadGraphMetrics()" class="refresh-btn">↻</button>
@@ -1185,251 +1194,34 @@ export function renderDashboardHTML(version) {
1185
1194
  </div>
1186
1195
  </div>
1187
1196
 
1188
- <!-- VM Lab Panel (v14.0) -->
1189
- <div id="vm-content" class="fade-in" style="display:none; margin: 0 auto; max-width: 1100px; padding: 0 1rem;">
1197
+ <!-- VM Lab: inventory must come from a runtime provider. -->
1198
+ <div id="vm-content" class="fade-in" style="display:none;max-width:1100px;margin:auto;padding:0 1rem;">
1190
1199
  <div class="card">
1191
- <div class="card-title">
1192
- <span class="dot" style="background:var(--accent-blue)"></span>
1193
- Device Template Gallery 🖥️
1194
- <span style="margin-left:auto;font-size:0.72rem;color:var(--text-muted)">14 templates</span>
1195
- </div>
1196
- <div class="vm-gallery">
1197
- <div class="vm-card"><span class="vm-tier free">FREE</span><div class="vm-icon">🐧</div><div class="vm-name">Ubuntu 24.04 LTS</div><div class="vm-specs">4 cores · 8 GB · 64 GB SSD</div></div>
1198
- <div class="vm-card"><span class="vm-tier free">FREE</span><div class="vm-icon">🐧</div><div class="vm-name">Debian 12 Minimal</div><div class="vm-specs">2 cores · 4 GB · 32 GB SSD</div></div>
1199
- <div class="vm-card"><span class="vm-tier standard">STD</span><div class="vm-icon">🪣</div><div class="vm-name">Windows 11 Dev</div><div class="vm-specs">4 cores · 16 GB · 128 GB SSD</div></div>
1200
- <div class="vm-card"><span class="vm-tier standard">STD</span><div class="vm-icon">🪣</div><div class="vm-name">Windows Server 2022</div><div class="vm-specs">8 cores · 32 GB · 256 GB SSD</div></div>
1201
- <div class="vm-card"><span class="vm-tier advanced">ADV</span><div class="vm-icon">📱</div><div class="vm-name">iOS 18 Simulator</div><div class="vm-specs">4 cores · 8 GB · 64 GB SSD</div></div>
1202
- <div class="vm-card"><span class="vm-tier advanced">ADV</span><div class="vm-icon">📱</div><div class="vm-name">iPadOS 18</div><div class="vm-specs">4 cores · 8 GB · 64 GB SSD</div></div>
1203
- <div class="vm-card"><span class="vm-tier advanced">ADV</span><div class="vm-icon">⌚</div><div class="vm-name">watchOS 11</div><div class="vm-specs">2 cores · 2 GB · 16 GB SSD</div></div>
1204
- <div class="vm-card"><span class="vm-tier standard">STD</span><div class="vm-icon">🤖</div><div class="vm-name">Android 15</div><div class="vm-specs">4 cores · 8 GB · 64 GB SSD</div></div>
1205
- <div class="vm-card"><span class="vm-tier standard">STD</span><div class="vm-icon">⌚</div><div class="vm-name">Wear OS 5</div><div class="vm-specs">2 cores · 2 GB · 16 GB SSD</div></div>
1206
- <div class="vm-card"><span class="vm-tier advanced">ADV</span><div class="vm-icon">🌎</div><div class="vm-name">macOS Sequoia</div><div class="vm-specs">8 cores · 16 GB · 256 GB SSD</div></div>
1207
- <div class="vm-card"><span class="vm-tier enterprise">ENT</span><div class="vm-icon">🥽</div><div class="vm-name">visionOS 2</div><div class="vm-specs">8 cores · 16 GB · 128 GB SSD</div></div>
1208
- <div class="vm-card"><span class="vm-tier enterprise">ENT</span><div class="vm-icon">🥽</div><div class="vm-name">Meta Quest 3</div><div class="vm-specs">8 cores · 12 GB · 128 GB SSD</div></div>
1209
- <div class="vm-card"><span class="vm-tier free">FREE</span><div class="vm-icon">🐧</div><div class="vm-name">Alpine Linux</div><div class="vm-specs">1 core · 1 GB · 8 GB SSD</div></div>
1210
- <div class="vm-card"><span class="vm-tier standard">STD</span><div class="vm-icon">🐧</div><div class="vm-name">Fedora 41</div><div class="vm-specs">4 cores · 8 GB · 64 GB SSD</div></div>
1211
- </div>
1212
- </div>
1213
-
1214
- <div class="card" style="margin-top:1.25rem">
1215
- <div class="card-title">
1216
- <span class="dot" style="background:var(--accent-green)"></span>
1217
- Active VMs
1218
- <span style="margin-left:auto;font-size:0.72rem;color:var(--text-muted)">0 running</span>
1219
- </div>
1220
- <div class="vm-status-list">
1221
- <div style="text-align:center;padding:2rem;color:var(--text-muted);font-size:0.85rem">
1222
- <div style="font-size:2rem;margin-bottom:0.5rem">💤</div>
1223
- No active VMs. Click a template above to create one.
1224
- </div>
1225
- </div>
1200
+ <div class="card-title">${DASHBOARD_READ_MESSAGES.vmTitle}</div>
1201
+ <p role="status">${DASHBOARD_READ_MESSAGES.vmUnavailable}</p>
1202
+ <button class="lc-btn compact" type="button" onclick="showFixedToast(dashboardReadMessages.vmActionUnavailable,false)">${DASHBOARD_READ_MESSAGES.vmAvailability}</button>
1226
1203
  </div>
1227
1204
  </div>
1228
1205
 
1229
- <!-- Marketplace Panel (v14.0) -->
1230
- <div id="marketplace-content" class="fade-in" style="display:none; margin: 0 auto; max-width: 1100px; padding: 0 1rem;">
1206
+ <!-- Marketplace: never advertise an unverified catalog or payout terms. -->
1207
+ <div id="marketplace-content" class="fade-in" style="display:none;max-width:1100px;margin:auto;padding:0 1rem;">
1231
1208
  <div class="card">
1232
- <div class="card-title">
1233
- <span class="dot" style="background:var(--accent-cyan)"></span>
1234
- Component Marketplace 🛒
1235
- <button style="margin-left:auto;background:var(--gradient-hero);color:white;border:none;padding:0.35rem 0.85rem;border-radius:var(--radius-sm);font-size:0.78rem;font-weight:600;cursor:pointer">+ Publish Component</button>
1236
- </div>
1237
-
1238
- <div style="display:flex;gap:0.75rem;margin-bottom:1rem;align-items:center">
1239
- <input type="text" class="input-modern" style="flex:1;padding:0.6rem 1rem;font-size:0.9rem" placeholder="Search components, shaders, UI kits...">
1240
- </div>
1241
-
1242
- <div class="mp-filters">
1243
- <button class="mp-filter-chip active">All</button>
1244
- <button class="mp-filter-chip">🎨 UI</button>
1245
- <button class="mp-filter-chip">⚙️ Logic</button>
1246
- <button class="mp-filter-chip">🎧 Audio</button>
1247
- <button class="mp-filter-chip">✨ Shader</button>
1248
- <button class="mp-filter-chip">📄 Template</button>
1249
- <button class="mp-filter-chip">🎮 Game</button>
1250
- <button class="mp-filter-chip">🤖 AI/ML</button>
1251
- </div>
1252
-
1253
- <div class="mp-grid">
1254
- <div class="mp-card">
1255
- <div class="mp-header">
1256
- <div><div class="mp-name">🎨 Glassmorphism UI Kit</div><div class="mp-author">by @studioflow</div></div>
1257
- <span class="mp-price-badge free">FREE</span>
1258
- </div>
1259
- <div class="mp-desc">Premium glass-effect components with dark/light mode support. 24 components included.</div>
1260
- <button class="mp-install-btn">⬇ Install</button>
1261
- </div>
1262
- <div class="mp-card">
1263
- <div class="mp-header">
1264
- <div><div class="mp-name">✨ PBR Shader Pack</div><div class="mp-author">by @renderlab</div></div>
1265
- <span class="mp-price-badge paid">$14.99</span>
1266
- </div>
1267
- <div class="mp-desc">Physically-based rendering shaders for Metal, Vulkan, and WebGPU. Includes toon, cel, and water effects.</div>
1268
- <button class="mp-install-btn">🛒 Purchase</button>
1269
- </div>
1270
- <div class="mp-card">
1271
- <div class="mp-header">
1272
- <div><div class="mp-name">🎮 Netcode Toolkit</div><div class="mp-author">by @multiplayerlabs</div></div>
1273
- <span class="mp-price-badge paid">$29.99</span>
1274
- </div>
1275
- <div class="mp-desc">Client-side prediction, server reconciliation, and lag compensation for multiplayer games.</div>
1276
- <button class="mp-install-btn">🛒 Purchase</button>
1277
- </div>
1278
- <div class="mp-card">
1279
- <div class="mp-header">
1280
- <div><div class="mp-name">🤖 Edge AI Runtime</div><div class="mp-author">by @inferenceio</div></div>
1281
- <span class="mp-price-badge free">FREE</span>
1282
- </div>
1283
- <div class="mp-desc">On-device ML inference with CoreML, ONNX, and TFLite. Includes pose estimation and object detection models.</div>
1284
- <button class="mp-install-btn">⬇ Install</button>
1285
- </div>
1286
- <div class="mp-card">
1287
- <div class="mp-header">
1288
- <div><div class="mp-name">🎧 Spatial Audio Engine</div><div class="mp-author">by @soundscapevr</div></div>
1289
- <span class="mp-price-badge paid">$19.99</span>
1290
- </div>
1291
- <div class="mp-desc">HRTF-based 3D audio for VR/AR. Supports ambisonics, reverb zones, and occlusion.</div>
1292
- <button class="mp-install-btn">🛒 Purchase</button>
1293
- </div>
1294
- <div class="mp-card">
1295
- <div class="mp-header">
1296
- <div><div class="mp-name">📄 SaaS Starter Kit</div><div class="mp-author">by @launchfast</div></div>
1297
- <span class="mp-price-badge free">FREE</span>
1298
- </div>
1299
- <div class="mp-desc">Full-stack SaaS boilerplate with auth, billing, dashboard, and API. Next.js + Supabase.</div>
1300
- <button class="mp-install-btn">⬇ Install</button>
1301
- </div>
1302
- </div>
1303
-
1304
- <div style="margin-top:1.25rem;padding:0.85rem;border-radius:var(--radius-sm);background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);font-size:0.78rem;color:var(--text-secondary)">
1305
- 💰 <strong>Revenue Sharing:</strong> Earn 70% of sales on paid components. Payouts via Synalux every 30 days.
1209
+ <div class="card-title">${DASHBOARD_READ_MESSAGES.marketplaceTitle}</div>
1210
+ <p role="status">${DASHBOARD_READ_MESSAGES.marketplaceUnavailable}</p>
1211
+ <div class="lc-row">
1212
+ <button class="lc-btn compact" type="button" onclick="showFixedToast(dashboardReadMessages.marketplaceActionUnavailable,false)">${DASHBOARD_READ_MESSAGES.marketplaceAvailability}</button>
1213
+ <button class="lc-btn export" type="button" onclick="showFixedToast(dashboardReadMessages.marketplaceActionUnavailable,false)">${DASHBOARD_READ_MESSAGES.publishComponent}</button>
1306
1214
  </div>
1307
1215
  </div>
1308
1216
  </div>
1309
1217
 
1310
- <!-- Compliance Panel (v14.0) -->
1311
- <div id="compliance-content" class="fade-in" style="display:none; margin: 0 auto; max-width: 1100px; padding: 0 1rem;">
1218
+ <!-- Compliance: this view has no connected live enforcement/audit feed. -->
1219
+ <div id="compliance-content" class="fade-in" style="display:none;max-width:1100px;margin:auto;padding:0 1rem;">
1312
1220
  <div class="card">
1313
- <div class="card-title">
1314
- <span class="dot" style="background:var(--accent-green)"></span>
1315
- Enforcement Pipeline — 6 Layers 🛡️
1316
- </div>
1317
- <div class="compliance-layers">
1318
- <div class="compliance-layer">
1319
- <div class="cl-icon">📋</div>
1320
- <div class="cl-name">Registration Gate</div>
1321
- <div class="cl-status active">Active</div>
1322
- </div>
1323
- <div class="compliance-layer">
1324
- <div class="cl-icon">🌐</div>
1325
- <div class="cl-name">Geofence Gate</div>
1326
- <div class="cl-status active">Active</div>
1327
- </div>
1328
- <div class="compliance-layer">
1329
- <div class="cl-icon">🧠</div>
1330
- <div class="cl-name">Use-Case AI</div>
1331
- <div class="cl-status active">Active</div>
1332
- </div>
1333
- <div class="compliance-layer">
1334
- <div class="cl-icon">📊</div>
1335
- <div class="cl-name">Runtime Monitor</div>
1336
- <div class="cl-status active">Active</div>
1337
- </div>
1338
- <div class="compliance-layer">
1339
- <div class="cl-icon">🚨</div>
1340
- <div class="cl-name">Kill Switch</div>
1341
- <div class="cl-status active">Armed</div>
1342
- </div>
1343
- <div class="compliance-layer">
1344
- <div class="cl-icon">🔗</div>
1345
- <div class="cl-name">Audit Trail</div>
1346
- <div class="cl-status active">Hash-Chain</div>
1347
- </div>
1348
- </div>
1349
- </div>
1350
-
1351
- <div class="card" style="margin-top:1.25rem">
1352
- <div class="card-title">
1353
- <span class="dot" style="background:var(--accent-rose)"></span>
1354
- Embargoed Countries — Hard-Blocked
1355
- </div>
1356
- <div class="country-badges">
1357
- <span class="country-badge embargoed">⛔ BY — Belarus</span>
1358
- <span class="country-badge embargoed">⛔ CU — Cuba</span>
1359
- <span class="country-badge embargoed">⛔ IR — Iran</span>
1360
- <span class="country-badge embargoed">⛔ KP — DPRK</span>
1361
- <span class="country-badge embargoed">⛔ SY — Syria</span>
1362
- </div>
1363
-
1364
- <div class="card-title" style="margin-top:1rem">
1365
- <span class="dot" style="background:var(--accent-amber)"></span>
1366
- Restricted — Civilian Only
1367
- </div>
1368
- <div class="country-badges">
1369
- <span class="country-badge restricted">🟡 RU — Russia</span>
1370
- <span class="country-badge restricted">🟡 CN — China</span>
1371
- <span class="country-badge restricted">🟡 VE</span>
1372
- <span class="country-badge restricted">🟡 MM</span>
1373
- <span class="country-badge restricted">🟡 SD</span>
1374
- <span class="country-badge restricted">🟡 SS</span>
1375
- <span class="country-badge restricted">🟡 LY</span>
1376
- <span class="country-badge restricted">🟡 SO</span>
1377
- <span class="country-badge restricted">🟡 YE</span>
1378
- <span class="country-badge restricted">🟡 ZW</span>
1379
- <span class="country-badge restricted">🟡 CD</span>
1380
- <span class="country-badge restricted">🟡 CF</span>
1381
- <span class="country-badge restricted">🟡 IQ</span>
1382
- <span class="country-badge restricted">🟡 LB</span>
1383
- </div>
1384
- </div>
1385
-
1386
- <div class="card" style="margin-top:1.25rem">
1387
- <div class="card-title">
1388
- <span class="dot" style="background:var(--accent-purple)"></span>
1389
- Recent Audit Events 📜
1390
- </div>
1391
- <table class="audit-table">
1392
- <thead>
1393
- <tr><th>Time</th><th>Event</th><th>Severity</th><th>Details</th></tr>
1394
- </thead>
1395
- <tbody>
1396
- <tr>
1397
- <td style="font-family:var(--font-mono);font-size:0.72rem">2 min ago</td>
1398
- <td>Registration screened</td>
1399
- <td><span class="audit-severity low">Low</span></td>
1400
- <td style="color:var(--text-muted)">KYC passed — US-based individual</td>
1401
- </tr>
1402
- <tr>
1403
- <td style="font-family:var(--font-mono);font-size:0.72rem">18 min ago</td>
1404
- <td>Use-case classified</td>
1405
- <td><span class="audit-severity low">Low</span></td>
1406
- <td style="color:var(--text-muted)">Project "pet-tracker" — consumer app (2% risk)</td>
1407
- </tr>
1408
- <tr>
1409
- <td style="font-family:var(--font-mono);font-size:0.72rem">1 hr ago</td>
1410
- <td>Geofence mismatch</td>
1411
- <td><span class="audit-severity medium">Medium</span></td>
1412
- <td style="color:var(--text-muted)">IP: DE, Billing: FR — flagged for human review</td>
1413
- </tr>
1414
- <tr>
1415
- <td style="font-family:var(--font-mono);font-size:0.72rem">4 hr ago</td>
1416
- <td>VPN detected</td>
1417
- <td><span class="audit-severity medium">Medium</span></td>
1418
- <td style="color:var(--text-muted)">NordVPN exit node — billing triangulation passed</td>
1419
- </tr>
1420
- <tr>
1421
- <td style="font-family:var(--font-mono);font-size:0.72rem">1 day ago</td>
1422
- <td>Sanctions check failed</td>
1423
- <td><span class="audit-severity high">High</span></td>
1424
- <td style="color:var(--text-muted)">OFAC SDN match — registration blocked</td>
1425
- </tr>
1426
- </tbody>
1427
- </table>
1428
- </div>
1429
-
1430
- <div style="margin-top:1.25rem;padding:0.85rem;border-radius:var(--radius-sm);background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.2);font-size:0.78rem;color:var(--text-secondary)">
1431
- ✅ <strong>All 6 enforcement layers are active.</strong> Ethics enforcement is NOT tier-gated — applies equally to Free, the free account, Pro and Team.
1432
- <a href="/ethics" style="color:var(--accent-purple);margin-left:0.5rem">View full policy →</a>
1221
+ <div class="card-title">${DASHBOARD_READ_MESSAGES.complianceTitle}</div>
1222
+ <p role="status">${DASHBOARD_READ_MESSAGES.complianceUnavailable}</p>
1223
+ <p>${DASHBOARD_READ_MESSAGES.auditUnavailable}</p>
1224
+ <button class="lc-btn compact" type="button" onclick="showFixedToast(dashboardReadMessages.policyUnavailable,false)">${DASHBOARD_READ_MESSAGES.policyAvailability}</button>
1433
1225
  </div>
1434
1226
  </div>
1435
1227
 
@@ -1486,9 +1278,9 @@ export function renderDashboardHTML(version) {
1486
1278
  <div class="setting-desc">Default level for session_load_context</div>
1487
1279
  </div>
1488
1280
  <select class="setting-select" id="select-context-depth" onchange="saveSetting('default_context_depth', this.value)">
1489
- <option value="standard">Standard (~200 tokens)</option>
1490
- <option value="quick">Quick (~50 tokens)</option>
1491
- <option value="deep">Deep (~1000+ tokens)</option>
1281
+ <option value="standard">Standard</option>
1282
+ <option value="quick">Quick</option>
1283
+ <option value="deep">Deep</option>
1492
1284
  </select>
1493
1285
  </div>
1494
1286
 
@@ -1616,8 +1408,7 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1616
1408
  <button class="skill-clear-btn" onclick="clearCurrentSkill()">🗑️ Clear</button>
1617
1409
  </div>
1618
1410
  <div class="skill-hint">
1619
- Skills are auto-injected into <code>session_load_context</code> responses for this role.<br>
1620
- Use Markdown. Changes take effect immediately — no restart needed.
1411
+ ${DASHBOARD_READ_MESSAGES.customRoleInstructions}
1621
1412
  </div>
1622
1413
  </div><!-- /spanel-skills -->
1623
1414
 
@@ -1646,7 +1437,7 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1646
1437
  <div class="setting-row">
1647
1438
  <div>
1648
1439
  <div class="setting-label">Google API Key</div>
1649
- <div class="setting-desc">GOOGLE_API_KEY — required for Gemini text &amp; embeddings</div>
1440
+ <div class="setting-desc">${DASHBOARD_READ_MESSAGES.directGeminiKey}</div>
1650
1441
  </div>
1651
1442
  <input type="password" id="input-google-api-key"
1652
1443
  placeholder="AIza…"
@@ -1759,8 +1550,7 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1759
1550
  </div>
1760
1551
 
1761
1552
  <div style="margin-top:1rem;padding:0.6rem 0.8rem;background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);border-radius:6px;font-size:0.78rem;color:var(--text-secondary);line-height:1.5">
1762
- 💡 <strong>Cost-optimized setup:</strong> Text Provider → <code>Anthropic</code>, Embedding Provider → <code>OpenAI / Ollama</code>.<br>
1763
- Use Claude 3.5 Sonnet for reasoning &amp; <code>nomic-embed-text</code> (free, local) for embeddings.
1553
+ ${DASHBOARD_READ_MESSAGES.providerChoice}
1764
1554
  </div>
1765
1555
 
1766
1556
  <span class="setting-saved" id="savedToastProviders">Saved ✓</span>
@@ -1858,6 +1648,19 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1858
1648
  // This HTML is served as a raw template literal; mixing ES6 in the
1859
1649
  // inline script causes SyntaxError in some browser/context combos.
1860
1650
  // ═══════════════════════════════════════════════════════════════════
1651
+ var dashboardReadMessages = ${JSON.stringify(DASHBOARD_READ_MESSAGES)};
1652
+ var analyticsMetricIds = ${JSON.stringify(ANALYTICS_METRIC_IDS)};
1653
+ var analyticsReadSequence = 0;
1654
+ var pipelineReadSequence = 0;
1655
+ var projectReadSequence = 0;
1656
+ var retentionReadSequence = 0;
1657
+ var teamReadSequence = 0;
1658
+ var intentReadSequence = 0;
1659
+ var graphReadSequence = 0;
1660
+ var healthReadSequence = 0;
1661
+ var graphMetricsReadSequence = 0;
1662
+ var activeGraphNetwork = null;
1663
+ var analyticsDatePattern = new RegExp(${JSON.stringify(ANALYTICS_DATE_PATTERN.source)});
1861
1664
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
1862
1665
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
1863
1666
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -1924,20 +1727,28 @@ function switchMainTab(tabId) {
1924
1727
  if (tabId === 'factory') {
1925
1728
  loadPipelines();
1926
1729
  }
1730
+ if (tabId === 'project' && projectLoaded) loadGraph();
1927
1731
  }
1928
1732
  // ─── DARK FACTORY (v7.3) ───
1929
1733
  var factoryPollTimer = null;
1930
1734
  function loadPipelines() {
1735
+ var readSequence = ++pipelineReadSequence;
1736
+ document.getElementById('factoryCount').textContent = dashboardReadMessages.pipelinesLoading;
1931
1737
  var statusFilter = document.getElementById('factoryStatusFilter').value;
1932
1738
  var url = '/api/pipelines';
1933
1739
  if (statusFilter)
1934
1740
  url += '?status=' + encodeURIComponent(statusFilter);
1935
- fetch(url)
1936
- .then(function (r) { return r.json(); })
1741
+ return fetch(url)
1742
+ .then(function (r) {
1743
+ if (!r.ok) throw new Error(dashboardReadMessages.pipelinesUnavailable);
1744
+ return r.json();
1745
+ })
1937
1746
  .then(function (data) {
1747
+ if (readSequence !== pipelineReadSequence) return;
1748
+ if (!data || !Array.isArray(data.pipelines)) throw new Error(dashboardReadMessages.pipelinesUnavailable);
1938
1749
  var list = document.getElementById('factoryList');
1939
1750
  var count = document.getElementById('factoryCount');
1940
- var pipelines = data.pipelines || [];
1751
+ var pipelines = data.pipelines;
1941
1752
  count.textContent = pipelines.length + ' pipeline' + (pipelines.length !== 1 ? 's' : '');
1942
1753
  if (pipelines.length === 0) {
1943
1754
  list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted)"><div style="font-size:2rem;margin-bottom:0.5rem">🏭</div>No pipelines found. Use <code>session_start_pipeline</code> to create one.</div>';
@@ -1988,7 +1799,10 @@ function loadPipelines() {
1988
1799
  }
1989
1800
  })
1990
1801
  .catch(function (err) {
1991
- document.getElementById('factoryList').innerHTML = '<div style="color:var(--accent-rose);padding:1rem">Failed to load pipelines: ' + escapeHtml(err.message) + '</div>';
1802
+ if (readSequence !== pipelineReadSequence) return;
1803
+ document.getElementById('factoryCount').textContent = '—';
1804
+ document.getElementById('factoryList').innerHTML = '<div role="status" style="color:var(--accent-rose);padding:1rem">' + escapeHtml(dashboardReadMessages.pipelinesUnavailable) +
1805
+ ' <button class="refresh-btn" id="factoryRetryBtn" type="button" onclick="loadPipelines()">' + escapeHtml(dashboardReadMessages.retry) + '</button></div>';
1992
1806
  });
1993
1807
  }
1994
1808
  function abortPipeline(id) {
@@ -2446,7 +2260,62 @@ function signOutAccount() {
2446
2260
  });
2447
2261
  });
2448
2262
  })();
2263
+ function isCurrentProjectRead(project, sequence) {
2264
+ return document.getElementById('projectSelect').value === project
2265
+ && (typeof sequence === 'undefined' || sequence === projectReadSequence);
2266
+ }
2267
+ function applyHealthReport(data) {
2268
+ var partial = !!data && data.coverage === 'embedding_inventory';
2269
+ var totals = data && data.totals;
2270
+ var validStatus = data && (data.status === 'healthy' || data.status === 'degraded' || data.status === 'unhealthy');
2271
+ var validCoverage = data && data.scope === 'account'
2272
+ && (data.backend === 'synalux' && data.coverage === 'embedding_inventory'
2273
+ || (data.backend === 'local' || data.backend === 'supabase') && data.coverage === 'backend_scan');
2274
+ var available = !!validStatus && validCoverage && totals && typeof totals.activeEntries === 'number' && isFinite(totals.activeEntries) && totals.activeEntries >= 0
2275
+ && typeof totals.handoffs === 'number' && isFinite(totals.handoffs) && totals.handoffs >= 0
2276
+ && (partial || (typeof totals.rollups === 'number' && isFinite(totals.rollups) && totals.rollups >= 0)) && Array.isArray(data.issues)
2277
+ && data.issues.every(function (issue) { return issue && typeof issue.message === 'string'; });
2278
+ var issues = available ? data.issues : [];
2279
+ document.getElementById('healthCard').style.display = 'block';
2280
+ document.getElementById('healthDot').className = 'health-dot ' + (available && !partial ? data.status : 'unknown');
2281
+ document.getElementById('healthLabel').textContent = !available ? dashboardReadMessages.scanUnavailable
2282
+ : partial ? dashboardReadMessages.partialScan
2283
+ : data.status === 'healthy' ? dashboardReadMessages.scanPassed : dashboardReadMessages.scanIssues;
2284
+ document.getElementById('healthSummary').textContent = available
2285
+ ? totals.activeEntries + ' entries · ' + totals.handoffs + ' handoffs' + (partial ? '' : ' · ' + totals.rollups + ' rollups')
2286
+ + (!partial && typeof totals.crdtMerges === 'number' && totals.crdtMerges > 0 ? ' · ' + totals.crdtMerges + ' merges' : '')
2287
+ : '—';
2288
+ document.getElementById('healthCoverageNote').textContent = !available ? dashboardReadMessages.coverageUnavailable
2289
+ : partial ? dashboardReadMessages.partialCoverage : dashboardReadMessages.backendCoverage;
2290
+ var issueContainer = document.getElementById('healthIssues');
2291
+ issueContainer.textContent = !available ? dashboardReadMessages.scanUnavailable
2292
+ : issues.length ? '' : partial ? dashboardReadMessages.noEmbeddingIssues : dashboardReadMessages.noScanIssues;
2293
+ issues.forEach(function (issue) {
2294
+ var row = document.createElement('div'); row.className = 'issue-row'; row.textContent = issue.message;
2295
+ issueContainer.appendChild(row);
2296
+ });
2297
+ var actionable = issues.some(function (issue) {
2298
+ return (issue.check === 'missing_embeddings' || issue.check === 'orphaned_handoffs') && issue.count > 0;
2299
+ });
2300
+ document.getElementById('cleanupBtn').style.display = actionable ? 'inline-block' : 'none';
2301
+ }
2302
+ function refreshHealthScan() {
2303
+ var sequence = ++healthReadSequence;
2304
+ applyHealthReport(null);
2305
+ document.getElementById('healthLabel').textContent = dashboardReadMessages.scanLoading;
2306
+ document.getElementById('healthIssues').textContent = '';
2307
+ return fetch('/api/health').then(function (response) {
2308
+ if (!response.ok) throw new Error(dashboardReadMessages.scanUnavailable);
2309
+ return response.json();
2310
+ }).then(function (data) {
2311
+ if (sequence === healthReadSequence) applyHealthReport(data);
2312
+ }).catch(function () {
2313
+ if (sequence === healthReadSequence) applyHealthReport(null);
2314
+ });
2315
+ }
2449
2316
  function loadProject() {
2317
+ var readSequence = ++projectReadSequence;
2318
+ projectLoaded = false;
2450
2319
  return __awaiter(this, void 0, void 0, function () {
2451
2320
  var project, res, data, ctx, ledgerEntries, latestLedger, contextTime, latestLedgerTime, useLatestLedger, versionBadge, currentStateSource, currentSummary, todos, todoList, meta, briefingCard, visualCard, visuals, historyEl, ledgerEl, healthRes, healthData, healthCard, healthDot, healthLabel, healthSummary, healthIssues, statusMap, t, issues, cleanupBtn, sevIcons, he_1, e_3;
2452
2321
  return __generator(this, function (_a) {
@@ -2454,6 +2323,7 @@ function loadProject() {
2454
2323
  case 0:
2455
2324
  project = document.getElementById('projectSelect').value;
2456
2325
  if (!project) {
2326
+ document.getElementById('loading').style.display = 'none';
2457
2327
  var hc = document.getElementById('intentHealthCard');
2458
2328
  if (hc) hc.style.display = 'none';
2459
2329
  var welcomeEl = document.getElementById('welcome');
@@ -2476,6 +2346,8 @@ function loadProject() {
2476
2346
  return [4 /*yield*/, res.json()];
2477
2347
  case 3:
2478
2348
  data = _a.sent();
2349
+ if (!isCurrentProjectRead(project, readSequence)) return [2 /*return*/];
2350
+ if (!res.ok || !data || data.error) throw new Error(dashboardReadMessages.projectUnavailable);
2479
2351
  ctx = data.context || {};
2480
2352
  ledgerEntries = Array.isArray(data.ledger) ? data.ledger.slice() : [];
2481
2353
  ledgerEntries.sort(function (a, b) {
@@ -2562,6 +2434,8 @@ function loadProject() {
2562
2434
  ledgerEl = document.getElementById('ledgerTimeline');
2563
2435
  if (ledgerEntries.length > 0) {
2564
2436
  ledgerEl.innerHTML = ledgerEntries.map(function (l) {
2437
+ var entryKind = l.is_rollup === true || l.is_rollup === 1 || l.is_rollup === 'true'
2438
+ ? dashboardReadMessages.rollupEntry : dashboardReadMessages.sessionEntry;
2565
2439
  var summary = l.summary || l.content || 'Entry';
2566
2440
  var decisions = l.decisions;
2567
2441
  var extra = '';
@@ -2575,7 +2449,7 @@ function loadProject() {
2575
2449
  catch (e) { }
2576
2450
  }
2577
2451
  return '<div class="timeline-item">' +
2578
- '<div class="meta"><span class="badge badge-amber">session</span>' +
2452
+ '<div class="meta"><span class="badge badge-amber">' + entryKind + '</span>' +
2579
2453
  '<span>' + formatDate(l.created_at) + '</span></div>' +
2580
2454
  escapeHtml(summary) + extra + '</div>';
2581
2455
  }).join('');
@@ -2585,56 +2459,16 @@ function loadProject() {
2585
2459
  }
2586
2460
  _a.label = 4;
2587
2461
  case 4:
2588
- _a.trys.push([4, 7, , 8]);
2589
- return [4 /*yield*/, fetch('/api/health')];
2462
+ return [4 /*yield*/, refreshHealthScan()];
2590
2463
  case 5:
2591
- healthRes = _a.sent();
2592
- return [4 /*yield*/, healthRes.json()];
2593
- case 6:
2594
- healthData = _a.sent();
2595
- healthCard = document.getElementById('healthCard');
2596
- healthDot = document.getElementById('healthDot');
2597
- healthLabel = document.getElementById('healthLabel');
2598
- healthSummary = document.getElementById('healthSummary');
2599
- healthIssues = document.getElementById('healthIssues');
2600
- // Set the dot color based on status
2601
- healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
2602
- statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
2603
- healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
2604
- t = healthData.totals || {};
2605
- healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' +
2606
- (t.handoffs || 0) + ' handoffs · ' +
2607
- (t.rollups || 0) + ' rollups' +
2608
- (t.crdtMerges ? ' · 🔄 ' + t.crdtMerges + ' merges' : '');
2609
- issues = healthData.issues || [];
2610
- cleanupBtn = document.getElementById('cleanupBtn');
2611
- if (issues.length > 0) {
2612
- sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
2613
- healthIssues.innerHTML = issues.map(function (i) {
2614
- return '<div class="issue-row">' +
2615
- '<span>' + (sevIcons[i.severity] || '❓') + '</span>' +
2616
- '<span>' + escapeHtml(i.message) + '</span>' +
2617
- '</div>';
2618
- }).join('');
2619
- if (cleanupBtn)
2620
- cleanupBtn.style.display = 'inline-block';
2621
- }
2622
- else {
2623
- healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
2624
- if (cleanupBtn)
2625
- cleanupBtn.style.display = 'none';
2626
- }
2627
- healthCard.style.display = 'block';
2628
- return [3 /*break*/, 8];
2629
- case 7:
2630
- he_1 = _a.sent();
2631
- // Health check not available — silently skip
2632
- console.warn('Health check unavailable:', he_1);
2464
+ _a.sent();
2633
2465
  return [3 /*break*/, 8];
2634
2466
  case 8:
2467
+ if (!isCurrentProjectRead(project, readSequence)) return [2 /*return*/];
2635
2468
  document.getElementById('content').className = 'grid grid-main fade-in';
2636
2469
  document.getElementById('content').style.display = 'grid';
2637
2470
  projectLoaded = true; // Fix 3: mark project as loaded for tab-switch restore
2471
+ loadGraph(); // Fit the network after the previously hidden panel becomes visible.
2638
2472
 
2639
2473
  // Feature A: Run Intent Health Fetch
2640
2474
  fetchIntentHealth(project);
@@ -2649,10 +2483,10 @@ function loadProject() {
2649
2483
  return [3 /*break*/, 11];
2650
2484
  case 9:
2651
2485
  e_3 = _a.sent();
2652
- alert('Failed to load project data: ' + e_3.message);
2486
+ if (isCurrentProjectRead(project, readSequence)) alert(dashboardReadMessages.projectUnavailable);
2653
2487
  return [3 /*break*/, 11];
2654
2488
  case 10:
2655
- document.getElementById('loading').style.display = 'none';
2489
+ if (isCurrentProjectRead(project, readSequence)) document.getElementById('loading').style.display = 'none';
2656
2490
  return [7 /*endfinally*/];
2657
2491
  case 11: return [2 /*return*/];
2658
2492
  }
@@ -2661,6 +2495,14 @@ function loadProject() {
2661
2495
  }
2662
2496
  // ─── v3.1: Memory Analytics ───────────────────────────────────────────────
2663
2497
  function loadAnalytics(project) {
2498
+ if (!isCurrentProjectRead(project)) return Promise.resolve();
2499
+ var readSequence = ++analyticsReadSequence;
2500
+ var parentSequence = projectReadSequence;
2501
+ var readStatus = document.getElementById('analyticsReadStatus');
2502
+ analyticsMetricIds.forEach(function (id) { document.getElementById(id).textContent = '—'; });
2503
+ document.getElementById('sparkline').innerHTML = '';
2504
+ readStatus.textContent = dashboardReadMessages.analyticsLoading;
2505
+ readStatus.style.display = 'block';
2664
2506
  return __awaiter(this, void 0, void 0, function () {
2665
2507
  var res, d, sparkEl, days, maxCount, e_4;
2666
2508
  return __generator(this, function (_a) {
@@ -2673,6 +2515,20 @@ function loadAnalytics(project) {
2673
2515
  return [4 /*yield*/, res.json()];
2674
2516
  case 2:
2675
2517
  d = _a.sent();
2518
+ if (readSequence !== analyticsReadSequence || !isCurrentProjectRead(project, parentSequence)) return [2 /*return*/];
2519
+ if (!res.ok || !d || d.error || typeof d.totalEntries !== 'number'
2520
+ || typeof d.totalRollups !== 'number' || typeof d.rollupSavings !== 'number'
2521
+ || typeof d.avgSummaryLength !== 'number' || !isFinite(d.totalEntries)
2522
+ || !isFinite(d.totalRollups) || !isFinite(d.rollupSavings) || !isFinite(d.avgSummaryLength)
2523
+ || d.totalEntries < 0 || d.totalRollups < 0 || d.rollupSavings < 0 || d.avgSummaryLength < 0
2524
+ || !Array.isArray(d.sessionsByDay) || d.sessionsByDay.some(function (day) {
2525
+ return !day || typeof day.date !== 'string' || !analyticsDatePattern.test(day.date)
2526
+ || typeof day.count !== 'number' || !isFinite(day.count) || day.count < 0;
2527
+ })) {
2528
+ throw new Error(dashboardReadMessages.analyticsUnavailable);
2529
+ }
2530
+ readStatus.textContent = '';
2531
+ readStatus.style.display = 'none';
2676
2532
  document.getElementById('astat-entries').textContent = (d.totalEntries || 0);
2677
2533
  document.getElementById('astat-rollups').textContent = (d.totalRollups || 0);
2678
2534
  document.getElementById('astat-savings').textContent = (d.rollupSavings || 0);
@@ -2683,18 +2539,23 @@ function loadAnalytics(project) {
2683
2539
  // Pad with 14 zero days
2684
2540
  days = Array.from({ length: 14 }, function (_, i) {
2685
2541
  var dt = new Date();
2686
- dt.setDate(dt.getDate() - (13 - i));
2542
+ dt.setUTCDate(dt.getUTCDate() - (13 - i));
2687
2543
  return { date: dt.toISOString().slice(0, 10), count: 0 };
2688
2544
  });
2689
2545
  }
2690
2546
  maxCount = Math.max.apply(null, days.map(function (x) { return x.count || 0; })) || 1;
2691
2547
  sparkEl.innerHTML = days.slice(-14).map(function (d) {
2692
2548
  var pct = Math.max(4, Math.round(((d.count || 0) / maxCount) * 100));
2693
- return '<div class="spark-bar" style="height:' + pct + '%" title="' + d.date + ': ' + d.count + '"></div>';
2549
+ return '<div class="spark-bar" style="height:' + pct + '%" title="' + escapeHtml(d.date) + ': ' + d.count + '"></div>';
2694
2550
  }).join('');
2695
2551
  return [3 /*break*/, 4];
2696
2552
  case 3:
2697
2553
  e_4 = _a.sent();
2554
+ if (readSequence !== analyticsReadSequence || !isCurrentProjectRead(project, parentSequence)) return [2 /*return*/];
2555
+ analyticsMetricIds.forEach(function (id) { document.getElementById(id).textContent = '—'; });
2556
+ document.getElementById('sparkline').innerHTML = '';
2557
+ readStatus.textContent = dashboardReadMessages.analyticsUnavailable;
2558
+ readStatus.style.display = 'block';
2698
2559
  console.warn('Analytics load failed:', e_4);
2699
2560
  return [3 /*break*/, 4];
2700
2561
  case 4: return [2 /*return*/];
@@ -2704,6 +2565,9 @@ function loadAnalytics(project) {
2704
2565
  }
2705
2566
  // ─── v3.1: TTL Retention ───────────────────────────────────────────────
2706
2567
  function loadRetention(project) {
2568
+ if (!isCurrentProjectRead(project)) return Promise.resolve();
2569
+ var readSequence = ++retentionReadSequence;
2570
+ var parentSequence = projectReadSequence;
2707
2571
  return __awaiter(this, void 0, void 0, function () {
2708
2572
  var res, d, inp, e_5;
2709
2573
  return __generator(this, function (_a) {
@@ -2716,6 +2580,7 @@ function loadRetention(project) {
2716
2580
  return [4 /*yield*/, res.json()];
2717
2581
  case 2:
2718
2582
  d = _a.sent();
2583
+ if (readSequence !== retentionReadSequence || !isCurrentProjectRead(project, parentSequence)) return [2 /*return*/];
2719
2584
  inp = document.getElementById('ttlInput');
2720
2585
  if (inp)
2721
2586
  inp.value = d.ttl_days || 0;
@@ -3244,7 +3109,29 @@ function getDecayColor(daysSince, decayedImportance, group, baseImportance) {
3244
3109
  return { bg: hex, border: hex, fontColor: fontBrightness };
3245
3110
  }
3246
3111
  // ─── Neural Graph (v2.3.0 / v5.1 / v6.2 Decay Heatmap) ───
3112
+ function graphStatisticsElement(container) {
3113
+ var title = container.parentElement.querySelector('.card-title');
3114
+ var stats = title.querySelector('.graph-stats');
3115
+ if (!stats) {
3116
+ stats = document.createElement('span');
3117
+ stats.className = 'graph-stats';
3118
+ stats.style.cssText = 'margin-left:auto;font-size:0.7rem;color:var(--text-muted);font-family:var(--font-mono);font-weight:400;text-transform:none;letter-spacing:0';
3119
+ title.appendChild(stats);
3120
+ }
3121
+ return stats;
3122
+ }
3123
+ function updateGraphStatistics(container, data) {
3124
+ var nodes = data.nodes;
3125
+ var projects = nodes.filter(function (node) { return node.group === 'project'; }).length;
3126
+ var keywords = nodes.filter(function (node) { return node.group === 'keyword'; }).length;
3127
+ graphStatisticsElement(container).textContent = data.graphType === 'memory'
3128
+ ? nodes.length + ' ' + dashboardReadMessages.graphSessions + ' · ' + data.edges.length + ' ' + dashboardReadMessages.graphStoredLinks
3129
+ : projects + ' ' + dashboardReadMessages.graphProjects + ' · ' + keywords + ' ' + dashboardReadMessages.graphKeywords + ' · ' + data.edges.length + ' ' + dashboardReadMessages.graphEdges;
3130
+ }
3247
3131
  function loadGraph() {
3132
+ var readSequence = ++graphReadSequence;
3133
+ if (activeGraphNetwork && typeof activeGraphNetwork.destroy === 'function') activeGraphNetwork.destroy();
3134
+ activeGraphNetwork = null;
3248
3135
  return __awaiter(this, void 0, void 0, function () {
3249
3136
  var container, proj, days, imp, qs, url, res, data, graphDataNote, dens, graduatedNodes, denPercentage, dens, MAX_NODES, priority, kept, options, network, allNodes, allEdges, isFiltered, graphTitle, statsSpan, projectCount, kwCount, e_11;
3250
3137
  return __generator(this, function (_a) {
@@ -3253,6 +3140,10 @@ function loadGraph() {
3253
3140
  container = document.getElementById('network-container');
3254
3141
  if (!container)
3255
3142
  return [2 /*return*/];
3143
+ graphStatisticsElement(container).textContent = dashboardReadMessages.graphLoading;
3144
+ container.textContent = dashboardReadMessages.graphLoading;
3145
+ document.getElementById('densityStatContainer').style.display = 'none';
3146
+ document.getElementById('graphDataNote').style.display = 'none';
3256
3147
  proj = document.getElementById('graphProjectFilter') ? document.getElementById('graphProjectFilter').value : '';
3257
3148
  days = document.getElementById('graphDaysFilter') ? document.getElementById('graphDaysFilter').value : '';
3258
3149
  imp = document.getElementById('graphImportanceFilter') ? document.getElementById('graphImportanceFilter').value : '';
@@ -3273,8 +3164,9 @@ function loadGraph() {
3273
3164
  return [4 /*yield*/, res.json()];
3274
3165
  case 3:
3275
3166
  data = _a.sent();
3276
- if (!res.ok || data.error)
3277
- throw new Error(data.error || 'Unable to load graph');
3167
+ if (readSequence !== graphReadSequence) return [2 /*return*/];
3168
+ if (!res.ok || !data || data.error || !Array.isArray(data.nodes) || !Array.isArray(data.edges))
3169
+ throw new Error(dashboardReadMessages.graphUnavailable);
3278
3170
  graphDataNote = document.getElementById('graphDataNote');
3279
3171
  if (graphDataNote) {
3280
3172
  graphDataNote.style.display = data.graphType === 'memory' ? 'block' : 'none';
@@ -3284,7 +3176,8 @@ function loadGraph() {
3284
3176
  }
3285
3177
  // Empty state — no ledger entries yet
3286
3178
  if (data.nodes.length === 0) {
3287
- container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:0.85rem">No knowledge associations found yet.</div>';
3179
+ updateGraphStatistics(container, data);
3180
+ container.textContent = dashboardReadMessages.graphEmpty;
3288
3181
  dens = document.getElementById('densityStatContainer');
3289
3182
  if (dens)
3290
3183
  dens.style.display = 'none';
@@ -3370,6 +3263,7 @@ function loadGraph() {
3370
3263
  interaction: { hover: true, tooltipDelay: 200 }
3371
3264
  };
3372
3265
  network = new vis.Network(container, data, options);
3266
+ activeGraphNetwork = network;
3373
3267
  allNodes = data.nodes;
3374
3268
  allEdges = data.edges;
3375
3269
  isFiltered = false;
@@ -3441,26 +3335,14 @@ function loadGraph() {
3441
3335
  network.setData({ nodes: allNodes, edges: allEdges });
3442
3336
  isFiltered = false;
3443
3337
  });
3444
- graphTitle = container.parentElement.querySelector('.card-title');
3445
- if (graphTitle) {
3446
- statsSpan = graphTitle.querySelector('.graph-stats');
3447
- if (!statsSpan) {
3448
- statsSpan = document.createElement('span');
3449
- statsSpan.className = 'graph-stats';
3450
- statsSpan.style.cssText = 'margin-left:auto;font-size:0.7rem;color:var(--text-muted);font-family:var(--font-mono);font-weight:400;text-transform:none;letter-spacing:0';
3451
- graphTitle.appendChild(statsSpan);
3452
- }
3453
- projectCount = allNodes.filter(function (n) { return n.group === 'project'; }).length;
3454
- kwCount = allNodes.filter(function (n) { return n.group === 'keyword'; }).length;
3455
- statsSpan.textContent = data.graphType === 'memory'
3456
- ? allNodes.length + ' sessions · ' + allEdges.length + ' stored links'
3457
- : projectCount + ' projects · ' + kwCount + ' keywords · ' + allEdges.length + ' edges';
3458
- }
3338
+ updateGraphStatistics(container, data);
3459
3339
  return [3 /*break*/, 5];
3460
3340
  case 4:
3461
3341
  e_11 = _a.sent();
3342
+ if (readSequence !== graphReadSequence) return [2 /*return*/];
3462
3343
  console.error('Graph error', e_11);
3463
- container.innerHTML = '<div style="padding:1rem;color:var(--accent-rose)">Graph failed to load</div>';
3344
+ graphStatisticsElement(container).textContent = dashboardReadMessages.graphUnavailable;
3345
+ container.textContent = dashboardReadMessages.graphUnavailable;
3464
3346
  return [3 /*break*/, 5];
3465
3347
  case 5: return [2 /*return*/];
3466
3348
  }
@@ -4276,6 +4158,8 @@ function showToast(msg) {
4276
4158
  // ─── Hivemind Radar (v5.3 — Health Watchdog) ───
4277
4159
  var hivemindRefreshTimer = null;
4278
4160
  function loadTeam() {
4161
+ var readSequence = ++teamReadSequence;
4162
+ var parentSequence = projectReadSequence;
4279
4163
  return __awaiter(this, void 0, void 0, function () {
4280
4164
  var project, card, res, data, team, list, roleIcons, statusColors, statusLabels, healthyCt, warnCt, summary, e_22;
4281
4165
  return __generator(this, function (_a) {
@@ -4294,6 +4178,7 @@ function loadTeam() {
4294
4178
  return [4 /*yield*/, res.json()];
4295
4179
  case 3:
4296
4180
  data = _a.sent();
4181
+ if (readSequence !== teamReadSequence || !isCurrentProjectRead(project, parentSequence)) return [2 /*return*/];
4297
4182
  team = data.team || [];
4298
4183
  list = document.getElementById('teamList');
4299
4184
  if (team.length > 0) {
@@ -4442,6 +4327,7 @@ function loadSchedulerStatus() {
4442
4327
  });
4443
4328
  }
4444
4329
  function loadGraphMetrics() {
4330
+ var readSequence = ++graphMetricsReadSequence;
4445
4331
  return __awaiter(this, void 0, void 0, function () {
4446
4332
  var el, warn, res, m, parts, synthStatus, tmStatus, pruneRatio, pruneSkipParts, rate, ratePct, rateColor, netNew, netColor, netSign, pruneRatioPct, cogTotal, autoP, clarP, fallP, ambPct, ambColor, fbPct, fbColor, lastRoute, lastConcept, lastConf, badges, e_24;
4447
4333
  return __generator(this, function (_a) {
@@ -4457,9 +4343,11 @@ function loadGraphMetrics() {
4457
4343
  return [4 /*yield*/, fetch('/api/graph/metrics')];
4458
4344
  case 2:
4459
4345
  res = _a.sent();
4346
+ if (!res.ok) throw new Error(dashboardReadMessages.graphUnavailable);
4460
4347
  return [4 /*yield*/, res.json()];
4461
4348
  case 3:
4462
4349
  m = _a.sent();
4350
+ if (readSequence !== graphMetricsReadSequence) return [2 /*return*/];
4463
4351
  parts = [];
4464
4352
  // Synthesis row
4465
4353
  parts.push('<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.3rem;margin-bottom:0.5rem">');
@@ -4530,23 +4418,26 @@ function loadGraphMetrics() {
4530
4418
  // SLO derivations row (WS4)
4531
4419
  if (m.slo) {
4532
4420
  parts.push('<div style="border-top:1px solid var(--border-glass);padding-top:0.4rem;margin-top:0.2rem;font-size:0.75rem">');
4533
- parts.push('<strong>SLO</strong>');
4421
+ parts.push('<strong>' + escapeHtml(dashboardReadMessages.recordedResults) + '</strong>');
4422
+ if (m.scheduler && m.scheduler.last_sweep_at) {
4423
+ parts.push('<br>' + escapeHtml(dashboardReadMessages.schedulerOutcome) + ': ' + m.scheduler.projects_succeeded_last +
4424
+ ' ' + dashboardReadMessages.schedulerSucceeded + ' · ' + m.scheduler.projects_failed_last + ' ' + dashboardReadMessages.schedulerFailed + ' · ' + m.scheduler.projects_processed_last + ' ' + dashboardReadMessages.schedulerAttempted);
4425
+ }
4534
4426
  // Synthesis success rate — color-coded
4535
4427
  if (m.slo.synthesis_success_rate !== null) {
4536
4428
  rate = m.slo.synthesis_success_rate;
4537
4429
  ratePct = Math.round(rate * 100);
4538
4430
  rateColor = rate >= 0.95 ? 'var(--accent-green)' : rate >= 0.80 ? 'var(--accent-amber)' : 'var(--accent-rose)';
4539
- parts.push('<br>Success rate: <span style="color:' + rateColor + ';font-weight:600">' + ratePct + '%</span>');
4431
+ parts.push('<br>' + escapeHtml(dashboardReadMessages.recordedSuccess) + ': <span style="color:' + rateColor + ';font-weight:600">' + ratePct + '%</span>');
4540
4432
  }
4541
4433
  else {
4542
- parts.push('<br>Success rate: <span style="color:var(--text-muted)">—</span>');
4434
+ parts.push('<br>' + escapeHtml(dashboardReadMessages.recordedSuccess) + ': <span style="color:var(--text-muted)">—</span>');
4543
4435
  }
4544
- netNew = m.slo.net_new_links_last_sweep;
4545
- netColor = netNew > 0 ? 'var(--accent-green)' : netNew < 0 ? 'var(--accent-rose)' : 'var(--text-muted)';
4546
- netSign = netNew > 0 ? '+' : '';
4547
- parts.push(' · Net links: <span style="color:' + netColor + '">' + netSign + netNew + '</span>');
4548
- pruneRatioPct = Math.round(m.slo.prune_ratio_last_sweep * 100);
4549
- parts.push(' · Prune: ' + pruneRatioPct + '%');
4436
+ parts.push('<br>' + escapeHtml(dashboardReadMessages.lastDirectLinks) + ': ' +
4437
+ (m.synthesis && m.synthesis.last_run_at ? m.synthesis.last_links_created : '—'));
4438
+ parts.push(' · ' + escapeHtml(dashboardReadMessages.lastPrune) + ': ' +
4439
+ (m.pruning && m.pruning.last_run_at && m.pruning.links_scanned_last > 0
4440
+ ? Math.round(m.slo.prune_ratio_last_sweep * 100) + '%' : '—'));
4550
4441
  // Sweep duration
4551
4442
  if (m.slo.scheduler_sweep_duration_ms_last > 0) {
4552
4443
  parts.push(' · Sweep: ' + m.slo.scheduler_sweep_duration_ms_last + 'ms');
@@ -4625,7 +4516,9 @@ function loadGraphMetrics() {
4625
4516
  return [3 /*break*/, 5];
4626
4517
  case 4:
4627
4518
  e_24 = _a.sent();
4519
+ if (readSequence !== graphMetricsReadSequence) return [2 /*return*/];
4628
4520
  el.innerHTML = '<div style="color:var(--text-muted)">Graph metrics unavailable</div>';
4521
+ if (warn) warn.textContent = '';
4629
4522
  return [3 /*break*/, 5];
4630
4523
  case 5: return [2 /*return*/];
4631
4524
  }
@@ -4765,60 +4658,7 @@ function cleanupIssues() {
4765
4658
  btn.textContent = '🧹 Fix Issues';
4766
4659
  }
4767
4660
  // Re-run health check to refresh the card
4768
- setTimeout(function () {
4769
- return __awaiter(this, void 0, void 0, function () {
4770
- var healthRes, healthData, healthDot, healthLabel, healthSummary, healthIssues, cleanupBtn, statusMap, t, issues, sevIcons, e_27;
4771
- return __generator(this, function (_a) {
4772
- switch (_a.label) {
4773
- case 0:
4774
- _a.trys.push([0, 3, , 4]);
4775
- return [4 /*yield*/, fetch('/api/health')];
4776
- case 1:
4777
- healthRes = _a.sent();
4778
- return [4 /*yield*/, healthRes.json()];
4779
- case 2:
4780
- healthData = _a.sent();
4781
- healthDot = document.getElementById('healthDot');
4782
- healthLabel = document.getElementById('healthLabel');
4783
- healthSummary = document.getElementById('healthSummary');
4784
- healthIssues = document.getElementById('healthIssues');
4785
- cleanupBtn = document.getElementById('cleanupBtn');
4786
- statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
4787
- healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
4788
- healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
4789
- t = healthData.totals || {};
4790
- healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' + (t.handoffs || 0) + ' handoffs · ' + (t.rollups || 0) + ' rollups' + (t.crdtMerges ? ' · 🔄 ' + t.crdtMerges + ' merges' : '');
4791
- issues = healthData.issues || [];
4792
- if (issues.length > 0) {
4793
- sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
4794
- healthIssues.innerHTML = issues.map(function (i) {
4795
- return '<div class="issue-row"><span>' + (sevIcons[i.severity] || '❓') + '</span><span>' + escapeHtml(i.message) + '</span></div>';
4796
- }).join('');
4797
- if (cleanupBtn) {
4798
- cleanupBtn.disabled = false;
4799
- cleanupBtn.textContent = '🧹 Fix Issues';
4800
- cleanupBtn.style.display = 'inline-block';
4801
- }
4802
- }
4803
- else {
4804
- healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
4805
- if (cleanupBtn)
4806
- cleanupBtn.style.display = 'none';
4807
- }
4808
- return [3 /*break*/, 4];
4809
- case 3:
4810
- e_27 = _a.sent();
4811
- // Health re-check failed — ensure button is usable
4812
- if (btn) {
4813
- btn.disabled = false;
4814
- btn.textContent = '🧹 Fix Issues';
4815
- }
4816
- return [3 /*break*/, 4];
4817
- case 4: return [2 /*return*/];
4818
- }
4819
- });
4820
- });
4821
- }, 400);
4661
+ setTimeout(function () { refreshHealthScan(); }, 400);
4822
4662
  return [3 /*break*/, 5];
4823
4663
  case 4:
4824
4664
  e_26 = _a.sent();
@@ -4876,6 +4716,9 @@ if ('serviceWorker' in navigator) {
4876
4716
  /* --- INTENT HEALTH COMPONENT (ES5 Safe) --- */
4877
4717
 
4878
4718
  function fetchIntentHealth(project) {
4719
+ if (!isCurrentProjectRead(project)) return;
4720
+ var readSequence = ++intentReadSequence;
4721
+ var parentSequence = projectReadSequence;
4879
4722
  var container = document.getElementById('intentHealthCardContent');
4880
4723
  var card = document.getElementById('intentHealthCard');
4881
4724
  if (!container || !card) return;
@@ -4886,6 +4729,7 @@ function fetchIntentHealth(project) {
4886
4729
  fetch('/api/intent-health?project=' + encodeURIComponent(project))
4887
4730
  .then(function(res) { return res.json(); })
4888
4731
  .then(function(data) {
4732
+ if (readSequence !== intentReadSequence || !isCurrentProjectRead(project, parentSequence)) return;
4889
4733
  if (data.error) {
4890
4734
  container.innerHTML = '<div style="color: #ff7b72; padding: 16px;">Error: ' + escapeHtml(data.error) + '</div>';
4891
4735
  return;
@@ -4893,6 +4737,7 @@ function fetchIntentHealth(project) {
4893
4737
  renderIntentHealthCard(data);
4894
4738
  })
4895
4739
  .catch(function(err) {
4740
+ if (readSequence !== intentReadSequence || !isCurrentProjectRead(project, parentSequence)) return;
4896
4741
  container.innerHTML = '<div style="color: #ff7b72; padding: 16px;">Failed to load intent health.</div>';
4897
4742
  });
4898
4743
  }