@dtoolkit/dbrain 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +39 -7
  2. package/dist/cli/compact.d.ts +10 -0
  3. package/dist/cli/compact.d.ts.map +1 -0
  4. package/dist/cli/compact.js +44 -0
  5. package/dist/cli/compact.js.map +1 -0
  6. package/dist/cli/configure.d.ts +2 -0
  7. package/dist/cli/configure.d.ts.map +1 -0
  8. package/dist/cli/configure.js +126 -0
  9. package/dist/cli/configure.js.map +1 -0
  10. package/dist/cli/index.js +63 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/init.d.ts.map +1 -1
  13. package/dist/cli/init.js +18 -0
  14. package/dist/cli/init.js.map +1 -1
  15. package/dist/cli/keys.d.ts +11 -0
  16. package/dist/cli/keys.d.ts.map +1 -0
  17. package/dist/cli/keys.js +85 -0
  18. package/dist/cli/keys.js.map +1 -0
  19. package/dist/cli/link.d.ts +12 -0
  20. package/dist/cli/link.d.ts.map +1 -0
  21. package/dist/cli/link.js +118 -0
  22. package/dist/cli/link.js.map +1 -0
  23. package/dist/cli/start.d.ts.map +1 -1
  24. package/dist/cli/start.js +31 -1
  25. package/dist/cli/start.js.map +1 -1
  26. package/dist/cli/status.d.ts.map +1 -1
  27. package/dist/cli/status.js +23 -1
  28. package/dist/cli/status.js.map +1 -1
  29. package/dist/core/compact.d.ts +18 -0
  30. package/dist/core/compact.d.ts.map +1 -0
  31. package/dist/core/compact.js +117 -0
  32. package/dist/core/compact.js.map +1 -0
  33. package/dist/core/config.d.ts +23 -0
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +31 -1
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/connections.d.ts +9 -0
  38. package/dist/core/connections.d.ts.map +1 -0
  39. package/dist/core/connections.js +25 -0
  40. package/dist/core/connections.js.map +1 -0
  41. package/dist/core/db.d.ts.map +1 -1
  42. package/dist/core/db.js +36 -0
  43. package/dist/core/db.js.map +1 -1
  44. package/dist/dashboard/index.html +250 -26
  45. package/dist/mcp/server.d.ts.map +1 -1
  46. package/dist/mcp/server.js +216 -15
  47. package/dist/mcp/server.js.map +1 -1
  48. package/dist/server/index.d.ts.map +1 -1
  49. package/dist/server/index.js +30 -2
  50. package/dist/server/index.js.map +1 -1
  51. package/dist/server/routes/compact.d.ts +3 -0
  52. package/dist/server/routes/compact.d.ts.map +1 -0
  53. package/dist/server/routes/compact.js +29 -0
  54. package/dist/server/routes/compact.js.map +1 -0
  55. package/dist/server/routes/entities.d.ts.map +1 -1
  56. package/dist/server/routes/entities.js +6 -1
  57. package/dist/server/routes/entities.js.map +1 -1
  58. package/dist/server/routes/facts.d.ts.map +1 -1
  59. package/dist/server/routes/facts.js +55 -2
  60. package/dist/server/routes/facts.js.map +1 -1
  61. package/dist/server/routes/health.d.ts.map +1 -1
  62. package/dist/server/routes/health.js +46 -2
  63. package/dist/server/routes/health.js.map +1 -1
  64. package/dist/server/routes/keys.d.ts +3 -0
  65. package/dist/server/routes/keys.d.ts.map +1 -0
  66. package/dist/server/routes/keys.js +73 -0
  67. package/dist/server/routes/keys.js.map +1 -0
  68. package/dist/server/routes/permissions.d.ts +3 -0
  69. package/dist/server/routes/permissions.d.ts.map +1 -0
  70. package/dist/server/routes/permissions.js +9 -0
  71. package/dist/server/routes/permissions.js.map +1 -0
  72. package/dist/server/routes/proxy.d.ts +3 -0
  73. package/dist/server/routes/proxy.d.ts.map +1 -0
  74. package/dist/server/routes/proxy.js +37 -0
  75. package/dist/server/routes/proxy.js.map +1 -0
  76. package/dist/server/routes/search.d.ts.map +1 -1
  77. package/dist/server/routes/search.js +113 -41
  78. package/dist/server/routes/search.js.map +1 -1
  79. package/dist/server/routes/workspace.d.ts.map +1 -1
  80. package/dist/server/routes/workspace.js +5 -0
  81. package/dist/server/routes/workspace.js.map +1 -1
  82. package/package.json +4 -2
@@ -94,6 +94,48 @@ html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t
94
94
  }
95
95
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}
96
96
  .brain-name{font-size:13px;font-weight:600;color:var(--text)}
97
+ .brain-type-badge{font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 6px;border-radius:4px;line-height:1}
98
+ .brain-type-badge.personal{background:oklch(0.90 0.10 250);color:oklch(0.35 0.15 250)}
99
+ .brain-type-badge.shared{background:oklch(0.90 0.10 148);color:oklch(0.30 0.15 148)}
100
+ .sb-user{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:8px}
101
+ .sb-user-info{flex:1;min-width:0}
102
+ .sb-user-name{font-size:12px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
103
+ .sb-user-role{font-size:10px;color:var(--text-3);font-family:var(--mono)}
104
+ .sb-logout{padding:3px 8px;border:1px solid var(--border);border-radius:5px;background:none;color:var(--text-3);font-size:10px;cursor:pointer;flex-shrink:0;transition:all .15s}
105
+ .sb-logout:hover{border-color:#ef4444;color:#ef4444;background:oklch(0.95 0.05 25)}
106
+ .sb-brains-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-3);padding:0 12px;margin-bottom:6px}
107
+ .sb-brain-item{display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--text-2);transition:background .15s}
108
+ .sb-brain-item:hover{background:var(--surface-2)}
109
+ .sb-brain-item.current{color:var(--text);font-weight:600}
110
+ .sb-brain-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
111
+ .sb-brain-dot.online{background:var(--online);box-shadow:0 0 4px var(--online)}
112
+ .sb-brain-dot.offline{background:#ef4444}
113
+ .sb-brain-meta{font-size:10px;color:var(--text-3);margin-left:auto;font-family:var(--mono)}
114
+ .author-badge{font-size:10px;font-family:var(--mono);color:var(--text-3);font-style:italic}
115
+ .origin-badge{font-size:9px;font-weight:600;padding:1px 5px;border-radius:3px;background:oklch(0.90 0.10 148);color:oklch(0.30 0.15 148);font-family:var(--mono)}
116
+ .keys-table{width:100%;border-collapse:collapse;font-size:12px;font-family:var(--mono)}
117
+ .keys-table th{text-align:left;padding:8px 10px;color:var(--text-3);font-weight:600;border-bottom:1px solid var(--border);font-size:11px;text-transform:uppercase;letter-spacing:.5px}
118
+ .keys-table td{padding:8px 10px;border-bottom:1px solid var(--border-subtle);color:var(--text)}
119
+ .keys-table tr:hover td{background:var(--surface-2)}
120
+ .conn-status{display:inline-flex;align-items:center;gap:5px;font-size:12px}
121
+ .conn-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
122
+ .conn-dot.online{background:var(--online);box-shadow:0 0 4px var(--online)}
123
+ .conn-dot.offline{background:#ef4444}
124
+ .conn-card{padding:14px 16px;background:var(--surface);border:1px solid var(--border-subtle);border-radius:10px;margin-bottom:10px}
125
+ .conn-card-head{display:flex;align-items:center;gap:8px;margin-bottom:6px}
126
+ .conn-card-name{font-size:14px;font-weight:600;color:var(--text)}
127
+ .conn-card-url{font-size:11px;font-family:var(--mono);color:var(--text-3)}
128
+ .conn-card-stats{font-size:12px;color:var(--text-2);margin-top:4px}
129
+ .form-row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-bottom:16px}
130
+ .form-field{display:flex;flex-direction:column;gap:4px}
131
+ .form-field label{font-size:11px;font-weight:600;color:var(--text-3);text-transform:uppercase;letter-spacing:.3px}
132
+ .form-field input,.form-field select{padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:12px;font-family:var(--mono)}
133
+ .form-btn{padding:6px 14px;border:none;border-radius:6px;background:var(--accent);color:white;font-size:12px;font-weight:600;cursor:pointer}
134
+ .form-btn:hover{opacity:.85}
135
+ .form-btn.danger{background:#ef4444}
136
+ .inline-msg{font-size:12px;padding:8px 12px;border-radius:6px;margin-bottom:12px}
137
+ .inline-msg.success{background:oklch(0.90 0.10 148);color:oklch(0.30 0.15 148)}
138
+ .inline-msg.error{background:oklch(0.90 0.10 25);color:oklch(0.40 0.15 25)}
97
139
  .brain-meta{font-size:11px;color:var(--text-3);line-height:1.7;font-family:var(--mono)}
98
140
  .main{margin-left:var(--sb);flex:1;min-width:0}
99
141
  .topbar{
@@ -283,7 +325,7 @@ const { useState, useEffect, useMemo, useCallback } = React;
283
325
 
284
326
  const TOKEN_KEY = 'dbrain_token';
285
327
  const THEME_KEY = 'dbrain_theme';
286
- const API_BASE = `http://${window.location.hostname}:7878`;
328
+ const API_BASE = `http://${window.location.hostname}:${Number(window.location.port) - 1}`;
287
329
 
288
330
  /* ── PALETTES ─────────────────────────────────────────── */
289
331
  const PALETTES = {
@@ -297,7 +339,19 @@ function applyPalette(name) {
297
339
  }
298
340
 
299
341
  /* ── API HELPER ───────────────────────────────────────── */
342
+ let _activeBrainPrefix = '';
343
+ function setActiveBrainPrefix(name) { _activeBrainPrefix = name ? `/proxy/${encodeURIComponent(name)}` : ''; }
344
+
300
345
  function api(path, opts = {}) {
346
+ const token = localStorage.getItem(TOKEN_KEY) || '';
347
+ const prefix = _activeBrainPrefix;
348
+ return fetch(`${API_BASE}${prefix}${path}`, {
349
+ ...opts,
350
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...opts.headers },
351
+ }).then(r => { if (!r.ok) throw new Error(r.status); return r.json(); });
352
+ }
353
+
354
+ function localApi(path, opts = {}) {
301
355
  const token = localStorage.getItem(TOKEN_KEY) || '';
302
356
  return fetch(`${API_BASE}${path}`, {
303
357
  ...opts,
@@ -362,13 +416,25 @@ function BackIcon() {
362
416
  }
363
417
 
364
418
  /* ── SIDEBAR ──────────────────────────────────────────── */
365
- function Sidebar({ route, nav, palette, onTogglePalette, brain }) {
366
- const links = [
367
- { href: '#/dashboard', label: 'Brain', icon: (
368
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
369
- <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z" />
370
- <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z" />
371
- </svg>) },
419
+ const BrainIcon = () => (
420
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
421
+ <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z" />
422
+ <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z" />
423
+ </svg>
424
+ );
425
+
426
+ function Sidebar({ route, nav, palette, onTogglePalette, brain, connections, user, onLogout, activeBrain, onBrainSwitch }) {
427
+ const brainItems = [
428
+ { name: null, label: brain.name || 'My Brain', badge: brain.brainType, online: true },
429
+ ...(connections || []).map(c => ({
430
+ name: c.name,
431
+ label: c.brainName || c.name,
432
+ badge: 'shared',
433
+ online: c.online,
434
+ })),
435
+ ];
436
+
437
+ const toolLinks = [
372
438
  { href: '#/api', label: 'REST API', icon: (
373
439
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
374
440
  <polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
@@ -380,12 +446,18 @@ function Sidebar({ route, nav, palette, onTogglePalette, brain }) {
380
446
  <line x1="6" y1="7" x2="9.5" y2="10.5" /><line x1="18" y1="7" x2="14.5" y2="10.5" />
381
447
  <line x1="6" y1="17" x2="9.5" y2="13.5" /><line x1="18" y1="17" x2="14.5" y2="13.5" />
382
448
  </svg>) },
383
- ];
449
+ brain.brainType === 'shared' && { href: '#/keys', label: 'API Keys', icon: (
450
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
451
+ <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
452
+ </svg>) },
453
+ ].filter(Boolean);
454
+
384
455
  const active = (href) => {
385
456
  const p = href.replace('#', '');
386
457
  if (p === '/dashboard') return route === '/' || route === '/dashboard' || route.startsWith('/entity') || route.startsWith('/conversation');
387
458
  return route.startsWith(p);
388
459
  };
460
+
389
461
  return (
390
462
  <aside className="sidebar">
391
463
  <div className="sb-logo">
@@ -396,7 +468,17 @@ function Sidebar({ route, nav, palette, onTogglePalette, brain }) {
396
468
  </div>
397
469
  </div>
398
470
  <nav className="sb-nav">
399
- {links.map(l => (
471
+ {brainItems.map(b => (
472
+ <button key={b.name ?? '_local'} className={`sb-link ${activeBrain === b.name ? 'active' : ''}`}
473
+ onClick={() => b.online && onBrainSwitch(b.name)}
474
+ style={!b.online ? {opacity:.5,cursor:'default'} : {}}>
475
+ <span className={`sb-brain-dot ${b.online ? 'online' : 'offline'}`} style={{marginRight:-2}}></span>
476
+ {b.label}
477
+ <span className={`brain-type-badge ${b.badge}`} style={{marginLeft:'auto'}}>{b.badge}</span>
478
+ </button>
479
+ ))}
480
+ <div className="sb-divider" style={{margin:'6px 0'}}></div>
481
+ {toolLinks.map(l => (
400
482
  <button key={l.href} className={`sb-link ${active(l.href) ? 'active' : ''}`} onClick={() => nav(l.href)}>
401
483
  {l.icon}{l.label}
402
484
  </button>
@@ -411,17 +493,13 @@ function Sidebar({ route, nav, palette, onTogglePalette, brain }) {
411
493
  <button className={`palette-toggle-opt ${palette === 'ocean' ? 'active' : ''}`} onClick={() => onTogglePalette('ocean')}>&#9679; Dark</button>
412
494
  </div>
413
495
  </div>
414
- <div className="brain-card">
415
- <div className="brain-card-head">
416
- <span className="online-dot"></span>
417
- <span className="brain-name">{brain.name || 'dBrain'}</span>
496
+ {user && <div className="sb-user">
497
+ <div className="sb-user-info">
498
+ <span className="sb-user-name">{user.userName}</span>
499
+ <span className="sb-user-role">{user.isAdmin ? 'admin' : user.permissions}</span>
418
500
  </div>
419
- <div className="brain-meta">
420
- {brain.entities || 0} entities<br />
421
- {(brain.facts || 0).toLocaleString()} facts<br />
422
- {brain.conversations || 0} conversations
423
- </div>
424
- </div>
501
+ <button className="sb-logout" onClick={onLogout}>Logout</button>
502
+ </div>}
425
503
  </div>
426
504
  </aside>
427
505
  );
@@ -459,7 +537,7 @@ function LoginPage({ onLogin }) {
459
537
  }
460
538
 
461
539
  /* ── DASHBOARD PAGE ───────────────────────────────────── */
462
- function DashboardPage({ nav, overview, conversations }) {
540
+ function DashboardPage({ nav, overview, conversations, brainName }) {
463
541
  const [q, setQ] = useState('');
464
542
  const [srcFilter, setSrcFilter] = useState('all');
465
543
 
@@ -486,7 +564,7 @@ function DashboardPage({ nav, overview, conversations }) {
486
564
  return (
487
565
  <>
488
566
  <div className="topbar">
489
- <span className="topbar-title">Overview</span>
567
+ <span className="topbar-title">{brainName || 'Overview'}</span>
490
568
  <span className="topbar-sep">&middot;</span>
491
569
  <div className="search-bar">
492
570
  <SearchIcon />
@@ -616,6 +694,8 @@ function EntityDetailPage({ id, nav }) {
616
694
  <div className="fact-meta">
617
695
  <TierPill tier={f.tier} count={null} />
618
696
  <span>accessed {f.access_count || 0}x &middot; last {f.last_accessed ? f.last_accessed.split('T')[0] : 'n/a'}</span>
697
+ {f.author_id && <span className="author-badge">by {f.author_id}</span>}
698
+ {f.origin_brain && <span className="origin-badge">{f.origin_brain}</span>}
619
699
  </div>
620
700
  </div>
621
701
  ))}
@@ -830,6 +910,125 @@ dbrain connect claude http://server:7878 --token=sk-dbr_...
830
910
  );
831
911
  }
832
912
 
913
+ /* ── KEYS PAGE (shared brains) ───────────────────────── */
914
+ function KeysPage() {
915
+ const [keys, setKeys] = useState([]);
916
+ const [loading, setLoading] = useState(true);
917
+ const [isAdmin, setIsAdmin] = useState(true);
918
+ const [userId, setUserId] = useState('');
919
+ const [userName, setUserName] = useState('');
920
+ const [perms, setPerms] = useState('read+write');
921
+ const [created, setCreated] = useState(null);
922
+ const [err, setErr] = useState('');
923
+
924
+ const load = () => localApi('/keys').then(setKeys).catch(e => { if (String(e).includes('403')) setIsAdmin(false); }).finally(() => setLoading(false));
925
+ useEffect(() => { load(); }, []);
926
+
927
+ const create = () => {
928
+ if (!userId.trim() || !userName.trim()) return;
929
+ setErr(''); setCreated(null);
930
+ localApi('/keys', { method: 'POST', body: JSON.stringify({ userId: userId.trim(), userName: userName.trim(), permissions: perms }) })
931
+ .then(k => { setCreated(k); setUserId(''); setUserName(''); load(); })
932
+ .catch(() => setErr('Failed to create key'));
933
+ };
934
+
935
+ const revoke = (id) => {
936
+ if (!confirm('Revoke this key?')) return;
937
+ localApi(`/keys/${id}`, { method: 'DELETE' }).then(() => load()).catch(() => {});
938
+ };
939
+
940
+ if (!isAdmin) return (
941
+ <>
942
+ <div className="topbar"><span className="topbar-title">API Keys</span></div>
943
+ <div className="page"><div className="empty" style={{padding:40}}>
944
+ <p style={{marginBottom:8}}>Admin access required</p>
945
+ <p style={{fontSize:12,color:'var(--text-3)'}}>Log in with the master token to manage API keys.</p>
946
+ </div></div>
947
+ </>
948
+ );
949
+
950
+ return (
951
+ <>
952
+ <div className="topbar"><span className="topbar-title">API Keys</span></div>
953
+ <div className="page">
954
+ <div className="sec-head"><span className="sec-title">Create Key</span></div>
955
+ <div className="form-row">
956
+ <div className="form-field"><label>User ID</label><input value={userId} onChange={e => setUserId(e.target.value)} placeholder="e.g. ivan" /></div>
957
+ <div className="form-field"><label>Display Name</label><input value={userName} onChange={e => setUserName(e.target.value)} placeholder="e.g. Iván Campillo" /></div>
958
+ <div className="form-field"><label>Permissions</label>
959
+ <select value={perms} onChange={e => setPerms(e.target.value)}>
960
+ <option value="read+write">read+write</option><option value="read">read</option><option value="write">write</option>
961
+ </select>
962
+ </div>
963
+ <button className="form-btn" onClick={create}>Create</button>
964
+ </div>
965
+ {created && <div className="inline-msg success">Key created. Token: <strong>{created.token}</strong> — save it now, it won't be shown again.</div>}
966
+ {err && <div className="inline-msg error">{err}</div>}
967
+
968
+ <div className="sec-head" style={{marginTop:24}}><span className="sec-title">Active Keys</span><span className="sec-badge">{keys.filter(k => k.status === 'active').length}</span></div>
969
+ {loading ? <div className="empty">Loading...</div> : (
970
+ <table className="keys-table">
971
+ <thead><tr><th>User</th><th>Permissions</th><th>Token</th><th>Last Used</th><th>Status</th><th></th></tr></thead>
972
+ <tbody>
973
+ {keys.map(k => (
974
+ <tr key={k.id}>
975
+ <td>{k.userName} <span style={{color:'var(--text-3)'}}>({k.userId})</span></td>
976
+ <td>{k.permissions}</td>
977
+ <td style={{color:'var(--text-3)'}}>{k.tokenPreview}</td>
978
+ <td style={{color:'var(--text-3)'}}>{k.lastUsed ? k.lastUsed.split('T')[0] : 'never'}</td>
979
+ <td>{k.status === 'active' ? <span style={{color:'var(--online)'}}>active</span> : <span style={{color:'#ef4444'}}>revoked</span>}</td>
980
+ <td>{k.status === 'active' && <button className="form-btn danger" onClick={() => revoke(k.id)} style={{padding:'3px 8px',fontSize:10}}>Revoke</button>}</td>
981
+ </tr>
982
+ ))}
983
+ {keys.length === 0 && <tr><td colSpan="6" style={{textAlign:'center',color:'var(--text-3)',padding:20}}>No API keys</td></tr>}
984
+ </tbody>
985
+ </table>
986
+ )}
987
+ </div>
988
+ </>
989
+ );
990
+ }
991
+
992
+ /* ── CONNECTIONS PAGE (personal brains) ──────────────── */
993
+ function ConnectionsPage() {
994
+ const [conns, setConns] = useState([]);
995
+ const [loading, setLoading] = useState(true);
996
+
997
+ useEffect(() => {
998
+ localApi('/connections').then(setConns).catch(() => {}).finally(() => setLoading(false));
999
+ }, []);
1000
+
1001
+ return (
1002
+ <>
1003
+ <div className="topbar"><span className="topbar-title">Connected Brains</span></div>
1004
+ <div className="page">
1005
+ {loading ? <div className="empty">Loading...</div> : conns.length === 0 ? (
1006
+ <div className="empty" style={{padding:40}}>
1007
+ <p style={{marginBottom:8}}>No connected brains</p>
1008
+ <p style={{fontSize:12,color:'var(--text-3)'}}>Use <code style={{background:'var(--surface-2)',padding:'2px 6px',borderRadius:4}}>dbrain link &lt;url&gt; --token &lt;token&gt;</code> to connect to a shared brain.</p>
1009
+ </div>
1010
+ ) : (
1011
+ <>
1012
+ <div className="sec-head"><span className="sec-title">Connections</span><span className="sec-badge">{conns.length}</span></div>
1013
+ {conns.map(c => (
1014
+ <div key={c.name} className="conn-card">
1015
+ <div className="conn-card-head">
1016
+ <span className={`conn-dot ${c.online ? 'online' : 'offline'}`}></span>
1017
+ <span className="conn-card-name">{c.brainName || c.name}</span>
1018
+ <span className="brain-type-badge shared">shared</span>
1019
+ </div>
1020
+ <div className="conn-card-url">{c.url}</div>
1021
+ {c.online && <div className="conn-card-stats">{c.entities} entities &middot; {c.facts} facts</div>}
1022
+ {!c.online && <div className="conn-card-stats" style={{color:'#ef4444'}}>offline</div>}
1023
+ </div>
1024
+ ))}
1025
+ </>
1026
+ )}
1027
+ </div>
1028
+ </>
1029
+ );
1030
+ }
1031
+
833
1032
  /* ── APP ──────────────────────────────────────────────── */
834
1033
  function App() {
835
1034
  const { route, segs, nav } = useRouter();
@@ -838,6 +1037,9 @@ function App() {
838
1037
  const [health, setHealth] = useState(null);
839
1038
  const [overview, setOverview] = useState(null);
840
1039
  const [conversations, setConversations] = useState(null);
1040
+ const [connections, setConnections] = useState([]);
1041
+ const [user, setUser] = useState(null);
1042
+ const [activeBrain, setActiveBrain] = useState(null);
841
1043
  const authed = Boolean(token);
842
1044
 
843
1045
  const togglePalette = (p) => {
@@ -850,10 +1052,24 @@ function App() {
850
1052
 
851
1053
  useEffect(() => {
852
1054
  if (!authed) return;
853
- api('/health').then(setHealth).catch(() => { localStorage.removeItem(TOKEN_KEY); setToken(''); });
1055
+ localApi('/health').then(setHealth).catch(() => { localStorage.removeItem(TOKEN_KEY); setToken(''); });
1056
+ localApi('/connections').then(setConnections).catch(() => {});
1057
+ localApi('/me').then(setUser).catch(() => {});
1058
+ }, [authed]);
1059
+
1060
+ useEffect(() => {
1061
+ if (!authed) return;
1062
+ setActiveBrainPrefix(activeBrain);
1063
+ setOverview(null);
1064
+ setConversations(null);
854
1065
  api('/memory/summary').then(setOverview).catch(() => {});
855
1066
  api('/conversations?limit=200').then(setConversations).catch(() => {});
856
- }, [authed]);
1067
+ }, [authed, activeBrain]);
1068
+
1069
+ const handleBrainSwitch = useCallback((name) => {
1070
+ setActiveBrain(name);
1071
+ nav('#/dashboard');
1072
+ }, []);
857
1073
 
858
1074
  useEffect(() => {
859
1075
  if (!authed && route !== '/login') nav('#/login');
@@ -866,24 +1082,32 @@ function App() {
866
1082
 
867
1083
  const brain = {
868
1084
  name: health.name || 'dBrain',
1085
+ brainType: health.brainType || 'personal',
869
1086
  version: health.version || '0.1.0',
870
1087
  entities: health.entities || 0,
871
1088
  facts: overview ? overview.reduce((s, e) => s + e.total, 0) : (health.facts || 0),
872
1089
  conversations: health.conversations || 0,
873
1090
  };
874
1091
 
1092
+ const activeBrainName = activeBrain
1093
+ ? (connections.find(c => c.name === activeBrain)?.brainName || activeBrain)
1094
+ : null;
1095
+
875
1096
  const renderPage = () => {
876
1097
  const [sec, ...rest] = segs;
877
1098
  if (sec === 'entity') return <EntityDetailPage id={rest[0]} nav={nav} />;
878
1099
  if (sec === 'conversation') return <ConversationDetailPage id={rest[0]} nav={nav} />;
879
1100
  if (sec === 'api') return <APIDocsPage />;
880
1101
  if (sec === 'mcp') return <MCPDocsPage />;
881
- return <DashboardPage nav={nav} overview={overview} conversations={conversations} />;
1102
+ if (sec === 'keys' && brain.brainType === 'shared') return <KeysPage />;
1103
+ return <DashboardPage nav={nav} overview={overview} conversations={conversations} brainName={activeBrainName} />;
882
1104
  };
883
1105
 
884
1106
  return (
885
1107
  <div className="shell">
886
- <Sidebar route={route} nav={nav} palette={palette} onTogglePalette={togglePalette} brain={brain} />
1108
+ <Sidebar route={route} nav={nav} palette={palette} onTogglePalette={togglePalette} brain={brain} connections={connections} user={user}
1109
+ activeBrain={activeBrain} onBrainSwitch={handleBrainSwitch}
1110
+ onLogout={() => { localStorage.removeItem(TOKEN_KEY); setToken(''); setHealth(null); setUser(null); setActiveBrain(null); nav('#/login'); }} />
887
1111
  <div className="main">{renderPage()}</div>
888
1112
  </div>
889
1113
  );
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAU/C,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,aAicnD;AA0BD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,eAAe,QAmC5C"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAc/C,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,aAgrBnD;AA0BD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,eAAe,QAmC5C"}