prism-mcp-server 2.5.2 → 3.0.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.
@@ -27,7 +27,8 @@ export function renderDashboardHTML(version) {
27
27
  <style>
28
28
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
29
29
 
30
- :root {
30
+ /* ─── Theme: Dark (Default) ─── */
31
+ :root, [data-theme="dark"] {
31
32
  --bg-primary: #0a0e1a;
32
33
  --bg-secondary: #111827;
33
34
  --bg-glass: rgba(17, 24, 39, 0.6);
@@ -49,6 +50,44 @@ export function renderDashboardHTML(version) {
49
50
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
50
51
  }
51
52
 
53
+ /* ─── Theme: Midnight — deeper blacks, blue-shifted accents ─── */
54
+ [data-theme="midnight"] {
55
+ --bg-primary: #020617;
56
+ --bg-secondary: #0f172a;
57
+ --bg-glass: rgba(2, 6, 23, 0.7);
58
+ --border-glass: rgba(59, 130, 246, 0.15);
59
+ --border-glow: rgba(59, 130, 246, 0.35);
60
+ --text-primary: #e2e8f0;
61
+ --text-secondary: #94a3b8;
62
+ --text-muted: #475569;
63
+ --accent-purple: #818cf8;
64
+ --accent-blue: #60a5fa;
65
+ --accent-cyan: #22d3ee;
66
+ --accent-green: #34d399;
67
+ --accent-amber: #fbbf24;
68
+ --accent-rose: #fb7185;
69
+ --gradient-hero: linear-gradient(135deg, #818cf8 0%, #60a5fa 50%, #22d3ee 100%);
70
+ }
71
+
72
+ /* ─── Theme: Purple Haze — warm violet tones ─── */
73
+ [data-theme="purple"] {
74
+ --bg-primary: #0c0515;
75
+ --bg-secondary: #1a0a2e;
76
+ --bg-glass: rgba(26, 10, 46, 0.65);
77
+ --border-glass: rgba(168, 85, 247, 0.2);
78
+ --border-glow: rgba(168, 85, 247, 0.4);
79
+ --text-primary: #f5f3ff;
80
+ --text-secondary: #c4b5fd;
81
+ --text-muted: #7c3aed;
82
+ --accent-purple: #a855f7;
83
+ --accent-blue: #7c3aed;
84
+ --accent-cyan: #c084fc;
85
+ --accent-green: #a78bfa;
86
+ --accent-amber: #e879f9;
87
+ --accent-rose: #f472b6;
88
+ --gradient-hero: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #c084fc 100%);
89
+ }
90
+
52
91
  body {
53
92
  background: var(--bg-primary);
54
93
  color: var(--text-primary);
@@ -256,6 +295,21 @@ export function renderDashboardHTML(version) {
256
295
  .health-issues .issue-row {
257
296
  padding: 0.3rem 0; display: flex; gap: 0.5rem; align-items: flex-start;
258
297
  }
298
+ .cleanup-btn {
299
+ margin-left: auto; background: rgba(244,63,94,0.12); border: 1px solid rgba(244,63,94,0.3);
300
+ color: var(--accent-rose); cursor: pointer; font-size: 0.75rem; font-weight: 600;
301
+ padding: 0.2rem 0.65rem; border-radius: 6px; transition: all 0.2s;
302
+ }
303
+ .cleanup-btn:hover { background: rgba(244,63,94,0.25); border-color: var(--accent-rose); }
304
+ .cleanup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
305
+ .toast-fixed {
306
+ position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 200;
307
+ padding: 0.65rem 1.2rem; border-radius: 10px; font-size: 0.85rem; font-weight: 500;
308
+ backdrop-filter: blur(10px); border: 1px solid var(--border-glow);
309
+ background: var(--bg-secondary); color: var(--text-primary);
310
+ opacity: 0; transition: opacity 0.3s; pointer-events: none;
311
+ }
312
+ .toast-fixed.show { opacity: 1; }
259
313
 
260
314
  /* ─── Neural Graph (v2.3.0) ─── */
261
315
  #network-container {
@@ -270,6 +324,126 @@ export function renderDashboardHTML(version) {
270
324
  transition: color 0.2s;
271
325
  }
272
326
  .refresh-btn:hover { color: var(--accent-purple); }
327
+
328
+ /* ─── Settings Modal (v3.0) ─── */
329
+ .settings-btn {
330
+ background: none; border: 1px solid var(--border-glass);
331
+ color: var(--text-secondary); cursor: pointer; font-size: 1.1rem;
332
+ padding: 0.4rem 0.7rem; border-radius: var(--radius-sm);
333
+ transition: all 0.2s;
334
+ }
335
+ .settings-btn:hover { border-color: var(--border-glow); color: var(--accent-purple); }
336
+ .identity-chip {
337
+ display: none; align-items: center; gap: 0.4rem;
338
+ padding: 0.35rem 0.75rem; border-radius: 999px;
339
+ background: rgba(139,92,246,0.12); border: 1px solid rgba(139,92,246,0.25);
340
+ color: var(--text-secondary); font-size: 0.8rem; font-weight: 500;
341
+ cursor: pointer; transition: all 0.2s;
342
+ }
343
+ .identity-chip:hover { border-color: var(--accent-purple); color: var(--accent-purple); background: rgba(139,92,246,0.2); }
344
+ .identity-chip .role-icon { font-size: 0.9rem; }
345
+ .identity-chip .identity-label { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
346
+ /* Settings modal tab bar */
347
+ .settings-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-glass); margin: 0 -1.5rem 1.2rem; padding: 0 1.5rem; }
348
+ .s-tab { padding: 0.55rem 1.1rem; font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); cursor: pointer;
349
+ border-bottom: 2px solid transparent; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; }
350
+ .s-tab.active { color: var(--accent-purple); border-bottom-color: var(--accent-purple); }
351
+ .s-tab:hover:not(.active) { color: var(--text-primary); }
352
+ .s-tab-panel { display: none; } .s-tab-panel.active { display: block; }
353
+ /* Skills editor */
354
+ .skill-role-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
355
+ .skill-role-row label { font-size: 0.82rem; color: var(--text-secondary); }
356
+ .skill-role-select { padding: 0.3rem 0.6rem; background: var(--bg-hover); color: var(--text-primary);
357
+ border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); }
358
+ .skill-textarea { width: 100%; min-height: 220px; background: var(--bg-hover); color: var(--text-primary);
359
+ border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.75rem;
360
+ font-size: 0.82rem; font-family: var(--font-mono); line-height: 1.5; resize: vertical;
361
+ box-sizing: border-box; transition: border-color 0.2s; }
362
+ .skill-textarea:focus { outline: none; border-color: var(--accent-purple); }
363
+ .skill-char-count { font-size: 0.74rem; color: var(--text-muted); text-align: right; margin-top: 0.3rem; }
364
+ .skill-actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; align-items: center; }
365
+ .skill-save-btn { background: var(--accent-purple); color: #fff; border: none; border-radius: var(--radius-sm);
366
+ padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
367
+ .skill-save-btn:hover { opacity: 0.85; }
368
+ .skill-upload-btn { background: none; border: 1px solid var(--border-glass); color: var(--text-secondary);
369
+ border-radius: var(--radius-sm); padding: 0.45rem 0.85rem; font-size: 0.82rem; cursor: pointer; transition: all 0.2s; }
370
+ .skill-upload-btn:hover { border-color: var(--accent-purple); color: var(--accent-purple); }
371
+ .skill-clear-btn { background: none; border: none; color: var(--text-muted); font-size: 0.8rem; cursor: pointer;
372
+ margin-left: auto; transition: color 0.2s; }
373
+ .skill-clear-btn:hover { color: #ef4444; }
374
+ .skill-hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.6rem; line-height: 1.5; }
375
+ .modal-overlay {
376
+ display: none; position: fixed; inset: 0; z-index: 100;
377
+ background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
378
+ justify-content: center; align-items: center;
379
+ }
380
+ .modal-overlay.active { display: flex; }
381
+ .modal {
382
+ background: var(--bg-secondary); border: 1px solid var(--border-glow);
383
+ border-radius: var(--radius); padding: 2rem; width: 480px; max-width: 90vw;
384
+ max-height: 85vh; overflow-y: auto; position: relative;
385
+ }
386
+ .modal h2 { font-size: 1.1rem; margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.5rem; }
387
+ .modal-close {
388
+ position: absolute; top: 1rem; right: 1rem; background: none;
389
+ border: none; color: var(--text-muted); cursor: pointer; font-size: 1.25rem;
390
+ }
391
+ .modal-close:hover { color: var(--text-primary); }
392
+ .setting-row {
393
+ display: flex; justify-content: space-between; align-items: center;
394
+ padding: 0.75rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
395
+ }
396
+ .setting-row:last-child { border-bottom: none; }
397
+ .setting-label { font-size: 0.85rem; color: var(--text-secondary); }
398
+ .setting-desc { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.2rem; }
399
+ .toggle {
400
+ position: relative; width: 44px; height: 24px;
401
+ background: rgba(100,116,139,0.3); border-radius: 12px;
402
+ cursor: pointer; transition: background 0.3s; flex-shrink: 0;
403
+ }
404
+ .toggle.active { background: var(--accent-purple); }
405
+ .toggle::after {
406
+ content: ''; position: absolute; top: 2px; left: 2px;
407
+ width: 20px; height: 20px; border-radius: 50%;
408
+ background: white; transition: transform 0.3s;
409
+ }
410
+ .toggle.active::after { transform: translateX(20px); }
411
+ .setting-select {
412
+ background: var(--bg-primary); border: 1px solid var(--border-glass);
413
+ color: var(--text-primary); padding: 0.4rem 0.6rem;
414
+ border-radius: 6px; font-size: 0.8rem; font-family: var(--font-sans);
415
+ }
416
+ .setting-section {
417
+ font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
418
+ letter-spacing: 0.1em; color: var(--accent-purple); margin: 1rem 0 0.5rem;
419
+ }
420
+ .setting-saved {
421
+ font-size: 0.75rem; color: var(--accent-green); opacity: 0;
422
+ transition: opacity 0.3s; margin-left: 0.5rem;
423
+ }
424
+ .setting-saved.show { opacity: 1; }
425
+ .boot-badge {
426
+ font-size: 0.6rem; padding: 0.15rem 0.5rem; border-radius: 4px;
427
+ background: rgba(245,158,11,0.15); color: var(--accent-amber);
428
+ font-weight: 600; text-transform: uppercase;
429
+ }
430
+
431
+ /* ─── Hivemind Radar (v3.0) ─── */
432
+ .team-list { list-style: none; padding: 0; }
433
+ .team-item {
434
+ display: flex; align-items: center; gap: 0.75rem;
435
+ padding: 0.6rem 0; border-bottom: 1px solid rgba(255,255,255,0.05);
436
+ font-size: 0.85rem;
437
+ }
438
+ .team-item:last-child { border-bottom: none; }
439
+ .team-role { font-weight: 600; color: var(--text-primary); min-width: 60px; }
440
+ .team-task { color: var(--text-secondary); flex: 1; }
441
+ .team-heartbeat { font-size: 0.7rem; color: var(--text-muted); font-family: var(--font-mono); }
442
+ .pulse-dot {
443
+ width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green);
444
+ flex-shrink: 0; animation: pulseDot 2s ease-in-out infinite;
445
+ }
446
+ @keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
273
447
  </style>
274
448
  </head>
275
449
  <body>
@@ -282,10 +456,12 @@ export function renderDashboardHTML(version) {
282
456
  <span class="version-badge">v${version}</span>
283
457
  </div>
284
458
  <div class="selector">
459
+ <span class="identity-chip" id="identityChip" onclick="openSettings()" title="Agent Identity — click to change"></span>
285
460
  <select id="projectSelect">
286
461
  <option value="">Loading projects...</option>
287
462
  </select>
288
463
  <button onclick="loadProject()">Inspect</button>
464
+ <button class="settings-btn" onclick="openSettings()" title="Settings">⚙️</button>
289
465
  </div>
290
466
  </header>
291
467
 
@@ -319,7 +495,10 @@ export function renderDashboardHTML(version) {
319
495
 
320
496
  <!-- Brain Health (v2.2.0) -->
321
497
  <div class="card" id="healthCard" style="display:none">
322
- <div class="card-title"><span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺</div>
498
+ <div class="card-title">
499
+ <span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺
500
+ <button class="cleanup-btn" id="cleanupBtn" onclick="cleanupIssues()" style="display:none">🧹 Fix Issues</button>
501
+ </div>
323
502
  <div class="health-status">
324
503
  <div class="health-dot unknown" id="healthDot"></div>
325
504
  <div>
@@ -367,11 +546,193 @@ export function renderDashboardHTML(version) {
367
546
  <div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Session Ledger</div>
368
547
  <div class="timeline" id="ledgerTimeline"></div>
369
548
  </div>
549
+ </div>
550
+
551
+ <!-- Hivemind Radar (v3.0) -->
552
+ <div class="card" id="hivemindCard" style="display:none">
553
+ <div class="card-title">
554
+ <span class="dot" style="background:var(--accent-cyan)"></span>
555
+ Hivemind Radar 🐝
556
+ <button onclick="loadTeam()" class="refresh-btn">↻</button>
557
+ </div>
558
+ <ul class="team-list" id="teamList">
559
+ <li style="color:var(--text-muted);font-size:0.85rem;text-align:center;padding:1rem">
560
+ No active agents. Set PRISM_ENABLE_HIVEMIND=true to enable.
561
+ </li>
562
+ </ul>
563
+ </div>
564
+ </div>
565
+ </div>
566
+
567
+ <!-- Settings Modal (v3.0) -->
568
+ <div class="modal-overlay" id="settingsModal">
569
+ <div class="modal">
570
+ <button class="modal-close" onclick="closeSettings()">✕</button>
571
+ <h2>⚙️ Settings</h2>
572
+
573
+ <!-- Tab bar -->
574
+ <div class="settings-tabs">
575
+ <button class="s-tab active" id="stab-settings" onclick="switchSettingsTab('settings')">⚙️ Settings</button>
576
+ <button class="s-tab" id="stab-skills" onclick="switchSettingsTab('skills')">📜 Skills</button>
577
+ </div>
578
+
579
+ <!-- Settings panel (existing content) -->
580
+ <div class="s-tab-panel active" id="spanel-settings">
581
+
582
+ <div class="setting-section">Runtime Settings</div>
583
+
584
+ <div class="setting-row">
585
+ <div>
586
+ <div class="setting-label">Auto-Capture HTML</div>
587
+ <div class="setting-desc">Capture local dev server UI on handoff save</div>
588
+ </div>
589
+ <div class="toggle" id="toggle-auto-capture" onclick="toggleSetting('auto_capture', this)"></div>
590
+ </div>
591
+
592
+ <div class="setting-row">
593
+ <div>
594
+ <div class="setting-label">Dashboard Theme</div>
595
+ <div class="setting-desc">Visual theme for Mind Palace</div>
596
+ </div>
597
+ <select class="setting-select" id="select-theme" onchange="saveSetting('dashboard_theme', this.value)">
598
+ <option value="dark">Dark (Default)</option>
599
+ <option value="midnight">Midnight</option>
600
+ <option value="purple">Purple Haze</option>
601
+ </select>
602
+ </div>
603
+
604
+ <div class="setting-row">
605
+ <div>
606
+ <div class="setting-label">Context Depth</div>
607
+ <div class="setting-desc">Default level for session_load_context</div>
608
+ </div>
609
+ <select class="setting-select" id="select-context-depth" onchange="saveSetting('default_context_depth', this.value)">
610
+ <option value="standard">Standard (~200 tokens)</option>
611
+ <option value="quick">Quick (~50 tokens)</option>
612
+ <option value="deep">Deep (~1000+ tokens)</option>
613
+ </select>
614
+ </div>
615
+
616
+ <div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
617
+
618
+ <div class="setting-row">
619
+ <div>
620
+ <div class="setting-label">Hivemind Mode</div>
621
+ <div class="setting-desc">Multi-agent coordination (PRISM_ENABLE_HIVEMIND)</div>
622
+ </div>
623
+ <div class="toggle" id="toggle-hivemind" onclick="toggleBootSetting('hivemind_enabled', this)"></div>
624
+ </div>
625
+ <div class="setting-row">
626
+ <div>
627
+ <div class="setting-label">Storage Backend</div>
628
+ <div class="setting-desc">Switch between SQLite and Supabase</div>
629
+ </div>
630
+ <select id="storageBackendSelect" onchange="window.saveBootSetting('PRISM_STORAGE', this.value)" style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;">
631
+ <option value="local">SQLite</option>
632
+ <option value="supabase">Supabase</option>
633
+ </select>
634
+ </div>
635
+
636
+ <div class="setting-section">Agent Identity</div>
637
+
638
+ <div class="setting-row">
639
+ <div>
640
+ <div class="setting-label">Default Role</div>
641
+ <div class="setting-desc">Used when no role is passed to memory/Hivemind tools</div>
642
+ </div>
643
+ <select class="setting-select" id="select-default-role" onchange="saveSetting('default_role', this.value)">
644
+ <option value="global">global (shared)</option>
645
+ <option value="dev">dev</option>
646
+ <option value="qa">qa</option>
647
+ <option value="pm">pm</option>
648
+ <option value="lead">lead</option>
649
+ <option value="security">security</option>
650
+ <option value="ux">ux</option>
651
+ </select>
652
+ </div>
653
+
654
+ <div class="setting-row">
655
+ <div>
656
+ <div class="setting-label">Agent Name</div>
657
+ <div class="setting-desc">Display name shown in Hivemind Radar (e.g. Dmitri, Dev Alex)</div>
658
+ </div>
659
+ <input type="text" id="input-agent-name"
660
+ placeholder="e.g. Dmitri"
661
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 130px;"
662
+ onchange="saveSetting('agent_name', this.value)"
663
+ oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('agent_name',this.value),800)" />
664
+ </div>
665
+
666
+ <span class="setting-saved" id="savedToast">Saved ✓</span>
667
+ </div><!-- /spanel-settings -->
668
+
669
+ <!-- Skills panel -->
670
+ <div class="s-tab-panel" id="spanel-skills">
671
+ <div class="skill-role-row">
672
+ <label>Role</label>
673
+ <select class="skill-role-select" id="skillRoleSelect" onchange="loadSkillForRole(this.value)">
674
+ <option value="global">🌐 global</option>
675
+ <option value="dev">🛠️ dev</option>
676
+ <option value="qa">🔍 qa</option>
677
+ <option value="pm">📋 pm</option>
678
+ <option value="lead">🏗️ lead</option>
679
+ <option value="security">🔒 security</option>
680
+ <option value="ux">🎨 ux</option>
681
+ </select>
682
+ </div>
683
+ <textarea class="skill-textarea" id="skillTextarea"
684
+ placeholder="Paste rules, conventions, or prompts for this role...
685
+ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode\n- Log errors to console.error"
686
+ oninput="document.getElementById('skillCharCount').textContent = this.value.length + ' chars'">
687
+ </textarea>
688
+ <div class="skill-char-count" id="skillCharCount">0 chars</div>
689
+ <div class="skill-actions">
690
+ <button class="skill-save-btn" onclick="saveCurrentSkill()">💾 Save</button>
691
+ <label class="skill-upload-btn" title="Upload a .md or .txt file">
692
+ 📎 Upload file
693
+ <input type="file" accept=".md,.txt,.markdown" style="display:none"
694
+ onchange="handleSkillUpload(this)">
695
+ </label>
696
+ <button class="skill-clear-btn" onclick="clearCurrentSkill()">🗑️ Clear</button>
697
+ </div>
698
+ <div class="skill-hint">
699
+ Skills are auto-injected into <code>session_load_context</code> responses for this role.<br>
700
+ Use Markdown. Changes take effect immediately — no restart needed.
701
+ </div>
702
+ </div><!-- /spanel-skills -->
703
+
370
704
  </div>
371
705
  </div>
372
706
  </div>
373
707
 
708
+ <!-- Fixed toast for cleanup feedback -->
709
+ <div class="toast-fixed" id="fixedToast"></div>
710
+
374
711
  <script>
712
+ // Role icon map
713
+ var ROLE_ICONS = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',global:'🌐',cmo:'📢'};
714
+
715
+ // Load and render the identity chip from settings
716
+ async function loadIdentityChip() {
717
+ try {
718
+ var res = await fetch('/api/settings');
719
+ var data = await res.json();
720
+ var s = data.settings || {};
721
+ var role = s.default_role || '';
722
+ var name = s.agent_name || '';
723
+ var chip = document.getElementById('identityChip');
724
+ if (!chip) return;
725
+ if (role && role !== 'global' || name) {
726
+ var icon = ROLE_ICONS[role] || '🤖';
727
+ var label = name ? (role && role !== 'global' ? role + ' · ' + name : name) : role;
728
+ chip.innerHTML = '<span class="role-icon">' + icon + '</span><span class="identity-label">' + escapeHtml(label) + '</span>';
729
+ chip.style.display = 'flex';
730
+ } else {
731
+ chip.style.display = 'none';
732
+ }
733
+ } catch(e) { /* silently skip */ }
734
+ }
735
+
375
736
  // Auto-load project list on page load
376
737
  (async function() {
377
738
  try {
@@ -387,6 +748,8 @@ export function renderDashboardHTML(version) {
387
748
  } catch(e) {
388
749
  document.getElementById('projectSelect').innerHTML = '<option value="">Error loading projects</option>';
389
750
  }
751
+ // Load identity chip once settings are available
752
+ loadIdentityChip();
390
753
  })();
391
754
 
392
755
  async function loadProject() {
@@ -508,6 +871,7 @@ export function renderDashboardHTML(version) {
508
871
 
509
872
  // Issue rows
510
873
  var issues = healthData.issues || [];
874
+ var cleanupBtn = document.getElementById('cleanupBtn');
511
875
  if (issues.length > 0) {
512
876
  var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
513
877
  healthIssues.innerHTML = issues.map(function(i) {
@@ -516,8 +880,10 @@ export function renderDashboardHTML(version) {
516
880
  '<span>' + escapeHtml(i.message) + '</span>' +
517
881
  '</div>';
518
882
  }).join('');
883
+ if (cleanupBtn) cleanupBtn.style.display = 'inline-block';
519
884
  } else {
520
885
  healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
886
+ if (cleanupBtn) cleanupBtn.style.display = 'none';
521
887
  }
522
888
 
523
889
  healthCard.style.display = 'block';
@@ -528,6 +894,7 @@ export function renderDashboardHTML(version) {
528
894
 
529
895
  document.getElementById('content').className = 'grid grid-main fade-in';
530
896
  document.getElementById('content').style.display = 'grid';
897
+ loadTeam(); // v3.0: auto-load Hivemind team
531
898
  } catch(e) {
532
899
  alert('Failed to load project data: ' + e.message);
533
900
  } finally {
@@ -618,6 +985,249 @@ export function renderDashboardHTML(version) {
618
985
 
619
986
  // Initialize the graph on page load
620
987
  loadGraph();
988
+
989
+ // ─── Settings Modal (v3.0) ───
990
+ function openSettings() {
991
+ document.getElementById('settingsModal').classList.add('active');
992
+ loadSettings();
993
+ }
994
+ function closeSettings() {
995
+ document.getElementById('settingsModal').classList.remove('active');
996
+ }
997
+ // Close on overlay click
998
+ document.getElementById('settingsModal').addEventListener('click', function(e) {
999
+ if (e.target === this) closeSettings();
1000
+ });
1001
+
1002
+ // ─── Skills Tab JS ───────────────────────────────────────────
1003
+ var _skillsCache = {}; // role → content cache
1004
+
1005
+ function switchSettingsTab(tab) {
1006
+ ['settings','skills'].forEach(function(t) {
1007
+ document.getElementById('stab-' + t).classList.toggle('active', t === tab);
1008
+ document.getElementById('spanel-' + t).classList.toggle('active', t === tab);
1009
+ });
1010
+ if (tab === 'skills') {
1011
+ // Load skill for whichever role is currently selected
1012
+ var role = document.getElementById('skillRoleSelect').value;
1013
+ loadSkillForRole(role);
1014
+ }
1015
+ }
1016
+
1017
+ async function loadSkillForRole(role) {
1018
+ try {
1019
+ var res = await fetch('/api/skills');
1020
+ var data = await res.json();
1021
+ _skillsCache = data.skills || {};
1022
+ var content = _skillsCache[role] || '';
1023
+ var ta = document.getElementById('skillTextarea');
1024
+ ta.value = content;
1025
+ document.getElementById('skillCharCount').textContent = content.length + ' chars';
1026
+ } catch(e) { console.warn('Skills load failed:', e); }
1027
+ }
1028
+
1029
+ async function saveCurrentSkill() {
1030
+ var role = document.getElementById('skillRoleSelect').value;
1031
+ var content = document.getElementById('skillTextarea').value;
1032
+ try {
1033
+ await fetch('/api/skills', {
1034
+ method: 'POST',
1035
+ headers: { 'Content-Type': 'application/json' },
1036
+ body: JSON.stringify({ role: role, content: content })
1037
+ });
1038
+ _skillsCache[role] = content;
1039
+ showFixedToast('✅ Skill saved for ' + role, true);
1040
+ } catch(e) { showFixedToast('❌ Save failed', false); }
1041
+ }
1042
+
1043
+ async function clearCurrentSkill() {
1044
+ var role = document.getElementById('skillRoleSelect').value;
1045
+ try {
1046
+ await fetch('/api/skills/' + role, { method: 'DELETE' });
1047
+ document.getElementById('skillTextarea').value = '';
1048
+ document.getElementById('skillCharCount').textContent = '0 chars';
1049
+ _skillsCache[role] = '';
1050
+ showFixedToast('🗑️ Skill cleared for ' + role, true);
1051
+ } catch(e) { showFixedToast('❌ Clear failed', false); }
1052
+ }
1053
+
1054
+ function handleSkillUpload(input) {
1055
+ var file = input.files[0];
1056
+ if (!file) return;
1057
+ var reader = new FileReader();
1058
+ reader.onload = async function(e) {
1059
+ var content = e.target.result;
1060
+ var ta = document.getElementById('skillTextarea');
1061
+ ta.value = content;
1062
+ document.getElementById('skillCharCount').textContent = content.length + ' chars';
1063
+ // Auto-save after upload
1064
+ await saveCurrentSkill();
1065
+ };
1066
+ reader.readAsText(file);
1067
+ input.value = ''; // reset so same file can be re-uploaded
1068
+ }
1069
+
1070
+
1071
+ async function loadSettings() {
1072
+ try {
1073
+ var res = await fetch('/api/settings');
1074
+ var data = await res.json();
1075
+ var s = data.settings || {};
1076
+ // Runtime toggles
1077
+ if (s.auto_capture === 'true') document.getElementById('toggle-auto-capture').classList.add('active');
1078
+ else document.getElementById('toggle-auto-capture').classList.remove('active');
1079
+ // Context depth
1080
+ if (s.default_context_depth) document.getElementById('select-context-depth').value = s.default_context_depth;
1081
+ // Theme
1082
+ if (s.dashboard_theme) {
1083
+ document.getElementById('select-theme').value = s.dashboard_theme;
1084
+ applyTheme(s.dashboard_theme);
1085
+ }
1086
+ // Boot toggles
1087
+ if (s.hivemind_enabled === 'true') document.getElementById('toggle-hivemind').classList.add('active');
1088
+ else document.getElementById('toggle-hivemind').classList.remove('active');
1089
+
1090
+ // Storage Backend
1091
+ if (s.PRISM_STORAGE) {
1092
+ document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
1093
+ }
1094
+ // Agent Identity
1095
+ if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
1096
+ if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
1097
+ } catch(e) { console.warn('Settings load failed:', e); }
1098
+ }
1099
+
1100
+ function toggleSetting(key, el) {
1101
+ var isActive = el.classList.toggle('active');
1102
+ saveSetting(key, isActive ? 'true' : 'false');
1103
+ }
1104
+ function toggleBootSetting(key, el) {
1105
+ var isActive = el.classList.toggle('active');
1106
+ saveSetting(key, isActive ? 'true' : 'false');
1107
+ showToast('Saved. Restart your AI client for this to take effect.');
1108
+ }
1109
+ function saveBootSetting(key, value) {
1110
+ saveSetting(key, value);
1111
+ showToast('Saved. Restart your AI client for this to take effect.');
1112
+ }
1113
+
1114
+ async function saveSetting(key, value) {
1115
+ try {
1116
+ await fetch('/api/settings', {
1117
+ method: 'POST',
1118
+ headers: { 'Content-Type': 'application/json' },
1119
+ body: JSON.stringify({ key: key, value: value })
1120
+ });
1121
+ if (key === 'dashboard_theme') applyTheme(value);
1122
+ // Refresh identity chip if role or name changed
1123
+ if (key === 'default_role' || key === 'agent_name') loadIdentityChip();
1124
+ showToast('Saved ✓');
1125
+ } catch(e) { console.error('Setting save failed:', e); }
1126
+ }
1127
+
1128
+ /**
1129
+ * applyTheme — sets the data-theme attribute on <html>
1130
+ * CSS custom properties in [data-theme="..."] blocks
1131
+ * override :root defaults instantly, no page reload needed.
1132
+ */
1133
+ function applyTheme(theme) {
1134
+ document.documentElement.setAttribute('data-theme', theme || 'dark');
1135
+ }
1136
+
1137
+ function showToast(msg) {
1138
+ var toast = document.getElementById('savedToast');
1139
+ toast.textContent = msg || 'Saved ✓';
1140
+ toast.classList.add('show');
1141
+ setTimeout(function() { toast.classList.remove('show'); }, 2000);
1142
+ }
1143
+
1144
+ // ─── Hivemind Radar (v3.0) ───
1145
+ async function loadTeam() {
1146
+ var project = document.getElementById('projectSelect').value;
1147
+ if (!project) return;
1148
+ var card = document.getElementById('hivemindCard');
1149
+ try {
1150
+ var res = await fetch('/api/team?project=' + encodeURIComponent(project));
1151
+ var data = await res.json();
1152
+ var team = data.team || [];
1153
+ var list = document.getElementById('teamList');
1154
+ if (team.length > 0) {
1155
+ var roleIcons = {dev:'🛠️',qa:'🔍',pm:'📋',lead:'🏗️',security:'🔒',ux:'🎨',cmo:'📢'};
1156
+ list.innerHTML = team.map(function(a) {
1157
+ var icon = roleIcons[a.role] || '🤖';
1158
+ var ago = a.last_heartbeat ? timeAgo(a.last_heartbeat) : '?';
1159
+ return '<li class="team-item">' +
1160
+ '<span class="pulse-dot"></span>' +
1161
+ '<span class="team-role">' + icon + ' ' + escapeHtml(a.role) + '</span>' +
1162
+ '<span class="team-task">' + escapeHtml(a.current_task || 'idle') + '</span>' +
1163
+ '<span class="team-heartbeat">' + ago + '</span></li>';
1164
+ }).join('');
1165
+ card.style.display = 'block';
1166
+ } else {
1167
+ list.innerHTML = '<li style="color:var(--text-muted);font-size:0.85rem;text-align:center;padding:1rem">No active agents on this project.</li>';
1168
+ card.style.display = 'block';
1169
+ }
1170
+ } catch(e) {
1171
+ console.warn('Team load failed:', e);
1172
+ }
1173
+ }
1174
+
1175
+ function timeAgo(iso) {
1176
+ var diff = Date.now() - new Date(iso).getTime();
1177
+ var mins = Math.floor(diff / 60000);
1178
+ if (mins < 1) return 'just now';
1179
+ if (mins < 60) return mins + 'm ago';
1180
+ return Math.floor(mins/60) + 'h ago';
1181
+ }
1182
+
1183
+ // ─── Brain Health Cleanup (v3.1) ───
1184
+ async function cleanupIssues() {
1185
+ var btn = document.getElementById('cleanupBtn');
1186
+ if (btn) { btn.disabled = true; btn.textContent = 'Cleaning...'; }
1187
+ try {
1188
+ var res = await fetch('/api/health/cleanup', { method: 'POST' });
1189
+ var data = await res.json();
1190
+ showFixedToast(data.message || (data.ok ? 'Cleanup complete.' : 'Cleanup failed.'), data.ok);
1191
+ // Re-run health check to refresh the card
1192
+ setTimeout(async function() {
1193
+ try {
1194
+ var healthRes = await fetch('/api/health');
1195
+ var healthData = await healthRes.json();
1196
+ var healthDot = document.getElementById('healthDot');
1197
+ var healthLabel = document.getElementById('healthLabel');
1198
+ var healthSummary = document.getElementById('healthSummary');
1199
+ var healthIssues = document.getElementById('healthIssues');
1200
+ var cleanupBtn = document.getElementById('cleanupBtn');
1201
+ var statusMap = { healthy: '✅ Healthy', degraded: '⚠️ Degraded', unhealthy: '🔴 Unhealthy' };
1202
+ healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
1203
+ healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
1204
+ var t = healthData.totals || {};
1205
+ healthSummary.textContent = (t.activeEntries || 0) + ' entries · ' + (t.handoffs || 0) + ' handoffs · ' + (t.rollups || 0) + ' rollups';
1206
+ var issues = healthData.issues || [];
1207
+ if (issues.length > 0) {
1208
+ var sevIcons = { error: '🔴', warning: '🟡', info: '🔵' };
1209
+ healthIssues.innerHTML = issues.map(function(i) {
1210
+ return '<div class="issue-row"><span>' + (sevIcons[i.severity] || '❓') + '</span><span>' + escapeHtml(i.message) + '</span></div>';
1211
+ }).join('');
1212
+ if (cleanupBtn) { cleanupBtn.disabled = false; cleanupBtn.textContent = '🧹 Fix Issues'; cleanupBtn.style.display = 'inline-block'; }
1213
+ } else {
1214
+ healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">🎉 No issues found</div>';
1215
+ if (cleanupBtn) cleanupBtn.style.display = 'none';
1216
+ }
1217
+ } catch(e) {}
1218
+ }, 400);
1219
+ } catch(e) {
1220
+ showFixedToast('Cleanup request failed.', false);
1221
+ if (btn) { btn.disabled = false; btn.textContent = '🧹 Fix Issues'; }
1222
+ }
1223
+ }
1224
+
1225
+ function showFixedToast(msg, ok) {
1226
+ var t = document.getElementById('fixedToast');
1227
+ t.textContent = (ok === false ? '❌ ' : '✅ ') + msg;
1228
+ t.classList.add('show');
1229
+ setTimeout(function() { t.classList.remove('show'); }, 3500);
1230
+ }
621
1231
  </script>
622
1232
  </body>
623
1233
  </html>`;