fastrmemory 0.3.0 → 0.3.1

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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +14 -14
  2. package/.claude-plugin/plugin.json +18 -18
  3. package/.cursor/mcp.json +3 -1
  4. package/.cursor/skills/fastr-compress/SKILL.md +33 -33
  5. package/.cursor/skills/fastr-curate/SKILL.md +62 -62
  6. package/.cursor/skills/fastr-dag/SKILL.md +47 -47
  7. package/.cursor/skills/fastr-doctor/SKILL.md +39 -39
  8. package/.cursor/skills/fastr-memory/SKILL.md +51 -51
  9. package/.cursor/skills/fastr-plan-estimate/SKILL.md +45 -45
  10. package/.cursor/skills/fastr-session-commit/SKILL.md +49 -49
  11. package/.cursor/skills/fastr-setup/SKILL.md +45 -45
  12. package/.cursor/skills/fastr-trap-log/SKILL.md +39 -39
  13. package/.cursor/skills/fastr-vision/SKILL.md +35 -35
  14. package/.cursor/skills/skills.json +65 -65
  15. package/README.md +727 -729
  16. package/bin/fastr-guard.mjs +61 -0
  17. package/dist/bin/fastr.js +10 -9
  18. package/dist/bin/fastr.js.map +1 -1
  19. package/dist/src/bench/benchmark.js +13 -13
  20. package/dist/src/bench/harness.js +17 -17
  21. package/dist/src/bench/public-harness.js +5 -5
  22. package/dist/src/branch/manager.js +31 -31
  23. package/dist/src/ci/guardian.js +9 -9
  24. package/dist/src/db/doctor.js +25 -25
  25. package/dist/src/db/hygiene.js +23 -23
  26. package/dist/src/db/repository.js +283 -283
  27. package/dist/src/db/schema.js +268 -268
  28. package/dist/src/db/spool.js +4 -4
  29. package/dist/src/export/obsidian.js +15 -15
  30. package/dist/src/import/local.js +4 -4
  31. package/dist/src/installer/setup.d.ts +2 -0
  32. package/dist/src/installer/setup.d.ts.map +1 -1
  33. package/dist/src/installer/setup.js +24 -18
  34. package/dist/src/installer/setup.js.map +1 -1
  35. package/dist/src/mcp/server.js +25 -25
  36. package/dist/src/mirror/markdown-pages.js +22 -22
  37. package/dist/src/search/backfill.js +6 -6
  38. package/dist/src/search/hybrid-rrf.js +60 -60
  39. package/dist/src/ui/server.js +1259 -1259
  40. package/dist/src/vision/assets.js +6 -6
  41. package/dist-ui/assets/ActionsPage-B3quzFNF.js +11 -11
  42. package/dist-ui/assets/GraphPage-DetiATmC.js +1 -1
  43. package/dist-ui/assets/ObservatoryPage-QIGMwENf.js +16 -16
  44. package/dist-ui/assets/TreemapPage-C7uLZd37.js +2 -2
  45. package/dist-ui/assets/index-BJnPINTu.css +1 -1
  46. package/dist-ui/assets/index-DWAH--Fu.js +329 -329
  47. package/dist-ui/assets/maximize-2-BebkGbZA.js +6 -6
  48. package/dist-ui/build-manifest.json +95 -95
  49. package/dist-ui/index.html +26 -26
  50. package/llms-install.md +69 -69
  51. package/package.json +108 -107
  52. package/server.json +28 -28
  53. package/skills/fastr-compress/SKILL.md +33 -33
  54. package/skills/fastr-curate/SKILL.md +62 -62
  55. package/skills/fastr-dag/SKILL.md +47 -47
  56. package/skills/fastr-doctor/SKILL.md +39 -39
  57. package/skills/fastr-memory/SKILL.md +51 -51
  58. package/skills/fastr-plan-estimate/SKILL.md +45 -45
  59. package/skills/fastr-session-commit/SKILL.md +49 -49
  60. package/skills/fastr-setup/SKILL.md +45 -45
  61. package/skills/fastr-trap-log/SKILL.md +39 -39
  62. package/skills/fastr-vision/SKILL.md +35 -35
  63. package/skills/skills.json +65 -65
@@ -442,15 +442,15 @@ export function startUiServer(config, options = {}) {
442
442
  const dataVersionRow = db.prepare('PRAGMA data_version').get();
443
443
  const dataVersion = Number(Object.values(dataVersionRow ?? {})[0] ?? 0);
444
444
  const totalChanges = Number(db.prepare('SELECT total_changes() AS c').get().c ?? 0);
445
- const counts = db.prepare(`
446
- SELECT
447
- (SELECT COUNT(*) FROM memories) AS memories,
448
- (SELECT COUNT(*) FROM memories WHERE valid_to IS NULL) AS active,
449
- (SELECT COALESCE(SUM(pinned), 0) FROM memories) AS pinned,
450
- (SELECT COALESCE(MAX(created_at), '') FROM memories) AS lastCreated,
451
- (SELECT COUNT(*) FROM actions) AS actions,
452
- (SELECT COUNT(*) FROM actions WHERE status = 'done') AS actionsDone,
453
- (SELECT COUNT(*) FROM memory_conflicts WHERE status = 'open') AS openConflicts
445
+ const counts = db.prepare(`
446
+ SELECT
447
+ (SELECT COUNT(*) FROM memories) AS memories,
448
+ (SELECT COUNT(*) FROM memories WHERE valid_to IS NULL) AS active,
449
+ (SELECT COALESCE(SUM(pinned), 0) FROM memories) AS pinned,
450
+ (SELECT COALESCE(MAX(created_at), '') FROM memories) AS lastCreated,
451
+ (SELECT COUNT(*) FROM actions) AS actions,
452
+ (SELECT COUNT(*) FROM actions WHERE status = 'done') AS actionsDone,
453
+ (SELECT COUNT(*) FROM memory_conflicts WHERE status = 'open') AS openConflicts
454
454
  `).get();
455
455
  const spoolPending = spool.hasPending();
456
456
  sendJson(res, 200, {
@@ -648,8 +648,8 @@ export function startUiServer(config, options = {}) {
648
648
  // API: Entities & Knowledge Relations
649
649
  if (url.pathname === '/api/entities') {
650
650
  const entities = repo.getEntities(config.projectId);
651
- const edges = db.prepare(`
652
- SELECT source_id, target_id, edge_type FROM graph_edges WHERE project_id = ? AND edge_type = 'entity_link'
651
+ const edges = db.prepare(`
652
+ SELECT source_id, target_id, edge_type FROM graph_edges WHERE project_id = ? AND edge_type = 'entity_link'
653
653
  `).all(config.projectId);
654
654
  res.writeHead(200, { 'Content-Type': 'application/json' });
655
655
  res.end(JSON.stringify({ entities, edges, count: entities.length }));
@@ -809,1254 +809,1254 @@ function getDistUiDir() {
809
809
  return null;
810
810
  }
811
811
  function renderDashboardHtml(projectId, projectName) {
812
- return `<!DOCTYPE html>
813
- <html lang="en">
814
- <head>
815
- <meta charset="UTF-8">
816
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
817
- <title>⚡ FastrMemory Hub &bull; ${escapeHtmlServer(projectName)}</title>
818
- <style>
819
- :root {
820
- --bg: #090d16;
821
- --surface: #111827;
822
- --card: #1f2937;
823
- --border: #374151;
824
- --text: #f9fafb;
825
- --text-muted: #9ca3af;
826
- --primary: #3b82f6;
827
- --emerald: #10b981;
828
- --amber: #f59e0b;
829
- --purple: #8b5cf6;
830
- --rose: #f43f5e;
831
- }
832
- * { box-sizing: border-box; margin: 0; padding: 0; }
833
- body {
834
- background: var(--bg);
835
- color: var(--text);
836
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
837
- min-height: 100vh;
838
- display: flex;
839
- flex-direction: column;
840
- overflow-x: hidden;
841
- }
842
- header {
843
- background: var(--surface);
844
- border-bottom: 1px solid var(--border);
845
- padding: 14px 24px;
846
- display: flex;
847
- align-items: center;
848
- justify-content: space-between;
849
- position: sticky;
850
- top: 0;
851
- z-index: 50;
852
- }
853
- .brand {
854
- display: flex;
855
- align-items: center;
856
- gap: 12px;
857
- font-size: 19px;
858
- font-weight: 700;
859
- letter-spacing: -0.4px;
860
- }
861
- .brand span { color: var(--primary); }
862
- .live-dot {
863
- width: 8px;
864
- height: 8px;
865
- background: var(--emerald);
866
- border-radius: 50%;
867
- box-shadow: 0 0 10px var(--emerald);
868
- animation: pulse 2s infinite;
869
- }
870
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
871
-
872
- .nav-tabs {
873
- display: flex;
874
- gap: 4px;
875
- background: var(--bg);
876
- padding: 4px;
877
- border-radius: 8px;
878
- border: 1px solid var(--border);
879
- }
880
- .tab-btn {
881
- background: transparent;
882
- border: none;
883
- color: var(--text-muted);
884
- padding: 6px 14px;
885
- border-radius: 6px;
886
- font-size: 13px;
887
- font-weight: 600;
888
- cursor: pointer;
889
- display: flex;
890
- align-items: center;
891
- gap: 6px;
892
- transition: all 0.15s;
893
- }
894
- .tab-btn:hover { color: var(--text); background: rgba(255, 255, 255, 0.05); }
895
- .tab-btn.active { color: #fff; background: var(--card); border: 1px solid var(--border); }
896
- .badge {
897
- font-size: 10px;
898
- font-weight: 700;
899
- padding: 2px 7px;
900
- border-radius: 9999px;
901
- text-transform: uppercase;
902
- letter-spacing: 0.5px;
903
- }
904
- .badge-rule { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
905
- .badge-trap { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.3); }
906
- .badge-decision { background: rgba(139, 92, 246, 0.15); color: #a78bfa; border: 1px solid rgba(139, 92, 246, 0.3); }
907
- .badge-file { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); }
908
-
909
- .stats-bar {
910
- display: grid;
911
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
912
- gap: 14px;
913
- padding: 18px 24px;
914
- background: var(--surface);
915
- border-bottom: 1px solid var(--border);
916
- }
917
- .stat-card {
918
- background: var(--card);
919
- border: 1px solid var(--border);
920
- padding: 12px 18px;
921
- border-radius: 8px;
922
- }
923
- .stat-val { font-size: 24px; font-weight: 700; color: #fff; }
924
- .stat-lbl { font-size: 11px; color: var(--text-muted); text-transform: uppercase; margin-top: 3px; font-weight: 600; }
925
-
926
- main {
927
- padding: 24px;
928
- flex: 1;
929
- max-width: 1440px;
930
- margin: 0 auto;
931
- width: 100%;
932
- }
933
- .tab-content { display: none; }
934
- .tab-content.active { display: block; }
935
-
936
- .search-row {
937
- display: flex;
938
- gap: 12px;
939
- margin-bottom: 16px;
940
- flex-wrap: wrap;
941
- }
942
- .search-input {
943
- flex: 1;
944
- min-width: 280px;
945
- background: var(--card);
946
- border: 1px solid var(--border);
947
- border-radius: 8px;
948
- padding: 11px 16px;
949
- color: var(--text);
950
- font-size: 14px;
951
- outline: none;
952
- transition: border-color 0.2s;
953
- }
954
- .search-input:focus { border-color: var(--primary); }
955
-
956
- .filter-pills {
957
- display: flex;
958
- gap: 8px;
959
- flex-wrap: wrap;
960
- margin-bottom: 20px;
961
- }
962
- .pill {
963
- background: var(--card);
964
- border: 1px solid var(--border);
965
- color: var(--text-muted);
966
- padding: 5px 12px;
967
- border-radius: 20px;
968
- font-size: 12px;
969
- cursor: pointer;
970
- font-weight: 500;
971
- transition: all 0.15s;
972
- }
973
- .pill:hover { color: #fff; }
974
- .pill.active { background: var(--primary); color: #fff; border-color: var(--primary); }
975
-
976
- .grid {
977
- display: grid;
978
- grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
979
- gap: 16px;
980
- }
981
- .card {
982
- background: var(--surface);
983
- border: 1px solid var(--border);
984
- border-radius: 10px;
985
- padding: 16px;
986
- display: flex;
987
- flex-direction: column;
988
- gap: 10px;
989
- cursor: pointer;
990
- transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
991
- }
992
- .card:hover { transform: translateY(-2px); border-color: #6b7280; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); }
993
- .card-trap { border-left: 4px solid var(--amber); }
994
- .card-header {
995
- display: flex;
996
- justify-content: space-between;
997
- align-items: flex-start;
998
- gap: 8px;
999
- }
1000
- .card-title { font-size: 15px; font-weight: 600; color: #fff; line-height: 1.4; }
1001
- .card-content {
1002
- font-size: 13px;
1003
- line-height: 1.5;
1004
- color: #d1d5db;
1005
- background: var(--card);
1006
- padding: 10px 12px;
1007
- border-radius: 6px;
1008
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1009
- max-height: 120px;
1010
- overflow: hidden;
1011
- text-overflow: ellipsis;
1012
- }
1013
- .card-footer {
1014
- display: flex;
1015
- justify-content: space-between;
1016
- align-items: center;
1017
- font-size: 11px;
1018
- color: var(--text-muted);
1019
- margin-top: auto;
1020
- }
1021
- .files-list { display: flex; flex-wrap: wrap; gap: 4px; }
1022
- .file-tag {
1023
- background: rgba(255, 255, 255, 0.05);
1024
- border: 1px solid rgba(255, 255, 255, 0.1);
1025
- padding: 2px 6px;
1026
- border-radius: 4px;
1027
- font-size: 11px;
1028
- font-family: monospace;
1029
- color: #93c5fd;
1030
- }
1031
-
1032
- /* Interactive Graph Canvas View */
1033
- #graph-container {
1034
- position: relative;
1035
- width: 100%;
1036
- height: 650px;
1037
- background: #06090e;
1038
- border: 1px solid var(--border);
1039
- border-radius: 10px;
1040
- overflow: hidden;
1041
- }
1042
- #graph-canvas {
1043
- width: 100%;
1044
- height: 100%;
1045
- cursor: grab;
1046
- }
1047
- #graph-canvas:active { cursor: grabbing; }
1048
- .graph-controls {
1049
- position: absolute;
1050
- bottom: 16px;
1051
- right: 16px;
1052
- display: flex;
1053
- gap: 8px;
1054
- background: rgba(17, 24, 39, 0.85);
1055
- backdrop-filter: blur(8px);
1056
- padding: 6px;
1057
- border-radius: 8px;
1058
- border: 1px solid var(--border);
1059
- }
1060
- .graph-btn {
1061
- background: var(--card);
1062
- border: 1px solid var(--border);
1063
- color: #fff;
1064
- padding: 6px 12px;
1065
- border-radius: 6px;
1066
- font-size: 12px;
1067
- cursor: pointer;
1068
- }
1069
- .graph-btn:hover { background: #374151; }
1070
- .graph-legend {
1071
- position: absolute;
1072
- top: 16px;
1073
- left: 16px;
1074
- background: rgba(17, 24, 39, 0.85);
1075
- backdrop-filter: blur(8px);
1076
- padding: 10px 14px;
1077
- border-radius: 8px;
1078
- border: 1px solid var(--border);
1079
- font-size: 12px;
1080
- display: flex;
1081
- flex-direction: column;
1082
- gap: 6px;
1083
- }
1084
- .legend-item { display: flex; align-items: center; gap: 8px; }
1085
- .legend-dot { width: 10px; height: 10px; border-radius: 50%; }
1086
-
1087
- /* Modal Drawer */
1088
- .modal-overlay {
1089
- display: none;
1090
- position: fixed;
1091
- inset: 0;
1092
- background: rgba(0, 0, 0, 0.75);
1093
- backdrop-filter: blur(4px);
1094
- z-index: 100;
1095
- align-items: center;
1096
- justify-content: center;
1097
- }
1098
- .modal-overlay.active { display: flex; }
1099
- .modal-card {
1100
- background: var(--surface);
1101
- border: 1px solid var(--border);
1102
- border-radius: 12px;
1103
- width: 90%;
1104
- max-width: 700px;
1105
- max-height: 85vh;
1106
- display: flex;
1107
- flex-direction: column;
1108
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
1109
- }
1110
- .modal-header {
1111
- padding: 16px 20px;
1112
- border-bottom: 1px solid var(--border);
1113
- display: flex;
1114
- justify-content: space-between;
1115
- align-items: center;
1116
- }
1117
- .modal-body {
1118
- padding: 20px;
1119
- overflow-y: auto;
1120
- display: flex;
1121
- flex-direction: column;
1122
- gap: 16px;
1123
- }
1124
- .modal-close {
1125
- background: transparent;
1126
- border: none;
1127
- color: var(--text-muted);
1128
- font-size: 20px;
1129
- cursor: pointer;
1130
- }
1131
- .modal-close:hover { color: #fff; }
1132
- </style>
1133
- </head>
1134
- <body>
1135
- <header>
1136
- <div class="brand">
1137
- <div class="live-dot"></div>
1138
- <span>⚡ FastrMemory</span> Hub
1139
- </div>
1140
-
1141
- <div class="nav-tabs">
1142
- <button class="tab-btn active" onclick="switchTab('overview')">📊 Overview</button>
1143
- <button class="tab-btn" onclick="switchTab('memories')">🧠 Memories (<span id="tab-cnt-mem">0</span>)</button>
1144
- <button class="tab-btn" onclick="switchTab('traps')">⚠️ Traps (<span id="tab-cnt-traps">0</span>)</button>
1145
- <button class="tab-btn" onclick="switchTab('graph')">🕸️ Visual Graph & DAG</button>
1146
- <button class="tab-btn" onclick="switchTab('actions')">🔀 Actions & Frontier (<span id="tab-cnt-actions">0</span>)</button>
1147
- <button class="tab-btn" onclick="switchTab('conflicts')">⚔️ Conflicts (<span id="tab-cnt-conflicts">0</span>)</button>
1148
- <button class="tab-btn" onclick="switchTab('timeline')">⏱️ Timeline</button>
1149
- </div>
1150
-
1151
- <div style="font-size: 12px; color: var(--text-muted);">
1152
- Node 24 WAL &bull; <code style="color: #60a5fa;">${escapeHtmlServer(projectId)}</code>
1153
- </div>
1154
- </header>
1155
-
1156
- <section class="stats-bar">
1157
- <div class="stat-card">
1158
- <div class="stat-val" id="stat-active">0</div>
1159
- <div class="stat-lbl">Active Rules</div>
1160
- </div>
1161
- <div class="stat-card">
1162
- <div class="stat-val" id="stat-traps" style="color: #fbbf24;">0</div>
1163
- <div class="stat-lbl">Error Traps (Conditional Boost)</div>
1164
- </div>
1165
- <div class="stat-card">
1166
- <div class="stat-val" id="stat-frontier" style="color: #f59e0b;">0</div>
1167
- <div class="stat-lbl">Unblocked Frontier Tasks</div>
1168
- </div>
1169
- <div class="stat-card">
1170
- <div class="stat-val" id="stat-graph-nodes" style="color: #38bdf8;">0</div>
1171
- <div class="stat-lbl">Knowledge Nodes</div>
1172
- </div>
1173
- <div class="stat-card">
1174
- <div class="stat-val" id="stat-conflicts" style="color: #f43f5e;">0</div>
1175
- <div class="stat-lbl">Open Contradictions</div>
1176
- </div>
1177
- <div class="stat-card">
1178
- <div class="stat-val" id="stat-spool" style="color: #34d399;">Clear</div>
1179
- <div class="stat-lbl">Offline Spool Queue</div>
1180
- </div>
1181
- </section>
1182
-
1183
- <main>
1184
- <!-- TAB 1: OVERVIEW -->
1185
- <div id="tab-overview" class="tab-content active">
1186
- <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 20px;">
1187
- <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 24px;">
1188
- <h2 style="font-size: 18px; margin-bottom: 12px;">Cognitive Memory Architecture</h2>
1189
- <p style="color: var(--text-muted); font-size: 14px; line-height: 1.6; margin-bottom: 20px;">
1190
- FastrMemory runs natively inside Node 24 with <b>zero background daemons</b>. Rules, error traps, and decisions are indexed via SQLite WAL mode, mirrored to <code>.fastr/pages/*.md</code> for Git diffs, and fused using Triple-Stream RRF ($k=20$) across full-text, local vectors, and code file graphs.
1191
- </p>
1192
- <div style="display: flex; gap: 10px;">
1193
- <button class="pill active" onclick="switchTab('traps')">Inspect Error Traps &rarr;</button>
1194
- <button class="pill" onclick="switchTab('graph')">Open Knowledge Graph &rarr;</button>
1195
- </div>
1196
- </div>
1197
-
1198
- <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 24px;">
1199
- <h3 style="font-size: 15px; margin-bottom: 12px;">Quick CLI Shortcuts</h3>
1200
- <ul style="color: var(--text-muted); font-size: 13px; line-height: 2; list-style: none;">
1201
- <li><code>fastrmemory status</code> &bull; Health overview</li>
1202
- <li><code>fastrmemory bench</code> &bull; Run Hit@5 benchmarks</li>
1203
- <li><code>fastrmemory recall &lt;q&gt;</code> &bull; Triple-stream search</li>
1204
- <li><code>fastrmemory remember &lt;t&gt;</code> &bull; Save verified rule</li>
1205
- </ul>
1206
- </div>
1207
- </div>
1208
- </div>
1209
-
1210
- <!-- TAB 2: ALL MEMORIES -->
1211
- <div id="tab-memories" class="tab-content">
1212
- <div class="search-row">
1213
- <input type="text" id="search-input" class="search-input" placeholder="Instant search by keyword, file path, concept, or [mem_id]..." />
1214
- </div>
1215
- <div class="filter-pills" id="module-pills">
1216
- <button class="pill active" onclick="filterByModule('all')">All Modules</button>
1217
- </div>
1218
- <div class="grid" id="memories-grid"></div>
1219
- </div>
1220
-
1221
- <!-- TAB 3: ERROR TRAPS -->
1222
- <div id="tab-traps" class="tab-content">
1223
- <div style="margin-bottom: 16px; color: var(--text-muted); font-size: 13px;">
1224
- Error traps receive a <b>conditional 2.0x boost</b> during recall when the query has trap intent or targets a linked file, so agents do not repeat known bugs.
1225
- </div>
1226
- <div class="grid" id="traps-grid"></div>
1227
- </div>
1228
-
1229
- <!-- TAB 4: VISUAL GRAPH & ACTION DAG -->
1230
- <div id="tab-graph" class="tab-content">
1231
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; flex-wrap: wrap; gap: 10px;">
1232
- <div style="display: flex; gap: 8px;">
1233
- <button id="btn-mode-knowledge" class="pill active" onclick="switchGraphMode('knowledge')">🕸️ Knowledge Graph</button>
1234
- <button id="btn-mode-actions" class="pill" onclick="switchGraphMode('actions')">🔀 Action DAG & Frontier</button>
1235
- </div>
1236
- <div id="graph-mode-desc" style="color: var(--text-muted); font-size: 13px;">
1237
- Visualizing semantic memory nodes, error traps (conditional boost), code files, and extracted entities.
1238
- </div>
1239
- </div>
1240
- <div id="graph-container">
1241
- <div class="graph-legend" id="graph-legend">
1242
- <div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div> Code File Node</div>
1243
- <div class="legend-item"><div class="legend-dot" style="background: #fbbf24;"></div> Error Trap [TRAP] (conditional boost)</div>
1244
- <div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Verified Rule</div>
1245
- <div class="legend-item"><div class="legend-dot" style="background: #8b5cf6;"></div> Architecture Decision</div>
1246
- <div class="legend-item"><div class="legend-dot" style="background: #ec4899;"></div> Entity Link</div>
1247
- </div>
1248
- <div class="graph-controls">
1249
- <button class="graph-btn" onclick="zoomGraph(1.2)">Zoom +</button>
1250
- <button class="graph-btn" onclick="zoomGraph(0.8)">Zoom -</button>
1251
- <button class="graph-btn" onclick="resetGraphZoom()">Reset View</button>
1252
- <button class="graph-btn" onclick="toggleGraphPhysics()">Pause/Play Physics</button>
1253
- </div>
1254
- <canvas id="graph-canvas"></canvas>
1255
- </div>
1256
- </div>
1257
-
1258
- <!-- TAB 5: ACTIONS & FRONTIER -->
1259
- <div id="tab-actions" class="tab-content">
1260
- <div style="margin-bottom: 16px; color: var(--text-muted); font-size: 13px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
1261
- <span>Multi-Agent Action DAG ensures zero task duplicate collisions and enforces topological execution order.</span>
1262
- <button class="pill active" onclick="switchTab('graph'); switchGraphMode('actions');">Inspect in Visual DAG &rarr;</button>
1263
- </div>
1264
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px;">
1265
- <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1266
- <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #f59e0b; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1267
- <span>⚡ Ready Frontier</span>
1268
- <span id="col-cnt-frontier" class="badge badge-trap">0</span>
1269
- </h3>
1270
- <div id="list-frontier" style="display: flex; flex-direction: column; gap: 10px;"></div>
1271
- </div>
1272
- <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1273
- <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #3b82f6; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1274
- <span>🔨 Active / In Progress</span>
1275
- <span id="col-cnt-active" class="badge badge-rule">0</span>
1276
- </h3>
1277
- <div id="list-active" style="display: flex; flex-direction: column; gap: 10px;"></div>
1278
- </div>
1279
- <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1280
- <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #9ca3af; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1281
- <span>⏳ Blocked (Prereqs)</span>
1282
- <span id="col-cnt-blocked" class="badge" style="background: rgba(156, 163, 175, 0.2); color: #9ca3af;">0</span>
1283
- </h3>
1284
- <div id="list-blocked" style="display: flex; flex-direction: column; gap: 10px;"></div>
1285
- </div>
1286
- <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1287
- <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #10b981; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1288
- <span>✅ Completed</span>
1289
- <span id="col-cnt-done" class="badge badge-file">0</span>
1290
- </h3>
1291
- <div id="list-done" style="display: flex; flex-direction: column; gap: 10px;"></div>
1292
- </div>
1293
- </div>
1294
- </div>
1295
-
1296
- <!-- TAB 6: CONFLICTS -->
1297
- <div id="tab-conflicts" class="tab-content">
1298
- <div style="margin-bottom: 16px; color: var(--text-muted); font-size: 13px;">
1299
- FastrMemory proactively detects contradictions between newly proposed facts and existing rules before they confuse autonomous coding agents.
1300
- </div>
1301
- <div id="conflicts-list" style="display: flex; flex-direction: column; gap: 14px;"></div>
1302
- </div>
1303
-
1304
- <!-- TAB 7: TIMELINE -->
1305
- <div id="tab-timeline" class="tab-content">
1306
- <div id="timeline-list" style="display: flex; flex-direction: column; gap: 12px;"></div>
1307
- </div>
1308
- </main>
1309
-
1310
- <!-- DETAIL MODAL: MEMORY -->
1311
- <div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
1312
- <div class="modal-card" onclick="event.stopPropagation()">
1313
- <div class="modal-header">
1314
- <div id="modal-title" style="font-weight: 700; font-size: 16px;"></div>
1315
- <button class="modal-close" onclick="closeModal()">&times;</button>
1316
- </div>
1317
- <div class="modal-body">
1318
- <div style="display: flex; gap: 8px;" id="modal-badges"></div>
1319
- <div style="font-size: 13px; color: var(--text-muted);">
1320
- <b>ID:</b> <code id="modal-id" style="color: #60a5fa;"></code> &bull;
1321
- <b>Module:</b> <span id="modal-module"></span> &bull;
1322
- <b>Score:</b> <span id="modal-score"></span>
1323
- </div>
1324
- <div style="font-size: 13px; color: var(--text-muted);" id="modal-files-row"></div>
1325
- <pre id="modal-content" style="background: var(--card); padding: 14px; border-radius: 8px; font-family: monospace; font-size: 13px; white-space: pre-wrap; line-height: 1.6;"></pre>
1326
- </div>
1327
- </div>
1328
- </div>
1329
-
1330
- <!-- DETAIL MODAL: ACTION -->
1331
- <div class="modal-overlay" id="action-modal-overlay" onclick="closeActionModal(event)">
1332
- <div class="modal-card" onclick="event.stopPropagation()">
1333
- <div class="modal-header">
1334
- <div id="act-modal-title" style="font-weight: 700; font-size: 16px;"></div>
1335
- <button class="modal-close" onclick="closeActionModal()">&times;</button>
1336
- </div>
1337
- <div class="modal-body">
1338
- <div style="display: flex; gap: 8px;" id="act-modal-badges"></div>
1339
- <div style="font-size: 13px; color: var(--text-muted);">
1340
- <b>Action ID:</b> <code id="act-modal-id" style="color: #60a5fa;"></code> &bull;
1341
- <b>Priority:</b> <span id="act-modal-priority"></span> &bull;
1342
- <b>Assigned To:</b> <span id="act-modal-agent"></span>
1343
- </div>
1344
- <div style="font-size: 13px; color: var(--text-muted);" id="act-modal-reqs"></div>
1345
- <div style="font-size: 13px; color: var(--text-muted);" id="act-modal-unlocks"></div>
1346
- <div style="font-size: 13px; color: var(--text-muted);" id="act-modal-conflicts"></div>
1347
- <div style="font-size: 13px; color: var(--text-muted);"><b>Description:</b></div>
1348
- <pre id="act-modal-desc" style="background: var(--card); padding: 14px; border-radius: 8px; font-family: monospace; font-size: 13px; white-space: pre-wrap; line-height: 1.6;"></pre>
1349
- </div>
1350
- </div>
1351
- </div>
1352
-
1353
- <script>
1354
- let currentTab = 'overview';
1355
- let allMemories = [];
1356
- let currentModule = 'all';
1357
- let graphMode = 'knowledge'; // 'knowledge' | 'actions'
1358
- let graphData = { nodes: [], edges: [] };
1359
- let allActionsData = { actions: [], edges: [], frontierCount: 0 };
1360
- let graphSimulationRunning = true;
1361
-
1362
- function escapeHtml(str) {
1363
- if (!str) return '';
1364
- return String(str)
1365
- .replace(/&/g, '&amp;')
1366
- .replace(/</g, '&lt;')
1367
- .replace(/>/g, '&gt;')
1368
- .replace(/"/g, '&quot;')
1369
- .replace(/'/g, '&#039;');
1370
- }
1371
-
1372
- function switchTab(tabId) {
1373
- currentTab = tabId;
1374
- document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
1375
- document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
1376
-
1377
- const targetBtn = Array.from(document.querySelectorAll('.tab-btn')).find(b => b.getAttribute('onclick')?.includes(tabId));
1378
- if (targetBtn) targetBtn.classList.add('active');
1379
-
1380
- const targetContent = document.getElementById('tab-' + tabId);
1381
- if (targetContent) targetContent.classList.add('active');
1382
-
1383
- if (tabId === 'graph') {
1384
- initGraphCanvas();
1385
- } else if (tabId === 'actions') {
1386
- loadActionsBoard();
1387
- } else if (tabId === 'conflicts') {
1388
- loadConflictsList();
1389
- } else if (tabId === 'timeline') {
1390
- loadTimeline();
1391
- }
1392
- }
1393
-
1394
- async function loadStatus() {
1395
- try {
1396
- const res = await fetch('/api/status');
1397
- const data = await res.json();
1398
- document.getElementById('stat-active').textContent = data.activeCount ?? 0;
1399
- document.getElementById('stat-traps').textContent = data.trapsCount ?? 0;
1400
- document.getElementById('stat-frontier').textContent = data.frontierCount ?? 0;
1401
- document.getElementById('stat-graph-nodes').textContent = data.graphNodesCount ?? 0;
1402
- document.getElementById('stat-conflicts').textContent = data.conflictsCount ?? 0;
1403
- document.getElementById('stat-spool').textContent = data.spoolPending ? 'Pending' : 'Clear (0)';
1404
-
1405
- document.getElementById('tab-cnt-mem').textContent = data.activeCount ?? 0;
1406
- document.getElementById('tab-cnt-traps').textContent = data.trapsCount ?? 0;
1407
- document.getElementById('tab-cnt-actions').textContent = data.actionsCount ?? 0;
1408
- document.getElementById('tab-cnt-conflicts').textContent = data.conflictsCount ?? 0;
1409
-
1410
- // Populate module filter pills
1411
- const pillsContainer = document.getElementById('module-pills');
1412
- if (pillsContainer) {
1413
- pillsContainer.innerHTML = '<button class="pill active" onclick="filterByModule(\\'all\\')">All Modules</button>' +
1414
- (data.modules || []).map(m => '<button class="pill" onclick="filterByModule(' + escapeHtml(JSON.stringify(m)) + ')">' + escapeHtml(m) + '</button>').join('');
1415
- }
1416
- } catch (err) {
1417
- console.error('Failed to load status', err);
1418
- }
1419
- }
1420
-
1421
- async function loadMemories(query = '') {
1422
- try {
1423
- let url = '/api/memories?q=' + encodeURIComponent(query);
1424
- if (currentModule !== 'all') {
1425
- url += '&module=' + encodeURIComponent(currentModule);
1426
- }
1427
- const res = await fetch(url);
1428
- allMemories = await res.json();
1429
-
1430
- renderMemoriesGrid(allMemories, 'memories-grid');
1431
-
1432
- // Also populate traps tab
1433
- const traps = allMemories.filter(m => m.kind === 'error_trap');
1434
- renderMemoriesGrid(traps, 'traps-grid');
1435
- } catch (err) {
1436
- console.error('Failed to load memories', err);
1437
- }
1438
- }
1439
-
1440
- function filterByModule(mod) {
1441
- currentModule = mod;
1442
- document.querySelectorAll('#module-pills .pill').forEach(p => p.classList.remove('active'));
1443
- const activePill = Array.from(document.querySelectorAll('#module-pills .pill')).find(p => p.textContent === mod || (mod === 'all' && p.textContent === 'All Modules'));
1444
- if (activePill) activePill.classList.add('active');
1445
- loadMemories(document.getElementById('search-input')?.value || '');
1446
- }
1447
-
1448
- function renderMemoriesGrid(memories, containerId) {
1449
- const grid = document.getElementById(containerId);
1450
- if (!grid) return;
1451
-
1452
- if (memories.length === 0) {
1453
- grid.innerHTML = '<div style="color: var(--text-muted); padding: 40px; text-align: center; grid-column: 1 / -1;">No memories match your query.</div>';
1454
- return;
1455
- }
1456
-
1457
- grid.innerHTML = memories.map(m => {
1458
- const isTrap = m.kind === 'error_trap';
1459
- const kindBadge = isTrap
1460
- ? '<span class="badge badge-trap">[TRAP]</span>'
1461
- : m.kind === 'decision'
1462
- ? '<span class="badge badge-decision">[DECISION]</span>'
1463
- : '<span class="badge badge-rule">[RULE]</span>';
1464
-
1465
- const filesHtml = (m.linked_files && m.linked_files.length > 0)
1466
- ? '<div class="files-list">' + m.linked_files.map(f => '<span class="file-tag">' + escapeHtml(f) + '</span>').join('') + '</div>'
1467
- : '<span style="color: var(--text-muted);">No linked files</span>';
1468
-
1469
- return '<div class="card ' + (isTrap ? 'card-trap' : '') + '" onclick="openMemoryModal(\\'' + m.id + '\\')">' +
1470
- '<div class="card-header">' +
1471
- '<div>' +
1472
- '<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 4px;">[' + m.id + '] &bull; ' + escapeHtml(m.module) + '</div>' +
1473
- '<div class="card-title">' + escapeHtml(m.title) + '</div>' +
1474
- '</div>' +
1475
- kindBadge +
1476
- '</div>' +
1477
- '<div class="card-content">' + escapeHtml(m.content) + '</div>' +
1478
- '<div class="card-footer">' +
1479
- filesHtml +
1480
- '<span>Score: ' + m.reward_score + '</span>' +
1481
- '</div>' +
1482
- '</div>';
1483
- }).join('');
1484
- }
1485
-
1486
- async function loadTimeline() {
1487
- try {
1488
- const res = await fetch('/api/timeline?limit=50');
1489
- const items = await res.json();
1490
- const list = document.getElementById('timeline-list');
1491
- if (!list) return;
1492
-
1493
- list.innerHTML = items.map(item => {
1494
- const isSuperseded = item.status === 'superseded';
1495
- const badgeClass = item.kind === 'error_trap' ? 'badge-trap' : 'badge-rule';
1496
- return '<div style="background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 18px; display: flex; justify-content: space-between; align-items: center;">' +
1497
- '<div>' +
1498
- '<div style="display: flex; gap: 8px; align-items: center; margin-bottom: 4px;">' +
1499
- '<span class="badge ' + badgeClass + '">[' + item.kind.toUpperCase() + ']</span>' +
1500
- '<span style="font-size: 11px; color: var(--text-muted);">' + item.createdAt + '</span>' +
1501
- (isSuperseded ? '<span style="font-size: 10px; color: #f87171; border: 1px solid rgba(248,113,113,0.3); padding: 1px 5px; border-radius: 4px;">SUPERSEDED</span>' : '') +
1502
- '</div>' +
1503
- '<div style="font-weight: 600; font-size: 14px;">' + escapeHtml(item.title) + '</div>' +
1504
- '<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Files: ' + (item.linkedFiles.map(escapeHtml).join(', ') || 'None') + '</div>' +
1505
- '</div>' +
1506
- '<div style="font-size: 12px; color: #60a5fa;">Score: ' + item.rewardScore + '</div>' +
1507
- '</div>';
1508
- }).join('');
1509
- } catch (err) {
1510
- console.error('Failed to load timeline', err);
1511
- }
1512
- }
1513
-
1514
- async function loadActionsBoard() {
1515
- try {
1516
- const res = await fetch('/api/actions');
1517
- allActionsData = await res.json();
1518
-
1519
- const frontier = allActionsData.actions.filter(a => a.isFrontier);
1520
- const active = allActionsData.actions.filter(a => a.status === 'active');
1521
- const blocked = allActionsData.actions.filter(a => a.status === 'blocked' || (a.status === 'pending' && !a.isFrontier));
1522
- const done = allActionsData.actions.filter(a => a.status === 'done');
1523
-
1524
- document.getElementById('col-cnt-frontier').textContent = frontier.length;
1525
- document.getElementById('col-cnt-active').textContent = active.length;
1526
- document.getElementById('col-cnt-blocked').textContent = blocked.length;
1527
- document.getElementById('col-cnt-done').textContent = done.length;
1528
-
1529
- renderActionList(frontier, 'list-frontier', 'frontier');
1530
- renderActionList(active, 'list-active', 'active');
1531
- renderActionList(blocked, 'list-blocked', 'blocked');
1532
- renderActionList(done, 'list-done', 'done');
1533
- } catch (err) {
1534
- console.error('Failed to load actions board', err);
1535
- }
1536
- }
1537
-
1538
- function renderActionList(items, containerId, category) {
1539
- const container = document.getElementById(containerId);
1540
- if (!container) return;
1541
- if (items.length === 0) {
1542
- container.innerHTML = '<div style="color: var(--text-muted); font-size: 12px; padding: 16px; border: 1px dashed var(--border); border-radius: 8px; text-align: center;">Empty</div>';
1543
- return;
1544
- }
1545
- container.innerHTML = items.map(function(a) {
1546
- var borderCol = category === 'frontier' ? '#f59e0b' : category === 'active' ? '#3b82f6' : category === 'done' ? '#10b981' : '#6b7280';
1547
- var assignHtml = a.assigned_to ? '<span>&bull; ' + escapeHtml(a.assigned_to) + '</span>' : '<span style="color: #34d399;">&bull; Unassigned</span>';
1548
- var reqHtml = (a.requires && a.requires.length > 0) ? '<span style="color: #94a3b8;">&bull; Req: ' + a.requires.length + '</span>' : '';
1549
- return '<div class="card" onclick="openActionModal(\\'' + a.id + '\\')" style="padding: 12px; border-left: 3px solid ' + borderCol + ';">' +
1550
- '<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;">' +
1551
- '<div style="font-size: 13px; font-weight: 600; color: #fff;">' + escapeHtml(a.title) + '</div>' +
1552
- '<span class="badge" style="background: rgba(255,255,255,0.08); color: #cbd5e1;">P' + a.priority + '</span>' +
1553
- '</div>' +
1554
- '<div style="font-size: 11px; color: var(--text-muted); display: flex; gap: 8px; align-items: center; margin-top: 4px;">' +
1555
- '<code>' + a.id + '</code>' +
1556
- assignHtml +
1557
- reqHtml +
1558
- '</div>' +
1559
- '</div>';
1560
- }).join('');
1561
- }
1562
-
1563
- async function resolveConflictAction(conflictId, keepMemoryId, forgetLoser) {
1564
- try {
1565
- const res = await fetch('/api/conflicts/resolve', {
1566
- method: 'POST',
1567
- headers: { 'Content-Type': 'application/json' },
1568
- body: JSON.stringify({ conflictId, keepMemoryId, forgetLoser, reason: 'Resolved via FastrMemory Hub' })
1569
- });
1570
- if (!res.ok) {
1571
- const err = await res.json();
1572
- alert('Failed to resolve: ' + (err.error || 'Unknown error'));
1573
- return;
1574
- }
1575
- await loadConflictsList();
1576
- await loadStatus();
1577
- } catch (err) {
1578
- alert('Resolution request failed: ' + err.message);
1579
- }
1580
- }
1581
-
1582
- async function loadConflictsList() {
1583
- try {
1584
- const res = await fetch('/api/conflicts');
1585
- const conflicts = await res.json();
1586
- const container = document.getElementById('conflicts-list');
1587
- if (!container) return;
1588
- if (conflicts.length === 0) {
1589
- container.innerHTML = '<div style="color: #34d399; padding: 40px; text-align: center; background: var(--surface); border: 1px solid var(--border); border-radius: 10px;">✨ Zero open contradictions detected. All knowledge rules are consistent!</div>';
1590
- return;
1591
- }
1592
- container.innerHTML = conflicts.map(function(c) {
1593
- var scorePct = (c.score * 100).toFixed(0);
1594
- return '<div class="card" style="border-left: 4px solid var(--rose); padding: 18px;">' +
1595
- '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">' +
1596
- '<span class="badge" style="background: rgba(244, 63, 94, 0.2); color: #fb7185;">Contradiction (' + scorePct + '% Conflict Score)</span>' +
1597
- '<span style="font-size: 11px; color: var(--text-muted);">' + c.created_at + '</span>' +
1598
- '</div>' +
1599
- '<div style="font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 6px;">' + escapeHtml(c.reason) + '</div>' +
1600
- '<div style="font-size: 12px; color: var(--text-muted); font-family: monospace; margin-bottom: 12px;">' +
1601
- 'Memory A: <code style="color: #60a5fa;">' + c.memory_a_id + '</code> vs Memory B: <code style="color: #f43f5e;">' + c.memory_b_id + '</code>' +
1602
- '</div>' +
1603
- '<div style="display: flex; gap: 8px; flex-wrap: wrap;">' +
1604
- '<button class="pill" style="background: rgba(59, 130, 246, 0.15); border-color: #3b82f6; color: #93c5fd; font-size: 11px; padding: 4px 10px;" onclick="resolveConflictAction(' + c.id + ', \\'' + c.memory_a_id + '\\', true)">Keep ' + c.memory_a_id + ' (Deprecate B)</button>' +
1605
- '<button class="pill" style="background: rgba(244, 63, 94, 0.15); border-color: #f43f5e; color: #fca5a5; font-size: 11px; padding: 4px 10px;" onclick="resolveConflictAction(' + c.id + ', \\'' + c.memory_b_id + '\\', true)">Keep ' + c.memory_b_id + ' (Deprecate A)</button>' +
1606
- '<button class="pill" style="background: rgba(255, 255, 255, 0.05); color: #cbd5e1; font-size: 11px; padding: 4px 10px;" onclick="resolveConflictAction(' + c.id + ', \\'' + c.memory_a_id + '\\', false)">Keep Both (Weaken B)</button>' +
1607
- '</div>' +
1608
- '</div>';
1609
- }).join('');
1610
- } catch (err) {
1611
- console.error('Failed to load conflicts', err);
1612
- }
1613
- }
1614
-
1615
- async function openMemoryModal(id) {
1616
- let mem = allMemories.find(m => m.id === id);
1617
- if (!mem) {
1618
- try {
1619
- const res = await fetch('/api/memories?q=' + encodeURIComponent(id));
1620
- const results = await res.json();
1621
- mem = results.find(m => m.id === id);
1622
- } catch (e) {}
1623
- }
1624
- if (!mem) return;
1625
-
1626
- document.getElementById('modal-title').textContent = mem.title;
1627
- document.getElementById('modal-id').textContent = mem.id;
1628
- document.getElementById('modal-module').textContent = mem.module;
1629
- document.getElementById('modal-score').textContent = mem.reward_score;
1630
- document.getElementById('modal-content').textContent = mem.content;
1631
-
1632
- const badgeContainer = document.getElementById('modal-badges');
1633
- badgeContainer.innerHTML = '<span class="badge ' + (mem.kind === 'error_trap' ? 'badge-trap' : 'badge-rule') + '">[' + mem.kind.toUpperCase() + ']</span>' +
1634
- '<span class="badge badge-file">' + mem.status.toUpperCase() + '</span>';
1635
-
1636
- const filesRow = document.getElementById('modal-files-row');
1637
- filesRow.innerHTML = (mem.linked_files && mem.linked_files.length > 0)
1638
- ? '<b>Linked Files:</b> ' + mem.linked_files.map(f => '<code style="color: #93c5fd; margin-right: 6px;">' + escapeHtml(f) + '</code>').join('')
1639
- : '<b>Linked Files:</b> None';
1640
-
1641
- document.getElementById('modal-overlay').classList.add('active');
1642
- }
1643
-
1644
- function closeModal(e) {
1645
- document.getElementById('modal-overlay').classList.remove('active');
1646
- }
1647
-
1648
- async function openActionModal(id) {
1649
- if (!allActionsData || !allActionsData.actions) {
1650
- try {
1651
- const res = await fetch('/api/actions');
1652
- allActionsData = await res.json();
1653
- } catch (e) {}
1654
- }
1655
- const act = (allActionsData?.actions || []).find(a => a.id === id);
1656
- if (!act) return;
1657
-
1658
- document.getElementById('act-modal-title').textContent = act.title;
1659
- document.getElementById('act-modal-id').textContent = act.id;
1660
- document.getElementById('act-modal-priority').textContent = 'P' + act.priority;
1661
- document.getElementById('act-modal-agent').textContent = act.assigned_to || 'Unassigned (Open for Claim)';
1662
- document.getElementById('act-modal-desc').textContent = act.description || 'No detailed description.';
1663
-
1664
- const badgeContainer = document.getElementById('act-modal-badges');
1665
- const isFrontier = act.isFrontier;
1666
- badgeContainer.innerHTML = (isFrontier ? '<span class="badge badge-trap">⚡ FRONTIER READY</span> ' : '') +
1667
- '<span class="badge" style="background: rgba(255,255,255,0.1); color: #fff;">' + act.status.toUpperCase() + '</span>';
1668
-
1669
- const reqsEl = document.getElementById('act-modal-reqs');
1670
- reqsEl.innerHTML = (act.requires && act.requires.length > 0)
1671
- ? '<b>Prerequisites (Requires):</b> ' + act.requires.map(r => '<code style="color: #93c5fd; margin-right: 6px;">' + escapeHtml(r) + '</code>').join('')
1672
- : '<b>Prerequisites:</b> None (Unblocked)';
1673
-
1674
- const unlocksEl = document.getElementById('act-modal-unlocks');
1675
- unlocksEl.innerHTML = (act.unlocks && act.unlocks.length > 0)
1676
- ? '<b>Unlocks Downstream:</b> ' + act.unlocks.map(u => '<code style="color: #34d399; margin-right: 6px;">' + escapeHtml(u) + '</code>').join('')
1677
- : '<b>Unlocks Downstream:</b> None';
1678
-
1679
- const conflictsEl = document.getElementById('act-modal-conflicts');
1680
- conflictsEl.innerHTML = (act.conflictsWith && act.conflictsWith.length > 0)
1681
- ? '<b style="color: #fb7185;">Mutex Conflicts With:</b> ' + act.conflictsWith.map(c => '<code style="color: #fb7185; margin-right: 6px;">' + escapeHtml(c) + '</code>').join('')
1682
- : '<b>Mutex Conflicts:</b> None';
1683
-
1684
- document.getElementById('action-modal-overlay').classList.add('active');
1685
- }
1686
-
1687
- function closeActionModal() {
1688
- document.getElementById('action-modal-overlay').classList.remove('active');
1689
- }
1690
-
1691
- // --- HTML5 CANVAS VISUALIZER (KNOWLEDGE GRAPH & ACTION DAG) ---
1692
- let canvas, ctx, nodes = [], edges = [], transform = { x: 0, y: 0, k: 1 };
1693
- let isDragging = false, dragNode = null, dragStart = { x: 0, y: 0 }, dragStartMouse = { x: 0, y: 0 };
1694
- let resizeListenerAdded = false, animationFrameId = null;
1695
-
1696
- async function initGraphCanvas() {
1697
- canvas = document.getElementById('graph-canvas');
1698
- if (!canvas) return;
1699
- ctx = canvas.getContext('2d');
1700
- resizeCanvas();
1701
- if (!resizeListenerAdded) {
1702
- window.addEventListener('resize', resizeCanvas);
1703
- resizeListenerAdded = true;
1704
- }
1705
-
1706
- if (graphMode === 'knowledge') {
1707
- await loadKnowledgeGraph();
1708
- } else {
1709
- await loadActionsGraph();
1710
- }
1711
- setupGraphInteraction();
1712
- if (animationFrameId) {
1713
- cancelAnimationFrame(animationFrameId);
1714
- animationFrameId = null;
1715
- }
1716
- animateGraph();
1717
- }
1718
-
1719
- function resizeCanvas() {
1720
- if (!canvas) return;
1721
- const rect = canvas.parentElement.getBoundingClientRect();
1722
- canvas.width = rect.width;
1723
- canvas.height = rect.height;
1724
- }
1725
-
1726
- function switchGraphMode(mode) {
1727
- graphMode = mode;
1728
- document.getElementById('btn-mode-knowledge')?.classList.toggle('active', mode === 'knowledge');
1729
- document.getElementById('btn-mode-actions')?.classList.toggle('active', mode === 'actions');
1730
-
1731
- const descEl = document.getElementById('graph-mode-desc');
1732
- const legendEl = document.getElementById('graph-legend');
1733
-
1734
- if (mode === 'knowledge') {
1735
- if (descEl) descEl.textContent = 'Visualizing semantic memory nodes, error traps (conditional boost), code files, and extracted entities.';
1736
- if (legendEl) {
1737
- legendEl.innerHTML = '<div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div> Code File Node</div>' +
1738
- '<div class="legend-item"><div class="legend-dot" style="background: #fbbf24;"></div> Error Trap [TRAP] (conditional boost)</div>' +
1739
- '<div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Verified Rule</div>' +
1740
- '<div class="legend-item"><div class="legend-dot" style="background: #8b5cf6;"></div> Architecture Decision</div>' +
1741
- '<div class="legend-item"><div class="legend-dot" style="background: #ec4899;"></div> Entity Link</div>';
1742
- }
1743
- loadKnowledgeGraph();
1744
- } else {
1745
- if (descEl) descEl.textContent = 'Multi-Agent Action DAG: Topological task dependencies, animated glowing Frontier nodes, and mutex locks.';
1746
- if (legendEl) {
1747
- legendEl.innerHTML = '<div class="legend-item"><div class="legend-dot" style="background: #f59e0b; box-shadow: 0 0 8px #f59e0b;"></div> Ready Frontier (Unblocked)</div>' +
1748
- '<div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Active (In Progress)</div>' +
1749
- '<div class="legend-item"><div class="legend-dot" style="background: #6b7280;"></div> Blocked on Prereqs</div>' +
1750
- '<div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div> Completed Task</div>' +
1751
- '<div class="legend-item"><span style="color: #38bdf8; font-weight: bold; font-size: 14px;">&rarr;</span> Dependency Flow</div>' +
1752
- '<div class="legend-item"><span style="color: #ef4444; font-weight: bold; font-size: 14px;">- - -</span> Mutex Conflict</div>';
1753
- }
1754
- loadActionsGraph();
1755
- }
1756
- }
1757
-
1758
- async function loadKnowledgeGraph() {
1759
- const res = await fetch('/api/graph');
1760
- graphData = await res.json();
1761
- setupKnowledgePhysics(graphData);
1762
- }
1763
-
1764
- function setupKnowledgePhysics(data) {
1765
- const cx = canvas.width / 2;
1766
- const cy = canvas.height / 2;
1767
- nodes = (data.nodes || []).map((n, i) => {
1768
- const angle = (i / Math.max(1, data.nodes.length)) * Math.PI * 2;
1769
- const radius = 120 + Math.random() * 100;
1770
- return {
1771
- ...n,
1772
- x: cx + Math.cos(angle) * radius,
1773
- y: cy + Math.sin(angle) * radius,
1774
- vx: 0,
1775
- vy: 0,
1776
- radius: n.type === 'file' ? 14 : n.type === 'entity' ? 13 : n.kind === 'error_trap' ? 16 : 12,
1777
- };
1778
- });
1779
- edges = (data.edges || []).map(e => ({
1780
- source: e.source,
1781
- target: e.target,
1782
- type: 'link'
1783
- }));
1784
- }
1785
-
1786
- async function loadActionsGraph() {
1787
- const res = await fetch('/api/actions');
1788
- allActionsData = await res.json();
1789
- setupActionsPhysics(allActionsData);
1790
- }
1791
-
1792
- function setupActionsPhysics(data) {
1793
- const cx = canvas.width / 2;
1794
- const cy = canvas.height / 2;
1795
- const statusColors = {
1796
- done: '#10b981',
1797
- active: '#3b82f6',
1798
- frontier: '#f59e0b',
1799
- blocked: '#6b7280',
1800
- cancelled: '#ef4444',
1801
- pending: '#8b5cf6'
1802
- };
1803
-
1804
- nodes = (data.actions || []).map((a, i) => {
1805
- const isFrontier = a.isFrontier;
1806
- const statusKey = isFrontier ? 'frontier' : a.status;
1807
- const angle = (i / Math.max(1, data.actions.length)) * Math.PI * 2;
1808
- const radius = 110 + (i % 3) * 60;
1809
- return {
1810
- id: a.id,
1811
- label: a.title,
1812
- type: 'action',
1813
- status: a.status,
1814
- isFrontier,
1815
- priority: a.priority,
1816
- assigned_to: a.assigned_to,
1817
- color: statusColors[statusKey] || '#6b7280',
1818
- radius: isFrontier ? 18 : 14,
1819
- x: cx + Math.cos(angle) * radius,
1820
- y: cy + Math.sin(angle) * radius,
1821
- vx: 0,
1822
- vy: 0
1823
- };
1824
- });
1825
-
1826
- edges = (data.edges || []).map(e => ({
1827
- source: e.source_action_id,
1828
- target: e.target_action_id,
1829
- type: e.edge_type // 'requires' or 'conflicts_with'
1830
- }));
1831
- }
1832
-
1833
- function setupGraphInteraction() {
1834
- canvas.onmousedown = (e) => {
1835
- dragStartMouse = { x: e.clientX, y: e.clientY };
1836
- const mouseX = (e.offsetX - transform.x) / transform.k;
1837
- const mouseY = (e.offsetY - transform.y) / transform.k;
1838
- const hit = nodes.find(n => Math.hypot(n.x - mouseX, n.y - mouseY) < n.radius + 6);
1839
- if (hit) {
1840
- dragNode = hit;
1841
- } else {
1842
- isDragging = true;
1843
- dragStart = { x: e.offsetX - transform.x, y: e.offsetY - transform.y };
1844
- }
1845
- };
1846
-
1847
- canvas.onmousemove = (e) => {
1848
- if (dragNode) {
1849
- dragNode.x = (e.offsetX - transform.x) / transform.k;
1850
- dragNode.y = (e.offsetY - transform.y) / transform.k;
1851
- dragNode.vx = 0;
1852
- dragNode.vy = 0;
1853
- } else if (isDragging) {
1854
- transform.x = e.offsetX - dragStart.x;
1855
- transform.y = e.offsetY - dragStart.y;
1856
- }
1857
- };
1858
-
1859
- window.onmouseup = (e) => {
1860
- const mouseDist = Math.hypot(e.clientX - dragStartMouse.x, e.clientY - dragStartMouse.y);
1861
- if (dragNode && mouseDist < 6) {
1862
- if (dragNode.type === 'memory') {
1863
- openMemoryModal(dragNode.id);
1864
- } else if (dragNode.type === 'action') {
1865
- openActionModal(dragNode.id);
1866
- } else if (dragNode.type === 'file') {
1867
- switchTab('memories');
1868
- const searchInput = document.getElementById('search-input');
1869
- if (searchInput) {
1870
- searchInput.value = dragNode.label;
1871
- loadMemories(dragNode.label);
1872
- }
1873
- }
1874
- }
1875
- dragNode = null;
1876
- isDragging = false;
1877
- };
1878
-
1879
- canvas.onwheel = (e) => {
1880
- e.preventDefault();
1881
- const factor = e.deltaY < 0 ? 1.1 : 0.9;
1882
- transform.k = Math.max(0.3, Math.min(3, transform.k * factor));
1883
- };
1884
- }
1885
-
1886
- function zoomGraph(factor) {
1887
- transform.k = Math.max(0.3, Math.min(3, transform.k * factor));
1888
- }
1889
-
1890
- function resetGraphZoom() {
1891
- transform = { x: 0, y: 0, k: 1 };
1892
- }
1893
-
1894
- function toggleGraphPhysics() {
1895
- graphSimulationRunning = !graphSimulationRunning;
1896
- }
1897
-
1898
- function animateGraph() {
1899
- if (currentTab !== 'graph') {
1900
- animationFrameId = null;
1901
- return;
1902
- }
1903
-
1904
- if (graphSimulationRunning) {
1905
- // Node repulsion
1906
- for (let i = 0; i < nodes.length; i++) {
1907
- for (let j = i + 1; j < nodes.length; j++) {
1908
- const dx = nodes[j].x - nodes[i].x;
1909
- const dy = nodes[j].y - nodes[i].y;
1910
- const dist = Math.hypot(dx, dy) || 1;
1911
- if (dist < 220) {
1912
- const f = (220 - dist) / dist * 0.45;
1913
- nodes[i].vx -= dx * f * 0.05;
1914
- nodes[i].vy -= dy * f * 0.05;
1915
- nodes[j].vx += dx * f * 0.05;
1916
- nodes[j].vy += dy * f * 0.05;
1917
- }
1918
- }
1919
- }
1920
-
1921
- // Edge attraction
1922
- for (const e of edges) {
1923
- const s = nodes.find(n => n.id === e.source);
1924
- const t = nodes.find(n => n.id === e.target);
1925
- if (s && t) {
1926
- const dx = t.x - s.x;
1927
- const dy = t.y - s.y;
1928
- const spring = e.type === 'conflicts_with' ? 0.001 : 0.0035;
1929
- s.vx += dx * spring;
1930
- s.vy += dy * spring;
1931
- t.vx -= dx * spring;
1932
- t.vy -= dy * spring;
1933
- }
1934
- }
1935
-
1936
- // Apply friction
1937
- for (const n of nodes) {
1938
- if (n !== dragNode) {
1939
- n.x += n.vx;
1940
- n.y += n.vy;
1941
- n.vx *= 0.85;
1942
- n.vy *= 0.85;
1943
- }
1944
- }
1945
- }
1946
-
1947
- // Draw frame
1948
- ctx.clearRect(0, 0, canvas.width, canvas.height);
1949
- ctx.save();
1950
- ctx.translate(transform.x, transform.y);
1951
- ctx.scale(transform.k, transform.k);
1952
-
1953
- // Draw edges
1954
- for (const e of edges) {
1955
- const s = nodes.find(n => n.id === e.source);
1956
- const t = nodes.find(n => n.id === e.target);
1957
- if (s && t) {
1958
- ctx.beginPath();
1959
- if (e.type === 'conflicts_with') {
1960
- ctx.setLineDash([6, 4]);
1961
- ctx.strokeStyle = '#ef4444';
1962
- ctx.lineWidth = 2;
1963
- ctx.moveTo(s.x, s.y);
1964
- ctx.lineTo(t.x, t.y);
1965
- ctx.stroke();
1966
- ctx.setLineDash([]);
1967
- } else if (e.type === 'requires') {
1968
- ctx.setLineDash([]);
1969
- ctx.strokeStyle = '#38bdf8';
1970
- ctx.lineWidth = 1.8;
1971
- ctx.moveTo(s.x, s.y);
1972
- ctx.lineTo(t.x, t.y);
1973
- ctx.stroke();
1974
-
1975
- // Directed arrow from prerequisite (target) to dependent (source)
1976
- const angle = Math.atan2(s.y - t.y, s.x - t.x);
1977
- const arrowDist = s.radius + 3;
1978
- const arrowX = s.x - Math.cos(angle) * arrowDist;
1979
- const arrowY = s.y - Math.sin(angle) * arrowDist;
1980
- ctx.beginPath();
1981
- ctx.moveTo(arrowX, arrowY);
1982
- ctx.lineTo(arrowX - 9 * Math.cos(angle - Math.PI / 6), arrowY - 9 * Math.sin(angle - Math.PI / 6));
1983
- ctx.lineTo(arrowX - 9 * Math.cos(angle + Math.PI / 6), arrowY - 9 * Math.sin(angle + Math.PI / 6));
1984
- ctx.fillStyle = '#38bdf8';
1985
- ctx.fill();
1986
- } else {
1987
- ctx.setLineDash([]);
1988
- ctx.strokeStyle = '#374151';
1989
- ctx.lineWidth = 1.5;
1990
- ctx.moveTo(s.x, s.y);
1991
- ctx.lineTo(t.x, t.y);
1992
- ctx.stroke();
1993
- }
1994
- }
1995
- }
1996
-
1997
- // Draw nodes
1998
- for (const n of nodes) {
1999
- // Frontier glowing aura animation
2000
- if (n.isFrontier) {
2001
- const pulse = (Math.sin(Date.now() / 220) + 1) * 3.5;
2002
- ctx.beginPath();
2003
- ctx.arc(n.x, n.y, n.radius + pulse + 3, 0, Math.PI * 2);
2004
- ctx.fillStyle = 'rgba(245, 158, 11, 0.22)';
2005
- ctx.fill();
2006
- ctx.strokeStyle = '#f59e0b';
2007
- ctx.lineWidth = 1.5;
2008
- ctx.stroke();
2009
- }
2010
-
2011
- ctx.beginPath();
2012
- ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
2013
-
2014
- if (n.type === 'action') {
2015
- ctx.fillStyle = n.color;
2016
- } else if (n.type === 'file') {
2017
- ctx.fillStyle = '#10b981';
2018
- } else if (n.type === 'entity') {
2019
- ctx.fillStyle = '#ec4899';
2020
- } else if (n.kind === 'error_trap') {
2021
- ctx.fillStyle = '#fbbf24';
2022
- } else if (n.kind === 'decision') {
2023
- ctx.fillStyle = '#8b5cf6';
2024
- } else {
2025
- ctx.fillStyle = '#3b82f6';
2026
- }
2027
- ctx.fill();
2028
- ctx.strokeStyle = '#111827';
2029
- ctx.lineWidth = 2.5;
2030
- ctx.stroke();
2031
-
2032
- // Node Label
2033
- ctx.fillStyle = '#e2e8f0';
2034
- ctx.font = '11px -apple-system, BlinkMacSystemFont, sans-serif';
2035
- ctx.textAlign = 'center';
2036
- const labelText = n.type === 'action' ? ('[P' + n.priority + '] ' + n.label) : n.label;
2037
- const truncated = labelText.length > 22 ? labelText.slice(0, 20) + '...' : labelText;
2038
- ctx.fillText(truncated, n.x, n.y + n.radius + 14);
2039
- }
2040
-
2041
- ctx.restore();
2042
- animationFrameId = requestAnimationFrame(animateGraph);
2043
- }
2044
-
2045
- // Debounce search
2046
- let debounceTimer;
2047
- document.getElementById('search-input')?.addEventListener('input', (e) => {
2048
- clearTimeout(debounceTimer);
2049
- debounceTimer = setTimeout(() => {
2050
- loadMemories(e.target.value);
2051
- }, 200);
2052
- });
2053
-
2054
- loadStatus();
2055
- loadMemories();
2056
- loadActionsBoard();
2057
- loadConflictsList();
2058
- </script>
2059
- </body>
812
+ return `<!DOCTYPE html>
813
+ <html lang="en">
814
+ <head>
815
+ <meta charset="UTF-8">
816
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
817
+ <title>⚡ FastrMemory Hub &bull; ${escapeHtmlServer(projectName)}</title>
818
+ <style>
819
+ :root {
820
+ --bg: #090d16;
821
+ --surface: #111827;
822
+ --card: #1f2937;
823
+ --border: #374151;
824
+ --text: #f9fafb;
825
+ --text-muted: #9ca3af;
826
+ --primary: #3b82f6;
827
+ --emerald: #10b981;
828
+ --amber: #f59e0b;
829
+ --purple: #8b5cf6;
830
+ --rose: #f43f5e;
831
+ }
832
+ * { box-sizing: border-box; margin: 0; padding: 0; }
833
+ body {
834
+ background: var(--bg);
835
+ color: var(--text);
836
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
837
+ min-height: 100vh;
838
+ display: flex;
839
+ flex-direction: column;
840
+ overflow-x: hidden;
841
+ }
842
+ header {
843
+ background: var(--surface);
844
+ border-bottom: 1px solid var(--border);
845
+ padding: 14px 24px;
846
+ display: flex;
847
+ align-items: center;
848
+ justify-content: space-between;
849
+ position: sticky;
850
+ top: 0;
851
+ z-index: 50;
852
+ }
853
+ .brand {
854
+ display: flex;
855
+ align-items: center;
856
+ gap: 12px;
857
+ font-size: 19px;
858
+ font-weight: 700;
859
+ letter-spacing: -0.4px;
860
+ }
861
+ .brand span { color: var(--primary); }
862
+ .live-dot {
863
+ width: 8px;
864
+ height: 8px;
865
+ background: var(--emerald);
866
+ border-radius: 50%;
867
+ box-shadow: 0 0 10px var(--emerald);
868
+ animation: pulse 2s infinite;
869
+ }
870
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
871
+
872
+ .nav-tabs {
873
+ display: flex;
874
+ gap: 4px;
875
+ background: var(--bg);
876
+ padding: 4px;
877
+ border-radius: 8px;
878
+ border: 1px solid var(--border);
879
+ }
880
+ .tab-btn {
881
+ background: transparent;
882
+ border: none;
883
+ color: var(--text-muted);
884
+ padding: 6px 14px;
885
+ border-radius: 6px;
886
+ font-size: 13px;
887
+ font-weight: 600;
888
+ cursor: pointer;
889
+ display: flex;
890
+ align-items: center;
891
+ gap: 6px;
892
+ transition: all 0.15s;
893
+ }
894
+ .tab-btn:hover { color: var(--text); background: rgba(255, 255, 255, 0.05); }
895
+ .tab-btn.active { color: #fff; background: var(--card); border: 1px solid var(--border); }
896
+ .badge {
897
+ font-size: 10px;
898
+ font-weight: 700;
899
+ padding: 2px 7px;
900
+ border-radius: 9999px;
901
+ text-transform: uppercase;
902
+ letter-spacing: 0.5px;
903
+ }
904
+ .badge-rule { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
905
+ .badge-trap { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.3); }
906
+ .badge-decision { background: rgba(139, 92, 246, 0.15); color: #a78bfa; border: 1px solid rgba(139, 92, 246, 0.3); }
907
+ .badge-file { background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); }
908
+
909
+ .stats-bar {
910
+ display: grid;
911
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
912
+ gap: 14px;
913
+ padding: 18px 24px;
914
+ background: var(--surface);
915
+ border-bottom: 1px solid var(--border);
916
+ }
917
+ .stat-card {
918
+ background: var(--card);
919
+ border: 1px solid var(--border);
920
+ padding: 12px 18px;
921
+ border-radius: 8px;
922
+ }
923
+ .stat-val { font-size: 24px; font-weight: 700; color: #fff; }
924
+ .stat-lbl { font-size: 11px; color: var(--text-muted); text-transform: uppercase; margin-top: 3px; font-weight: 600; }
925
+
926
+ main {
927
+ padding: 24px;
928
+ flex: 1;
929
+ max-width: 1440px;
930
+ margin: 0 auto;
931
+ width: 100%;
932
+ }
933
+ .tab-content { display: none; }
934
+ .tab-content.active { display: block; }
935
+
936
+ .search-row {
937
+ display: flex;
938
+ gap: 12px;
939
+ margin-bottom: 16px;
940
+ flex-wrap: wrap;
941
+ }
942
+ .search-input {
943
+ flex: 1;
944
+ min-width: 280px;
945
+ background: var(--card);
946
+ border: 1px solid var(--border);
947
+ border-radius: 8px;
948
+ padding: 11px 16px;
949
+ color: var(--text);
950
+ font-size: 14px;
951
+ outline: none;
952
+ transition: border-color 0.2s;
953
+ }
954
+ .search-input:focus { border-color: var(--primary); }
955
+
956
+ .filter-pills {
957
+ display: flex;
958
+ gap: 8px;
959
+ flex-wrap: wrap;
960
+ margin-bottom: 20px;
961
+ }
962
+ .pill {
963
+ background: var(--card);
964
+ border: 1px solid var(--border);
965
+ color: var(--text-muted);
966
+ padding: 5px 12px;
967
+ border-radius: 20px;
968
+ font-size: 12px;
969
+ cursor: pointer;
970
+ font-weight: 500;
971
+ transition: all 0.15s;
972
+ }
973
+ .pill:hover { color: #fff; }
974
+ .pill.active { background: var(--primary); color: #fff; border-color: var(--primary); }
975
+
976
+ .grid {
977
+ display: grid;
978
+ grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
979
+ gap: 16px;
980
+ }
981
+ .card {
982
+ background: var(--surface);
983
+ border: 1px solid var(--border);
984
+ border-radius: 10px;
985
+ padding: 16px;
986
+ display: flex;
987
+ flex-direction: column;
988
+ gap: 10px;
989
+ cursor: pointer;
990
+ transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
991
+ }
992
+ .card:hover { transform: translateY(-2px); border-color: #6b7280; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); }
993
+ .card-trap { border-left: 4px solid var(--amber); }
994
+ .card-header {
995
+ display: flex;
996
+ justify-content: space-between;
997
+ align-items: flex-start;
998
+ gap: 8px;
999
+ }
1000
+ .card-title { font-size: 15px; font-weight: 600; color: #fff; line-height: 1.4; }
1001
+ .card-content {
1002
+ font-size: 13px;
1003
+ line-height: 1.5;
1004
+ color: #d1d5db;
1005
+ background: var(--card);
1006
+ padding: 10px 12px;
1007
+ border-radius: 6px;
1008
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1009
+ max-height: 120px;
1010
+ overflow: hidden;
1011
+ text-overflow: ellipsis;
1012
+ }
1013
+ .card-footer {
1014
+ display: flex;
1015
+ justify-content: space-between;
1016
+ align-items: center;
1017
+ font-size: 11px;
1018
+ color: var(--text-muted);
1019
+ margin-top: auto;
1020
+ }
1021
+ .files-list { display: flex; flex-wrap: wrap; gap: 4px; }
1022
+ .file-tag {
1023
+ background: rgba(255, 255, 255, 0.05);
1024
+ border: 1px solid rgba(255, 255, 255, 0.1);
1025
+ padding: 2px 6px;
1026
+ border-radius: 4px;
1027
+ font-size: 11px;
1028
+ font-family: monospace;
1029
+ color: #93c5fd;
1030
+ }
1031
+
1032
+ /* Interactive Graph Canvas View */
1033
+ #graph-container {
1034
+ position: relative;
1035
+ width: 100%;
1036
+ height: 650px;
1037
+ background: #06090e;
1038
+ border: 1px solid var(--border);
1039
+ border-radius: 10px;
1040
+ overflow: hidden;
1041
+ }
1042
+ #graph-canvas {
1043
+ width: 100%;
1044
+ height: 100%;
1045
+ cursor: grab;
1046
+ }
1047
+ #graph-canvas:active { cursor: grabbing; }
1048
+ .graph-controls {
1049
+ position: absolute;
1050
+ bottom: 16px;
1051
+ right: 16px;
1052
+ display: flex;
1053
+ gap: 8px;
1054
+ background: rgba(17, 24, 39, 0.85);
1055
+ backdrop-filter: blur(8px);
1056
+ padding: 6px;
1057
+ border-radius: 8px;
1058
+ border: 1px solid var(--border);
1059
+ }
1060
+ .graph-btn {
1061
+ background: var(--card);
1062
+ border: 1px solid var(--border);
1063
+ color: #fff;
1064
+ padding: 6px 12px;
1065
+ border-radius: 6px;
1066
+ font-size: 12px;
1067
+ cursor: pointer;
1068
+ }
1069
+ .graph-btn:hover { background: #374151; }
1070
+ .graph-legend {
1071
+ position: absolute;
1072
+ top: 16px;
1073
+ left: 16px;
1074
+ background: rgba(17, 24, 39, 0.85);
1075
+ backdrop-filter: blur(8px);
1076
+ padding: 10px 14px;
1077
+ border-radius: 8px;
1078
+ border: 1px solid var(--border);
1079
+ font-size: 12px;
1080
+ display: flex;
1081
+ flex-direction: column;
1082
+ gap: 6px;
1083
+ }
1084
+ .legend-item { display: flex; align-items: center; gap: 8px; }
1085
+ .legend-dot { width: 10px; height: 10px; border-radius: 50%; }
1086
+
1087
+ /* Modal Drawer */
1088
+ .modal-overlay {
1089
+ display: none;
1090
+ position: fixed;
1091
+ inset: 0;
1092
+ background: rgba(0, 0, 0, 0.75);
1093
+ backdrop-filter: blur(4px);
1094
+ z-index: 100;
1095
+ align-items: center;
1096
+ justify-content: center;
1097
+ }
1098
+ .modal-overlay.active { display: flex; }
1099
+ .modal-card {
1100
+ background: var(--surface);
1101
+ border: 1px solid var(--border);
1102
+ border-radius: 12px;
1103
+ width: 90%;
1104
+ max-width: 700px;
1105
+ max-height: 85vh;
1106
+ display: flex;
1107
+ flex-direction: column;
1108
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
1109
+ }
1110
+ .modal-header {
1111
+ padding: 16px 20px;
1112
+ border-bottom: 1px solid var(--border);
1113
+ display: flex;
1114
+ justify-content: space-between;
1115
+ align-items: center;
1116
+ }
1117
+ .modal-body {
1118
+ padding: 20px;
1119
+ overflow-y: auto;
1120
+ display: flex;
1121
+ flex-direction: column;
1122
+ gap: 16px;
1123
+ }
1124
+ .modal-close {
1125
+ background: transparent;
1126
+ border: none;
1127
+ color: var(--text-muted);
1128
+ font-size: 20px;
1129
+ cursor: pointer;
1130
+ }
1131
+ .modal-close:hover { color: #fff; }
1132
+ </style>
1133
+ </head>
1134
+ <body>
1135
+ <header>
1136
+ <div class="brand">
1137
+ <div class="live-dot"></div>
1138
+ <span>⚡ FastrMemory</span> Hub
1139
+ </div>
1140
+
1141
+ <div class="nav-tabs">
1142
+ <button class="tab-btn active" onclick="switchTab('overview')">📊 Overview</button>
1143
+ <button class="tab-btn" onclick="switchTab('memories')">🧠 Memories (<span id="tab-cnt-mem">0</span>)</button>
1144
+ <button class="tab-btn" onclick="switchTab('traps')">⚠️ Traps (<span id="tab-cnt-traps">0</span>)</button>
1145
+ <button class="tab-btn" onclick="switchTab('graph')">🕸️ Visual Graph & DAG</button>
1146
+ <button class="tab-btn" onclick="switchTab('actions')">🔀 Actions & Frontier (<span id="tab-cnt-actions">0</span>)</button>
1147
+ <button class="tab-btn" onclick="switchTab('conflicts')">⚔️ Conflicts (<span id="tab-cnt-conflicts">0</span>)</button>
1148
+ <button class="tab-btn" onclick="switchTab('timeline')">⏱️ Timeline</button>
1149
+ </div>
1150
+
1151
+ <div style="font-size: 12px; color: var(--text-muted);">
1152
+ Node 24 WAL &bull; <code style="color: #60a5fa;">${escapeHtmlServer(projectId)}</code>
1153
+ </div>
1154
+ </header>
1155
+
1156
+ <section class="stats-bar">
1157
+ <div class="stat-card">
1158
+ <div class="stat-val" id="stat-active">0</div>
1159
+ <div class="stat-lbl">Active Rules</div>
1160
+ </div>
1161
+ <div class="stat-card">
1162
+ <div class="stat-val" id="stat-traps" style="color: #fbbf24;">0</div>
1163
+ <div class="stat-lbl">Error Traps (Conditional Boost)</div>
1164
+ </div>
1165
+ <div class="stat-card">
1166
+ <div class="stat-val" id="stat-frontier" style="color: #f59e0b;">0</div>
1167
+ <div class="stat-lbl">Unblocked Frontier Tasks</div>
1168
+ </div>
1169
+ <div class="stat-card">
1170
+ <div class="stat-val" id="stat-graph-nodes" style="color: #38bdf8;">0</div>
1171
+ <div class="stat-lbl">Knowledge Nodes</div>
1172
+ </div>
1173
+ <div class="stat-card">
1174
+ <div class="stat-val" id="stat-conflicts" style="color: #f43f5e;">0</div>
1175
+ <div class="stat-lbl">Open Contradictions</div>
1176
+ </div>
1177
+ <div class="stat-card">
1178
+ <div class="stat-val" id="stat-spool" style="color: #34d399;">Clear</div>
1179
+ <div class="stat-lbl">Offline Spool Queue</div>
1180
+ </div>
1181
+ </section>
1182
+
1183
+ <main>
1184
+ <!-- TAB 1: OVERVIEW -->
1185
+ <div id="tab-overview" class="tab-content active">
1186
+ <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 20px;">
1187
+ <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 24px;">
1188
+ <h2 style="font-size: 18px; margin-bottom: 12px;">Cognitive Memory Architecture</h2>
1189
+ <p style="color: var(--text-muted); font-size: 14px; line-height: 1.6; margin-bottom: 20px;">
1190
+ FastrMemory runs natively inside Node 24 with <b>zero background daemons</b>. Rules, error traps, and decisions are indexed via SQLite WAL mode, mirrored to <code>.fastr/pages/*.md</code> for Git diffs, and fused using Triple-Stream RRF ($k=20$) across full-text, local vectors, and code file graphs.
1191
+ </p>
1192
+ <div style="display: flex; gap: 10px;">
1193
+ <button class="pill active" onclick="switchTab('traps')">Inspect Error Traps &rarr;</button>
1194
+ <button class="pill" onclick="switchTab('graph')">Open Knowledge Graph &rarr;</button>
1195
+ </div>
1196
+ </div>
1197
+
1198
+ <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 24px;">
1199
+ <h3 style="font-size: 15px; margin-bottom: 12px;">Quick CLI Shortcuts</h3>
1200
+ <ul style="color: var(--text-muted); font-size: 13px; line-height: 2; list-style: none;">
1201
+ <li><code>fastrmemory status</code> &bull; Health overview</li>
1202
+ <li><code>fastrmemory bench</code> &bull; Run Hit@5 benchmarks</li>
1203
+ <li><code>fastrmemory recall &lt;q&gt;</code> &bull; Triple-stream search</li>
1204
+ <li><code>fastrmemory remember &lt;t&gt;</code> &bull; Save verified rule</li>
1205
+ </ul>
1206
+ </div>
1207
+ </div>
1208
+ </div>
1209
+
1210
+ <!-- TAB 2: ALL MEMORIES -->
1211
+ <div id="tab-memories" class="tab-content">
1212
+ <div class="search-row">
1213
+ <input type="text" id="search-input" class="search-input" placeholder="Instant search by keyword, file path, concept, or [mem_id]..." />
1214
+ </div>
1215
+ <div class="filter-pills" id="module-pills">
1216
+ <button class="pill active" onclick="filterByModule('all')">All Modules</button>
1217
+ </div>
1218
+ <div class="grid" id="memories-grid"></div>
1219
+ </div>
1220
+
1221
+ <!-- TAB 3: ERROR TRAPS -->
1222
+ <div id="tab-traps" class="tab-content">
1223
+ <div style="margin-bottom: 16px; color: var(--text-muted); font-size: 13px;">
1224
+ Error traps receive a <b>conditional 2.0x boost</b> during recall when the query has trap intent or targets a linked file, so agents do not repeat known bugs.
1225
+ </div>
1226
+ <div class="grid" id="traps-grid"></div>
1227
+ </div>
1228
+
1229
+ <!-- TAB 4: VISUAL GRAPH & ACTION DAG -->
1230
+ <div id="tab-graph" class="tab-content">
1231
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; flex-wrap: wrap; gap: 10px;">
1232
+ <div style="display: flex; gap: 8px;">
1233
+ <button id="btn-mode-knowledge" class="pill active" onclick="switchGraphMode('knowledge')">🕸️ Knowledge Graph</button>
1234
+ <button id="btn-mode-actions" class="pill" onclick="switchGraphMode('actions')">🔀 Action DAG & Frontier</button>
1235
+ </div>
1236
+ <div id="graph-mode-desc" style="color: var(--text-muted); font-size: 13px;">
1237
+ Visualizing semantic memory nodes, error traps (conditional boost), code files, and extracted entities.
1238
+ </div>
1239
+ </div>
1240
+ <div id="graph-container">
1241
+ <div class="graph-legend" id="graph-legend">
1242
+ <div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div> Code File Node</div>
1243
+ <div class="legend-item"><div class="legend-dot" style="background: #fbbf24;"></div> Error Trap [TRAP] (conditional boost)</div>
1244
+ <div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Verified Rule</div>
1245
+ <div class="legend-item"><div class="legend-dot" style="background: #8b5cf6;"></div> Architecture Decision</div>
1246
+ <div class="legend-item"><div class="legend-dot" style="background: #ec4899;"></div> Entity Link</div>
1247
+ </div>
1248
+ <div class="graph-controls">
1249
+ <button class="graph-btn" onclick="zoomGraph(1.2)">Zoom +</button>
1250
+ <button class="graph-btn" onclick="zoomGraph(0.8)">Zoom -</button>
1251
+ <button class="graph-btn" onclick="resetGraphZoom()">Reset View</button>
1252
+ <button class="graph-btn" onclick="toggleGraphPhysics()">Pause/Play Physics</button>
1253
+ </div>
1254
+ <canvas id="graph-canvas"></canvas>
1255
+ </div>
1256
+ </div>
1257
+
1258
+ <!-- TAB 5: ACTIONS & FRONTIER -->
1259
+ <div id="tab-actions" class="tab-content">
1260
+ <div style="margin-bottom: 16px; color: var(--text-muted); font-size: 13px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
1261
+ <span>Multi-Agent Action DAG ensures zero task duplicate collisions and enforces topological execution order.</span>
1262
+ <button class="pill active" onclick="switchTab('graph'); switchGraphMode('actions');">Inspect in Visual DAG &rarr;</button>
1263
+ </div>
1264
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px;">
1265
+ <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1266
+ <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #f59e0b; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1267
+ <span>⚡ Ready Frontier</span>
1268
+ <span id="col-cnt-frontier" class="badge badge-trap">0</span>
1269
+ </h3>
1270
+ <div id="list-frontier" style="display: flex; flex-direction: column; gap: 10px;"></div>
1271
+ </div>
1272
+ <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1273
+ <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #3b82f6; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1274
+ <span>🔨 Active / In Progress</span>
1275
+ <span id="col-cnt-active" class="badge badge-rule">0</span>
1276
+ </h3>
1277
+ <div id="list-active" style="display: flex; flex-direction: column; gap: 10px;"></div>
1278
+ </div>
1279
+ <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1280
+ <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #9ca3af; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1281
+ <span>⏳ Blocked (Prereqs)</span>
1282
+ <span id="col-cnt-blocked" class="badge" style="background: rgba(156, 163, 175, 0.2); color: #9ca3af;">0</span>
1283
+ </h3>
1284
+ <div id="list-blocked" style="display: flex; flex-direction: column; gap: 10px;"></div>
1285
+ </div>
1286
+ <div style="background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px;">
1287
+ <h3 style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: #10b981; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;">
1288
+ <span>✅ Completed</span>
1289
+ <span id="col-cnt-done" class="badge badge-file">0</span>
1290
+ </h3>
1291
+ <div id="list-done" style="display: flex; flex-direction: column; gap: 10px;"></div>
1292
+ </div>
1293
+ </div>
1294
+ </div>
1295
+
1296
+ <!-- TAB 6: CONFLICTS -->
1297
+ <div id="tab-conflicts" class="tab-content">
1298
+ <div style="margin-bottom: 16px; color: var(--text-muted); font-size: 13px;">
1299
+ FastrMemory proactively detects contradictions between newly proposed facts and existing rules before they confuse autonomous coding agents.
1300
+ </div>
1301
+ <div id="conflicts-list" style="display: flex; flex-direction: column; gap: 14px;"></div>
1302
+ </div>
1303
+
1304
+ <!-- TAB 7: TIMELINE -->
1305
+ <div id="tab-timeline" class="tab-content">
1306
+ <div id="timeline-list" style="display: flex; flex-direction: column; gap: 12px;"></div>
1307
+ </div>
1308
+ </main>
1309
+
1310
+ <!-- DETAIL MODAL: MEMORY -->
1311
+ <div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
1312
+ <div class="modal-card" onclick="event.stopPropagation()">
1313
+ <div class="modal-header">
1314
+ <div id="modal-title" style="font-weight: 700; font-size: 16px;"></div>
1315
+ <button class="modal-close" onclick="closeModal()">&times;</button>
1316
+ </div>
1317
+ <div class="modal-body">
1318
+ <div style="display: flex; gap: 8px;" id="modal-badges"></div>
1319
+ <div style="font-size: 13px; color: var(--text-muted);">
1320
+ <b>ID:</b> <code id="modal-id" style="color: #60a5fa;"></code> &bull;
1321
+ <b>Module:</b> <span id="modal-module"></span> &bull;
1322
+ <b>Score:</b> <span id="modal-score"></span>
1323
+ </div>
1324
+ <div style="font-size: 13px; color: var(--text-muted);" id="modal-files-row"></div>
1325
+ <pre id="modal-content" style="background: var(--card); padding: 14px; border-radius: 8px; font-family: monospace; font-size: 13px; white-space: pre-wrap; line-height: 1.6;"></pre>
1326
+ </div>
1327
+ </div>
1328
+ </div>
1329
+
1330
+ <!-- DETAIL MODAL: ACTION -->
1331
+ <div class="modal-overlay" id="action-modal-overlay" onclick="closeActionModal(event)">
1332
+ <div class="modal-card" onclick="event.stopPropagation()">
1333
+ <div class="modal-header">
1334
+ <div id="act-modal-title" style="font-weight: 700; font-size: 16px;"></div>
1335
+ <button class="modal-close" onclick="closeActionModal()">&times;</button>
1336
+ </div>
1337
+ <div class="modal-body">
1338
+ <div style="display: flex; gap: 8px;" id="act-modal-badges"></div>
1339
+ <div style="font-size: 13px; color: var(--text-muted);">
1340
+ <b>Action ID:</b> <code id="act-modal-id" style="color: #60a5fa;"></code> &bull;
1341
+ <b>Priority:</b> <span id="act-modal-priority"></span> &bull;
1342
+ <b>Assigned To:</b> <span id="act-modal-agent"></span>
1343
+ </div>
1344
+ <div style="font-size: 13px; color: var(--text-muted);" id="act-modal-reqs"></div>
1345
+ <div style="font-size: 13px; color: var(--text-muted);" id="act-modal-unlocks"></div>
1346
+ <div style="font-size: 13px; color: var(--text-muted);" id="act-modal-conflicts"></div>
1347
+ <div style="font-size: 13px; color: var(--text-muted);"><b>Description:</b></div>
1348
+ <pre id="act-modal-desc" style="background: var(--card); padding: 14px; border-radius: 8px; font-family: monospace; font-size: 13px; white-space: pre-wrap; line-height: 1.6;"></pre>
1349
+ </div>
1350
+ </div>
1351
+ </div>
1352
+
1353
+ <script>
1354
+ let currentTab = 'overview';
1355
+ let allMemories = [];
1356
+ let currentModule = 'all';
1357
+ let graphMode = 'knowledge'; // 'knowledge' | 'actions'
1358
+ let graphData = { nodes: [], edges: [] };
1359
+ let allActionsData = { actions: [], edges: [], frontierCount: 0 };
1360
+ let graphSimulationRunning = true;
1361
+
1362
+ function escapeHtml(str) {
1363
+ if (!str) return '';
1364
+ return String(str)
1365
+ .replace(/&/g, '&amp;')
1366
+ .replace(/</g, '&lt;')
1367
+ .replace(/>/g, '&gt;')
1368
+ .replace(/"/g, '&quot;')
1369
+ .replace(/'/g, '&#039;');
1370
+ }
1371
+
1372
+ function switchTab(tabId) {
1373
+ currentTab = tabId;
1374
+ document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
1375
+ document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
1376
+
1377
+ const targetBtn = Array.from(document.querySelectorAll('.tab-btn')).find(b => b.getAttribute('onclick')?.includes(tabId));
1378
+ if (targetBtn) targetBtn.classList.add('active');
1379
+
1380
+ const targetContent = document.getElementById('tab-' + tabId);
1381
+ if (targetContent) targetContent.classList.add('active');
1382
+
1383
+ if (tabId === 'graph') {
1384
+ initGraphCanvas();
1385
+ } else if (tabId === 'actions') {
1386
+ loadActionsBoard();
1387
+ } else if (tabId === 'conflicts') {
1388
+ loadConflictsList();
1389
+ } else if (tabId === 'timeline') {
1390
+ loadTimeline();
1391
+ }
1392
+ }
1393
+
1394
+ async function loadStatus() {
1395
+ try {
1396
+ const res = await fetch('/api/status');
1397
+ const data = await res.json();
1398
+ document.getElementById('stat-active').textContent = data.activeCount ?? 0;
1399
+ document.getElementById('stat-traps').textContent = data.trapsCount ?? 0;
1400
+ document.getElementById('stat-frontier').textContent = data.frontierCount ?? 0;
1401
+ document.getElementById('stat-graph-nodes').textContent = data.graphNodesCount ?? 0;
1402
+ document.getElementById('stat-conflicts').textContent = data.conflictsCount ?? 0;
1403
+ document.getElementById('stat-spool').textContent = data.spoolPending ? 'Pending' : 'Clear (0)';
1404
+
1405
+ document.getElementById('tab-cnt-mem').textContent = data.activeCount ?? 0;
1406
+ document.getElementById('tab-cnt-traps').textContent = data.trapsCount ?? 0;
1407
+ document.getElementById('tab-cnt-actions').textContent = data.actionsCount ?? 0;
1408
+ document.getElementById('tab-cnt-conflicts').textContent = data.conflictsCount ?? 0;
1409
+
1410
+ // Populate module filter pills
1411
+ const pillsContainer = document.getElementById('module-pills');
1412
+ if (pillsContainer) {
1413
+ pillsContainer.innerHTML = '<button class="pill active" onclick="filterByModule(\\'all\\')">All Modules</button>' +
1414
+ (data.modules || []).map(m => '<button class="pill" onclick="filterByModule(' + escapeHtml(JSON.stringify(m)) + ')">' + escapeHtml(m) + '</button>').join('');
1415
+ }
1416
+ } catch (err) {
1417
+ console.error('Failed to load status', err);
1418
+ }
1419
+ }
1420
+
1421
+ async function loadMemories(query = '') {
1422
+ try {
1423
+ let url = '/api/memories?q=' + encodeURIComponent(query);
1424
+ if (currentModule !== 'all') {
1425
+ url += '&module=' + encodeURIComponent(currentModule);
1426
+ }
1427
+ const res = await fetch(url);
1428
+ allMemories = await res.json();
1429
+
1430
+ renderMemoriesGrid(allMemories, 'memories-grid');
1431
+
1432
+ // Also populate traps tab
1433
+ const traps = allMemories.filter(m => m.kind === 'error_trap');
1434
+ renderMemoriesGrid(traps, 'traps-grid');
1435
+ } catch (err) {
1436
+ console.error('Failed to load memories', err);
1437
+ }
1438
+ }
1439
+
1440
+ function filterByModule(mod) {
1441
+ currentModule = mod;
1442
+ document.querySelectorAll('#module-pills .pill').forEach(p => p.classList.remove('active'));
1443
+ const activePill = Array.from(document.querySelectorAll('#module-pills .pill')).find(p => p.textContent === mod || (mod === 'all' && p.textContent === 'All Modules'));
1444
+ if (activePill) activePill.classList.add('active');
1445
+ loadMemories(document.getElementById('search-input')?.value || '');
1446
+ }
1447
+
1448
+ function renderMemoriesGrid(memories, containerId) {
1449
+ const grid = document.getElementById(containerId);
1450
+ if (!grid) return;
1451
+
1452
+ if (memories.length === 0) {
1453
+ grid.innerHTML = '<div style="color: var(--text-muted); padding: 40px; text-align: center; grid-column: 1 / -1;">No memories match your query.</div>';
1454
+ return;
1455
+ }
1456
+
1457
+ grid.innerHTML = memories.map(m => {
1458
+ const isTrap = m.kind === 'error_trap';
1459
+ const kindBadge = isTrap
1460
+ ? '<span class="badge badge-trap">[TRAP]</span>'
1461
+ : m.kind === 'decision'
1462
+ ? '<span class="badge badge-decision">[DECISION]</span>'
1463
+ : '<span class="badge badge-rule">[RULE]</span>';
1464
+
1465
+ const filesHtml = (m.linked_files && m.linked_files.length > 0)
1466
+ ? '<div class="files-list">' + m.linked_files.map(f => '<span class="file-tag">' + escapeHtml(f) + '</span>').join('') + '</div>'
1467
+ : '<span style="color: var(--text-muted);">No linked files</span>';
1468
+
1469
+ return '<div class="card ' + (isTrap ? 'card-trap' : '') + '" onclick="openMemoryModal(\\'' + m.id + '\\')">' +
1470
+ '<div class="card-header">' +
1471
+ '<div>' +
1472
+ '<div style="font-size: 11px; color: var(--text-muted); margin-bottom: 4px;">[' + m.id + '] &bull; ' + escapeHtml(m.module) + '</div>' +
1473
+ '<div class="card-title">' + escapeHtml(m.title) + '</div>' +
1474
+ '</div>' +
1475
+ kindBadge +
1476
+ '</div>' +
1477
+ '<div class="card-content">' + escapeHtml(m.content) + '</div>' +
1478
+ '<div class="card-footer">' +
1479
+ filesHtml +
1480
+ '<span>Score: ' + m.reward_score + '</span>' +
1481
+ '</div>' +
1482
+ '</div>';
1483
+ }).join('');
1484
+ }
1485
+
1486
+ async function loadTimeline() {
1487
+ try {
1488
+ const res = await fetch('/api/timeline?limit=50');
1489
+ const items = await res.json();
1490
+ const list = document.getElementById('timeline-list');
1491
+ if (!list) return;
1492
+
1493
+ list.innerHTML = items.map(item => {
1494
+ const isSuperseded = item.status === 'superseded';
1495
+ const badgeClass = item.kind === 'error_trap' ? 'badge-trap' : 'badge-rule';
1496
+ return '<div style="background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 18px; display: flex; justify-content: space-between; align-items: center;">' +
1497
+ '<div>' +
1498
+ '<div style="display: flex; gap: 8px; align-items: center; margin-bottom: 4px;">' +
1499
+ '<span class="badge ' + badgeClass + '">[' + item.kind.toUpperCase() + ']</span>' +
1500
+ '<span style="font-size: 11px; color: var(--text-muted);">' + item.createdAt + '</span>' +
1501
+ (isSuperseded ? '<span style="font-size: 10px; color: #f87171; border: 1px solid rgba(248,113,113,0.3); padding: 1px 5px; border-radius: 4px;">SUPERSEDED</span>' : '') +
1502
+ '</div>' +
1503
+ '<div style="font-weight: 600; font-size: 14px;">' + escapeHtml(item.title) + '</div>' +
1504
+ '<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">Files: ' + (item.linkedFiles.map(escapeHtml).join(', ') || 'None') + '</div>' +
1505
+ '</div>' +
1506
+ '<div style="font-size: 12px; color: #60a5fa;">Score: ' + item.rewardScore + '</div>' +
1507
+ '</div>';
1508
+ }).join('');
1509
+ } catch (err) {
1510
+ console.error('Failed to load timeline', err);
1511
+ }
1512
+ }
1513
+
1514
+ async function loadActionsBoard() {
1515
+ try {
1516
+ const res = await fetch('/api/actions');
1517
+ allActionsData = await res.json();
1518
+
1519
+ const frontier = allActionsData.actions.filter(a => a.isFrontier);
1520
+ const active = allActionsData.actions.filter(a => a.status === 'active');
1521
+ const blocked = allActionsData.actions.filter(a => a.status === 'blocked' || (a.status === 'pending' && !a.isFrontier));
1522
+ const done = allActionsData.actions.filter(a => a.status === 'done');
1523
+
1524
+ document.getElementById('col-cnt-frontier').textContent = frontier.length;
1525
+ document.getElementById('col-cnt-active').textContent = active.length;
1526
+ document.getElementById('col-cnt-blocked').textContent = blocked.length;
1527
+ document.getElementById('col-cnt-done').textContent = done.length;
1528
+
1529
+ renderActionList(frontier, 'list-frontier', 'frontier');
1530
+ renderActionList(active, 'list-active', 'active');
1531
+ renderActionList(blocked, 'list-blocked', 'blocked');
1532
+ renderActionList(done, 'list-done', 'done');
1533
+ } catch (err) {
1534
+ console.error('Failed to load actions board', err);
1535
+ }
1536
+ }
1537
+
1538
+ function renderActionList(items, containerId, category) {
1539
+ const container = document.getElementById(containerId);
1540
+ if (!container) return;
1541
+ if (items.length === 0) {
1542
+ container.innerHTML = '<div style="color: var(--text-muted); font-size: 12px; padding: 16px; border: 1px dashed var(--border); border-radius: 8px; text-align: center;">Empty</div>';
1543
+ return;
1544
+ }
1545
+ container.innerHTML = items.map(function(a) {
1546
+ var borderCol = category === 'frontier' ? '#f59e0b' : category === 'active' ? '#3b82f6' : category === 'done' ? '#10b981' : '#6b7280';
1547
+ var assignHtml = a.assigned_to ? '<span>&bull; ' + escapeHtml(a.assigned_to) + '</span>' : '<span style="color: #34d399;">&bull; Unassigned</span>';
1548
+ var reqHtml = (a.requires && a.requires.length > 0) ? '<span style="color: #94a3b8;">&bull; Req: ' + a.requires.length + '</span>' : '';
1549
+ return '<div class="card" onclick="openActionModal(\\'' + a.id + '\\')" style="padding: 12px; border-left: 3px solid ' + borderCol + ';">' +
1550
+ '<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 8px;">' +
1551
+ '<div style="font-size: 13px; font-weight: 600; color: #fff;">' + escapeHtml(a.title) + '</div>' +
1552
+ '<span class="badge" style="background: rgba(255,255,255,0.08); color: #cbd5e1;">P' + a.priority + '</span>' +
1553
+ '</div>' +
1554
+ '<div style="font-size: 11px; color: var(--text-muted); display: flex; gap: 8px; align-items: center; margin-top: 4px;">' +
1555
+ '<code>' + a.id + '</code>' +
1556
+ assignHtml +
1557
+ reqHtml +
1558
+ '</div>' +
1559
+ '</div>';
1560
+ }).join('');
1561
+ }
1562
+
1563
+ async function resolveConflictAction(conflictId, keepMemoryId, forgetLoser) {
1564
+ try {
1565
+ const res = await fetch('/api/conflicts/resolve', {
1566
+ method: 'POST',
1567
+ headers: { 'Content-Type': 'application/json' },
1568
+ body: JSON.stringify({ conflictId, keepMemoryId, forgetLoser, reason: 'Resolved via FastrMemory Hub' })
1569
+ });
1570
+ if (!res.ok) {
1571
+ const err = await res.json();
1572
+ alert('Failed to resolve: ' + (err.error || 'Unknown error'));
1573
+ return;
1574
+ }
1575
+ await loadConflictsList();
1576
+ await loadStatus();
1577
+ } catch (err) {
1578
+ alert('Resolution request failed: ' + err.message);
1579
+ }
1580
+ }
1581
+
1582
+ async function loadConflictsList() {
1583
+ try {
1584
+ const res = await fetch('/api/conflicts');
1585
+ const conflicts = await res.json();
1586
+ const container = document.getElementById('conflicts-list');
1587
+ if (!container) return;
1588
+ if (conflicts.length === 0) {
1589
+ container.innerHTML = '<div style="color: #34d399; padding: 40px; text-align: center; background: var(--surface); border: 1px solid var(--border); border-radius: 10px;">✨ Zero open contradictions detected. All knowledge rules are consistent!</div>';
1590
+ return;
1591
+ }
1592
+ container.innerHTML = conflicts.map(function(c) {
1593
+ var scorePct = (c.score * 100).toFixed(0);
1594
+ return '<div class="card" style="border-left: 4px solid var(--rose); padding: 18px;">' +
1595
+ '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">' +
1596
+ '<span class="badge" style="background: rgba(244, 63, 94, 0.2); color: #fb7185;">Contradiction (' + scorePct + '% Conflict Score)</span>' +
1597
+ '<span style="font-size: 11px; color: var(--text-muted);">' + c.created_at + '</span>' +
1598
+ '</div>' +
1599
+ '<div style="font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 6px;">' + escapeHtml(c.reason) + '</div>' +
1600
+ '<div style="font-size: 12px; color: var(--text-muted); font-family: monospace; margin-bottom: 12px;">' +
1601
+ 'Memory A: <code style="color: #60a5fa;">' + c.memory_a_id + '</code> vs Memory B: <code style="color: #f43f5e;">' + c.memory_b_id + '</code>' +
1602
+ '</div>' +
1603
+ '<div style="display: flex; gap: 8px; flex-wrap: wrap;">' +
1604
+ '<button class="pill" style="background: rgba(59, 130, 246, 0.15); border-color: #3b82f6; color: #93c5fd; font-size: 11px; padding: 4px 10px;" onclick="resolveConflictAction(' + c.id + ', \\'' + c.memory_a_id + '\\', true)">Keep ' + c.memory_a_id + ' (Deprecate B)</button>' +
1605
+ '<button class="pill" style="background: rgba(244, 63, 94, 0.15); border-color: #f43f5e; color: #fca5a5; font-size: 11px; padding: 4px 10px;" onclick="resolveConflictAction(' + c.id + ', \\'' + c.memory_b_id + '\\', true)">Keep ' + c.memory_b_id + ' (Deprecate A)</button>' +
1606
+ '<button class="pill" style="background: rgba(255, 255, 255, 0.05); color: #cbd5e1; font-size: 11px; padding: 4px 10px;" onclick="resolveConflictAction(' + c.id + ', \\'' + c.memory_a_id + '\\', false)">Keep Both (Weaken B)</button>' +
1607
+ '</div>' +
1608
+ '</div>';
1609
+ }).join('');
1610
+ } catch (err) {
1611
+ console.error('Failed to load conflicts', err);
1612
+ }
1613
+ }
1614
+
1615
+ async function openMemoryModal(id) {
1616
+ let mem = allMemories.find(m => m.id === id);
1617
+ if (!mem) {
1618
+ try {
1619
+ const res = await fetch('/api/memories?q=' + encodeURIComponent(id));
1620
+ const results = await res.json();
1621
+ mem = results.find(m => m.id === id);
1622
+ } catch (e) {}
1623
+ }
1624
+ if (!mem) return;
1625
+
1626
+ document.getElementById('modal-title').textContent = mem.title;
1627
+ document.getElementById('modal-id').textContent = mem.id;
1628
+ document.getElementById('modal-module').textContent = mem.module;
1629
+ document.getElementById('modal-score').textContent = mem.reward_score;
1630
+ document.getElementById('modal-content').textContent = mem.content;
1631
+
1632
+ const badgeContainer = document.getElementById('modal-badges');
1633
+ badgeContainer.innerHTML = '<span class="badge ' + (mem.kind === 'error_trap' ? 'badge-trap' : 'badge-rule') + '">[' + mem.kind.toUpperCase() + ']</span>' +
1634
+ '<span class="badge badge-file">' + mem.status.toUpperCase() + '</span>';
1635
+
1636
+ const filesRow = document.getElementById('modal-files-row');
1637
+ filesRow.innerHTML = (mem.linked_files && mem.linked_files.length > 0)
1638
+ ? '<b>Linked Files:</b> ' + mem.linked_files.map(f => '<code style="color: #93c5fd; margin-right: 6px;">' + escapeHtml(f) + '</code>').join('')
1639
+ : '<b>Linked Files:</b> None';
1640
+
1641
+ document.getElementById('modal-overlay').classList.add('active');
1642
+ }
1643
+
1644
+ function closeModal(e) {
1645
+ document.getElementById('modal-overlay').classList.remove('active');
1646
+ }
1647
+
1648
+ async function openActionModal(id) {
1649
+ if (!allActionsData || !allActionsData.actions) {
1650
+ try {
1651
+ const res = await fetch('/api/actions');
1652
+ allActionsData = await res.json();
1653
+ } catch (e) {}
1654
+ }
1655
+ const act = (allActionsData?.actions || []).find(a => a.id === id);
1656
+ if (!act) return;
1657
+
1658
+ document.getElementById('act-modal-title').textContent = act.title;
1659
+ document.getElementById('act-modal-id').textContent = act.id;
1660
+ document.getElementById('act-modal-priority').textContent = 'P' + act.priority;
1661
+ document.getElementById('act-modal-agent').textContent = act.assigned_to || 'Unassigned (Open for Claim)';
1662
+ document.getElementById('act-modal-desc').textContent = act.description || 'No detailed description.';
1663
+
1664
+ const badgeContainer = document.getElementById('act-modal-badges');
1665
+ const isFrontier = act.isFrontier;
1666
+ badgeContainer.innerHTML = (isFrontier ? '<span class="badge badge-trap">⚡ FRONTIER READY</span> ' : '') +
1667
+ '<span class="badge" style="background: rgba(255,255,255,0.1); color: #fff;">' + act.status.toUpperCase() + '</span>';
1668
+
1669
+ const reqsEl = document.getElementById('act-modal-reqs');
1670
+ reqsEl.innerHTML = (act.requires && act.requires.length > 0)
1671
+ ? '<b>Prerequisites (Requires):</b> ' + act.requires.map(r => '<code style="color: #93c5fd; margin-right: 6px;">' + escapeHtml(r) + '</code>').join('')
1672
+ : '<b>Prerequisites:</b> None (Unblocked)';
1673
+
1674
+ const unlocksEl = document.getElementById('act-modal-unlocks');
1675
+ unlocksEl.innerHTML = (act.unlocks && act.unlocks.length > 0)
1676
+ ? '<b>Unlocks Downstream:</b> ' + act.unlocks.map(u => '<code style="color: #34d399; margin-right: 6px;">' + escapeHtml(u) + '</code>').join('')
1677
+ : '<b>Unlocks Downstream:</b> None';
1678
+
1679
+ const conflictsEl = document.getElementById('act-modal-conflicts');
1680
+ conflictsEl.innerHTML = (act.conflictsWith && act.conflictsWith.length > 0)
1681
+ ? '<b style="color: #fb7185;">Mutex Conflicts With:</b> ' + act.conflictsWith.map(c => '<code style="color: #fb7185; margin-right: 6px;">' + escapeHtml(c) + '</code>').join('')
1682
+ : '<b>Mutex Conflicts:</b> None';
1683
+
1684
+ document.getElementById('action-modal-overlay').classList.add('active');
1685
+ }
1686
+
1687
+ function closeActionModal() {
1688
+ document.getElementById('action-modal-overlay').classList.remove('active');
1689
+ }
1690
+
1691
+ // --- HTML5 CANVAS VISUALIZER (KNOWLEDGE GRAPH & ACTION DAG) ---
1692
+ let canvas, ctx, nodes = [], edges = [], transform = { x: 0, y: 0, k: 1 };
1693
+ let isDragging = false, dragNode = null, dragStart = { x: 0, y: 0 }, dragStartMouse = { x: 0, y: 0 };
1694
+ let resizeListenerAdded = false, animationFrameId = null;
1695
+
1696
+ async function initGraphCanvas() {
1697
+ canvas = document.getElementById('graph-canvas');
1698
+ if (!canvas) return;
1699
+ ctx = canvas.getContext('2d');
1700
+ resizeCanvas();
1701
+ if (!resizeListenerAdded) {
1702
+ window.addEventListener('resize', resizeCanvas);
1703
+ resizeListenerAdded = true;
1704
+ }
1705
+
1706
+ if (graphMode === 'knowledge') {
1707
+ await loadKnowledgeGraph();
1708
+ } else {
1709
+ await loadActionsGraph();
1710
+ }
1711
+ setupGraphInteraction();
1712
+ if (animationFrameId) {
1713
+ cancelAnimationFrame(animationFrameId);
1714
+ animationFrameId = null;
1715
+ }
1716
+ animateGraph();
1717
+ }
1718
+
1719
+ function resizeCanvas() {
1720
+ if (!canvas) return;
1721
+ const rect = canvas.parentElement.getBoundingClientRect();
1722
+ canvas.width = rect.width;
1723
+ canvas.height = rect.height;
1724
+ }
1725
+
1726
+ function switchGraphMode(mode) {
1727
+ graphMode = mode;
1728
+ document.getElementById('btn-mode-knowledge')?.classList.toggle('active', mode === 'knowledge');
1729
+ document.getElementById('btn-mode-actions')?.classList.toggle('active', mode === 'actions');
1730
+
1731
+ const descEl = document.getElementById('graph-mode-desc');
1732
+ const legendEl = document.getElementById('graph-legend');
1733
+
1734
+ if (mode === 'knowledge') {
1735
+ if (descEl) descEl.textContent = 'Visualizing semantic memory nodes, error traps (conditional boost), code files, and extracted entities.';
1736
+ if (legendEl) {
1737
+ legendEl.innerHTML = '<div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div> Code File Node</div>' +
1738
+ '<div class="legend-item"><div class="legend-dot" style="background: #fbbf24;"></div> Error Trap [TRAP] (conditional boost)</div>' +
1739
+ '<div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Verified Rule</div>' +
1740
+ '<div class="legend-item"><div class="legend-dot" style="background: #8b5cf6;"></div> Architecture Decision</div>' +
1741
+ '<div class="legend-item"><div class="legend-dot" style="background: #ec4899;"></div> Entity Link</div>';
1742
+ }
1743
+ loadKnowledgeGraph();
1744
+ } else {
1745
+ if (descEl) descEl.textContent = 'Multi-Agent Action DAG: Topological task dependencies, animated glowing Frontier nodes, and mutex locks.';
1746
+ if (legendEl) {
1747
+ legendEl.innerHTML = '<div class="legend-item"><div class="legend-dot" style="background: #f59e0b; box-shadow: 0 0 8px #f59e0b;"></div> Ready Frontier (Unblocked)</div>' +
1748
+ '<div class="legend-item"><div class="legend-dot" style="background: #3b82f6;"></div> Active (In Progress)</div>' +
1749
+ '<div class="legend-item"><div class="legend-dot" style="background: #6b7280;"></div> Blocked on Prereqs</div>' +
1750
+ '<div class="legend-item"><div class="legend-dot" style="background: #10b981;"></div> Completed Task</div>' +
1751
+ '<div class="legend-item"><span style="color: #38bdf8; font-weight: bold; font-size: 14px;">&rarr;</span> Dependency Flow</div>' +
1752
+ '<div class="legend-item"><span style="color: #ef4444; font-weight: bold; font-size: 14px;">- - -</span> Mutex Conflict</div>';
1753
+ }
1754
+ loadActionsGraph();
1755
+ }
1756
+ }
1757
+
1758
+ async function loadKnowledgeGraph() {
1759
+ const res = await fetch('/api/graph');
1760
+ graphData = await res.json();
1761
+ setupKnowledgePhysics(graphData);
1762
+ }
1763
+
1764
+ function setupKnowledgePhysics(data) {
1765
+ const cx = canvas.width / 2;
1766
+ const cy = canvas.height / 2;
1767
+ nodes = (data.nodes || []).map((n, i) => {
1768
+ const angle = (i / Math.max(1, data.nodes.length)) * Math.PI * 2;
1769
+ const radius = 120 + Math.random() * 100;
1770
+ return {
1771
+ ...n,
1772
+ x: cx + Math.cos(angle) * radius,
1773
+ y: cy + Math.sin(angle) * radius,
1774
+ vx: 0,
1775
+ vy: 0,
1776
+ radius: n.type === 'file' ? 14 : n.type === 'entity' ? 13 : n.kind === 'error_trap' ? 16 : 12,
1777
+ };
1778
+ });
1779
+ edges = (data.edges || []).map(e => ({
1780
+ source: e.source,
1781
+ target: e.target,
1782
+ type: 'link'
1783
+ }));
1784
+ }
1785
+
1786
+ async function loadActionsGraph() {
1787
+ const res = await fetch('/api/actions');
1788
+ allActionsData = await res.json();
1789
+ setupActionsPhysics(allActionsData);
1790
+ }
1791
+
1792
+ function setupActionsPhysics(data) {
1793
+ const cx = canvas.width / 2;
1794
+ const cy = canvas.height / 2;
1795
+ const statusColors = {
1796
+ done: '#10b981',
1797
+ active: '#3b82f6',
1798
+ frontier: '#f59e0b',
1799
+ blocked: '#6b7280',
1800
+ cancelled: '#ef4444',
1801
+ pending: '#8b5cf6'
1802
+ };
1803
+
1804
+ nodes = (data.actions || []).map((a, i) => {
1805
+ const isFrontier = a.isFrontier;
1806
+ const statusKey = isFrontier ? 'frontier' : a.status;
1807
+ const angle = (i / Math.max(1, data.actions.length)) * Math.PI * 2;
1808
+ const radius = 110 + (i % 3) * 60;
1809
+ return {
1810
+ id: a.id,
1811
+ label: a.title,
1812
+ type: 'action',
1813
+ status: a.status,
1814
+ isFrontier,
1815
+ priority: a.priority,
1816
+ assigned_to: a.assigned_to,
1817
+ color: statusColors[statusKey] || '#6b7280',
1818
+ radius: isFrontier ? 18 : 14,
1819
+ x: cx + Math.cos(angle) * radius,
1820
+ y: cy + Math.sin(angle) * radius,
1821
+ vx: 0,
1822
+ vy: 0
1823
+ };
1824
+ });
1825
+
1826
+ edges = (data.edges || []).map(e => ({
1827
+ source: e.source_action_id,
1828
+ target: e.target_action_id,
1829
+ type: e.edge_type // 'requires' or 'conflicts_with'
1830
+ }));
1831
+ }
1832
+
1833
+ function setupGraphInteraction() {
1834
+ canvas.onmousedown = (e) => {
1835
+ dragStartMouse = { x: e.clientX, y: e.clientY };
1836
+ const mouseX = (e.offsetX - transform.x) / transform.k;
1837
+ const mouseY = (e.offsetY - transform.y) / transform.k;
1838
+ const hit = nodes.find(n => Math.hypot(n.x - mouseX, n.y - mouseY) < n.radius + 6);
1839
+ if (hit) {
1840
+ dragNode = hit;
1841
+ } else {
1842
+ isDragging = true;
1843
+ dragStart = { x: e.offsetX - transform.x, y: e.offsetY - transform.y };
1844
+ }
1845
+ };
1846
+
1847
+ canvas.onmousemove = (e) => {
1848
+ if (dragNode) {
1849
+ dragNode.x = (e.offsetX - transform.x) / transform.k;
1850
+ dragNode.y = (e.offsetY - transform.y) / transform.k;
1851
+ dragNode.vx = 0;
1852
+ dragNode.vy = 0;
1853
+ } else if (isDragging) {
1854
+ transform.x = e.offsetX - dragStart.x;
1855
+ transform.y = e.offsetY - dragStart.y;
1856
+ }
1857
+ };
1858
+
1859
+ window.onmouseup = (e) => {
1860
+ const mouseDist = Math.hypot(e.clientX - dragStartMouse.x, e.clientY - dragStartMouse.y);
1861
+ if (dragNode && mouseDist < 6) {
1862
+ if (dragNode.type === 'memory') {
1863
+ openMemoryModal(dragNode.id);
1864
+ } else if (dragNode.type === 'action') {
1865
+ openActionModal(dragNode.id);
1866
+ } else if (dragNode.type === 'file') {
1867
+ switchTab('memories');
1868
+ const searchInput = document.getElementById('search-input');
1869
+ if (searchInput) {
1870
+ searchInput.value = dragNode.label;
1871
+ loadMemories(dragNode.label);
1872
+ }
1873
+ }
1874
+ }
1875
+ dragNode = null;
1876
+ isDragging = false;
1877
+ };
1878
+
1879
+ canvas.onwheel = (e) => {
1880
+ e.preventDefault();
1881
+ const factor = e.deltaY < 0 ? 1.1 : 0.9;
1882
+ transform.k = Math.max(0.3, Math.min(3, transform.k * factor));
1883
+ };
1884
+ }
1885
+
1886
+ function zoomGraph(factor) {
1887
+ transform.k = Math.max(0.3, Math.min(3, transform.k * factor));
1888
+ }
1889
+
1890
+ function resetGraphZoom() {
1891
+ transform = { x: 0, y: 0, k: 1 };
1892
+ }
1893
+
1894
+ function toggleGraphPhysics() {
1895
+ graphSimulationRunning = !graphSimulationRunning;
1896
+ }
1897
+
1898
+ function animateGraph() {
1899
+ if (currentTab !== 'graph') {
1900
+ animationFrameId = null;
1901
+ return;
1902
+ }
1903
+
1904
+ if (graphSimulationRunning) {
1905
+ // Node repulsion
1906
+ for (let i = 0; i < nodes.length; i++) {
1907
+ for (let j = i + 1; j < nodes.length; j++) {
1908
+ const dx = nodes[j].x - nodes[i].x;
1909
+ const dy = nodes[j].y - nodes[i].y;
1910
+ const dist = Math.hypot(dx, dy) || 1;
1911
+ if (dist < 220) {
1912
+ const f = (220 - dist) / dist * 0.45;
1913
+ nodes[i].vx -= dx * f * 0.05;
1914
+ nodes[i].vy -= dy * f * 0.05;
1915
+ nodes[j].vx += dx * f * 0.05;
1916
+ nodes[j].vy += dy * f * 0.05;
1917
+ }
1918
+ }
1919
+ }
1920
+
1921
+ // Edge attraction
1922
+ for (const e of edges) {
1923
+ const s = nodes.find(n => n.id === e.source);
1924
+ const t = nodes.find(n => n.id === e.target);
1925
+ if (s && t) {
1926
+ const dx = t.x - s.x;
1927
+ const dy = t.y - s.y;
1928
+ const spring = e.type === 'conflicts_with' ? 0.001 : 0.0035;
1929
+ s.vx += dx * spring;
1930
+ s.vy += dy * spring;
1931
+ t.vx -= dx * spring;
1932
+ t.vy -= dy * spring;
1933
+ }
1934
+ }
1935
+
1936
+ // Apply friction
1937
+ for (const n of nodes) {
1938
+ if (n !== dragNode) {
1939
+ n.x += n.vx;
1940
+ n.y += n.vy;
1941
+ n.vx *= 0.85;
1942
+ n.vy *= 0.85;
1943
+ }
1944
+ }
1945
+ }
1946
+
1947
+ // Draw frame
1948
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1949
+ ctx.save();
1950
+ ctx.translate(transform.x, transform.y);
1951
+ ctx.scale(transform.k, transform.k);
1952
+
1953
+ // Draw edges
1954
+ for (const e of edges) {
1955
+ const s = nodes.find(n => n.id === e.source);
1956
+ const t = nodes.find(n => n.id === e.target);
1957
+ if (s && t) {
1958
+ ctx.beginPath();
1959
+ if (e.type === 'conflicts_with') {
1960
+ ctx.setLineDash([6, 4]);
1961
+ ctx.strokeStyle = '#ef4444';
1962
+ ctx.lineWidth = 2;
1963
+ ctx.moveTo(s.x, s.y);
1964
+ ctx.lineTo(t.x, t.y);
1965
+ ctx.stroke();
1966
+ ctx.setLineDash([]);
1967
+ } else if (e.type === 'requires') {
1968
+ ctx.setLineDash([]);
1969
+ ctx.strokeStyle = '#38bdf8';
1970
+ ctx.lineWidth = 1.8;
1971
+ ctx.moveTo(s.x, s.y);
1972
+ ctx.lineTo(t.x, t.y);
1973
+ ctx.stroke();
1974
+
1975
+ // Directed arrow from prerequisite (target) to dependent (source)
1976
+ const angle = Math.atan2(s.y - t.y, s.x - t.x);
1977
+ const arrowDist = s.radius + 3;
1978
+ const arrowX = s.x - Math.cos(angle) * arrowDist;
1979
+ const arrowY = s.y - Math.sin(angle) * arrowDist;
1980
+ ctx.beginPath();
1981
+ ctx.moveTo(arrowX, arrowY);
1982
+ ctx.lineTo(arrowX - 9 * Math.cos(angle - Math.PI / 6), arrowY - 9 * Math.sin(angle - Math.PI / 6));
1983
+ ctx.lineTo(arrowX - 9 * Math.cos(angle + Math.PI / 6), arrowY - 9 * Math.sin(angle + Math.PI / 6));
1984
+ ctx.fillStyle = '#38bdf8';
1985
+ ctx.fill();
1986
+ } else {
1987
+ ctx.setLineDash([]);
1988
+ ctx.strokeStyle = '#374151';
1989
+ ctx.lineWidth = 1.5;
1990
+ ctx.moveTo(s.x, s.y);
1991
+ ctx.lineTo(t.x, t.y);
1992
+ ctx.stroke();
1993
+ }
1994
+ }
1995
+ }
1996
+
1997
+ // Draw nodes
1998
+ for (const n of nodes) {
1999
+ // Frontier glowing aura animation
2000
+ if (n.isFrontier) {
2001
+ const pulse = (Math.sin(Date.now() / 220) + 1) * 3.5;
2002
+ ctx.beginPath();
2003
+ ctx.arc(n.x, n.y, n.radius + pulse + 3, 0, Math.PI * 2);
2004
+ ctx.fillStyle = 'rgba(245, 158, 11, 0.22)';
2005
+ ctx.fill();
2006
+ ctx.strokeStyle = '#f59e0b';
2007
+ ctx.lineWidth = 1.5;
2008
+ ctx.stroke();
2009
+ }
2010
+
2011
+ ctx.beginPath();
2012
+ ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
2013
+
2014
+ if (n.type === 'action') {
2015
+ ctx.fillStyle = n.color;
2016
+ } else if (n.type === 'file') {
2017
+ ctx.fillStyle = '#10b981';
2018
+ } else if (n.type === 'entity') {
2019
+ ctx.fillStyle = '#ec4899';
2020
+ } else if (n.kind === 'error_trap') {
2021
+ ctx.fillStyle = '#fbbf24';
2022
+ } else if (n.kind === 'decision') {
2023
+ ctx.fillStyle = '#8b5cf6';
2024
+ } else {
2025
+ ctx.fillStyle = '#3b82f6';
2026
+ }
2027
+ ctx.fill();
2028
+ ctx.strokeStyle = '#111827';
2029
+ ctx.lineWidth = 2.5;
2030
+ ctx.stroke();
2031
+
2032
+ // Node Label
2033
+ ctx.fillStyle = '#e2e8f0';
2034
+ ctx.font = '11px -apple-system, BlinkMacSystemFont, sans-serif';
2035
+ ctx.textAlign = 'center';
2036
+ const labelText = n.type === 'action' ? ('[P' + n.priority + '] ' + n.label) : n.label;
2037
+ const truncated = labelText.length > 22 ? labelText.slice(0, 20) + '...' : labelText;
2038
+ ctx.fillText(truncated, n.x, n.y + n.radius + 14);
2039
+ }
2040
+
2041
+ ctx.restore();
2042
+ animationFrameId = requestAnimationFrame(animateGraph);
2043
+ }
2044
+
2045
+ // Debounce search
2046
+ let debounceTimer;
2047
+ document.getElementById('search-input')?.addEventListener('input', (e) => {
2048
+ clearTimeout(debounceTimer);
2049
+ debounceTimer = setTimeout(() => {
2050
+ loadMemories(e.target.value);
2051
+ }, 200);
2052
+ });
2053
+
2054
+ loadStatus();
2055
+ loadMemories();
2056
+ loadActionsBoard();
2057
+ loadConflictsList();
2058
+ </script>
2059
+ </body>
2060
2060
  </html>`;
2061
2061
  }
2062
2062
  //# sourceMappingURL=server.js.map