total-recall-brain 3.21.2 → 3.21.3

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 (68) hide show
  1. package/frontend/dist/assets/index-BwUTxKNm.js +5742 -0
  2. package/frontend/dist/assets/index-C7_Xpk27.css +1 -0
  3. package/frontend/dist/index.html +2 -2
  4. package/frontend/src/App.spec.tsx +1 -1
  5. package/frontend/src/api/_base.ts +3 -3
  6. package/frontend/src/api/mesh.ts +47 -0
  7. package/frontend/src/api/network.spec.ts +9 -9
  8. package/frontend/src/api/network.ts +4 -4
  9. package/frontend/src/api/notifications.ts +5 -4
  10. package/frontend/src/api/webhooks.ts +1 -1
  11. package/frontend/src/components/BrainSelector.spec.tsx +4 -4
  12. package/frontend/src/components/BrainSelector.tsx +1 -1
  13. package/frontend/src/components/ContextualHelp.spec.tsx +14 -14
  14. package/frontend/src/components/ContextualHelp.tsx +3 -1
  15. package/frontend/src/components/DocumentTable.spec.tsx +1 -1
  16. package/frontend/src/components/Graph3D.spec.tsx +4 -4
  17. package/frontend/src/components/Graph3D.tsx +3 -3
  18. package/frontend/src/components/MobilePairing.spec.tsx +1 -1
  19. package/frontend/src/components/MobilePairing.tsx +1 -1
  20. package/frontend/src/components/UsageChart.spec.tsx +4 -4
  21. package/frontend/src/components/UsageChart.tsx +1 -1
  22. package/frontend/src/components/VoiceInput.spec.tsx +6 -6
  23. package/frontend/src/components/VoiceInput.tsx +19 -4
  24. package/frontend/src/components/brand/BrandMark.spec.tsx +3 -4
  25. package/frontend/src/index.css +6 -0
  26. package/frontend/src/pages/AutomationsPage.spec.tsx +4 -4
  27. package/frontend/src/pages/ChatPage.spec.tsx +3 -3
  28. package/frontend/src/pages/CollabPage.spec.tsx +1 -2
  29. package/frontend/src/pages/DesignDocsPage.spec.tsx +1 -1
  30. package/frontend/src/pages/FilesPage.spec.tsx +2 -2
  31. package/frontend/src/pages/GraphPage.spec.tsx +1 -1
  32. package/frontend/src/pages/HealthPage.spec.tsx +2 -2
  33. package/frontend/src/pages/HelpPage.spec.tsx +5 -5
  34. package/frontend/src/pages/InboxPage.spec.tsx +4 -4
  35. package/frontend/src/pages/InstructionsPage.spec.tsx +5 -5
  36. package/frontend/src/pages/IntegrationsPage.spec.tsx +7 -7
  37. package/frontend/src/pages/MemoryPage.spec.tsx +2 -2
  38. package/frontend/src/pages/MeshPage.spec.tsx +111 -2
  39. package/frontend/src/pages/MeshPage.tsx +90 -11
  40. package/frontend/src/pages/NetworkPage.spec.tsx +7 -8
  41. package/frontend/src/pages/NetworkPage.tsx +12 -12
  42. package/frontend/src/pages/NotificationsPage.tsx +9 -9
  43. package/frontend/src/pages/OnboardingPage.spec.tsx +7 -7
  44. package/frontend/src/pages/OpenWikiPage.tsx +3 -3
  45. package/frontend/src/pages/RulesPage.tsx +3 -3
  46. package/frontend/src/pages/SecretsPage.spec.tsx +5 -5
  47. package/frontend/src/pages/SecretsPage.tsx +13 -14
  48. package/frontend/src/pages/SettingsPage.spec.tsx +6 -6
  49. package/frontend/src/pages/SettingsPage.tsx +2 -2
  50. package/frontend/src/pages/SkillsPage.spec.tsx +1 -2
  51. package/frontend/src/pages/SkillsPage.tsx +3 -3
  52. package/frontend/src/pages/TasksPage.spec.tsx +3 -3
  53. package/frontend/src/pages/UsagePage.spec.tsx +2 -2
  54. package/frontend/src/pages/UsagePage.tsx +1 -1
  55. package/frontend/src/pages/WebhooksPage.spec.tsx +1 -1
  56. package/frontend/src/pages/WebhooksPage.tsx +19 -19
  57. package/frontend/src/setupTests.ts +4 -4
  58. package/package.json +1 -1
  59. package/scaffold/.agent/skills/total-recall/SKILL.md +53 -0
  60. package/scaffold/.agent/skills/total-recall/modules/agents/agents.yml +15 -13
  61. package/src/core/daemon-loop.mjs +28 -1
  62. package/src/core/headscale-client.mjs +227 -0
  63. package/src/core/mesh-enroll.mjs +439 -0
  64. package/src/server/route-manifest.json +8 -0
  65. package/src/server/routes/headscale.mjs +21 -81
  66. package/src/server/routes/mesh.mjs +41 -0
  67. package/frontend/dist/assets/index-B4RASWbV.css +0 -1
  68. package/frontend/dist/assets/index-D0XeeXhG.js +0 -5742
@@ -0,0 +1 @@
1
+ :root{--bg-primary:#070b14;--bg-secondary:#0c1220;--bg-tertiary:#121a2b;--bg-elevated:#172033;--bg-hover:#1c2740;--bg-active:#243352;--accent:#3b82f6;--accent-deep:#1e3a8a;--accent-glow:#3b82f647;--accent-hover:#60a5fa;--accent-muted:#3b82f624;--accent-soft:#93c5fd1f;--purple-primary:var(--accent);--purple-border:var(--border-accent);--success:#34d399;--success-muted:#34d3991f;--warning:#fbbf24;--warning-muted:#fbbf241f;--error:#f87171;--error-muted:#f871711f;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--text-tertiary:#64748b;--text-inverse:#070b14;--border:#94a3b81a;--border-hover:#94a3b838;--border-accent:#3b82f659;--glass:#0c1220b8;--glass-border:#94a3b81f;--mesh-1:#1e3a8a59;--mesh-2:#3b82f61f;--mesh-3:#0f172ae6;--font-sans:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", "SF Mono", ui-monospace, monospace;--sidebar-width:252px;--radius-sm:8px;--radius-md:12px;--radius-lg:18px;--radius-xl:24px;--shadow-sm:0 1px 2px #00000059;--shadow-md:0 8px 24px #00000059;--shadow-lg:0 24px 64px #00000073;--shadow-glow:0 0 40px #3b82f62e;--transition-fast:.14s cubic-bezier(.23, 1, .32, 1);--transition-normal:.22s cubic-bezier(.23, 1, .32, 1)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{background:var(--bg-primary);height:100%;color:var(--text-primary);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;font-size:14px;line-height:1.6}body{background:radial-gradient(1200px 600px at 10% -10%, var(--mesh-1), transparent 55%), radial-gradient(900px 500px at 100% 0%, var(--mesh-2), transparent 50%), radial-gradient(800px 400px at 50% 100%, #1e3a8a33, transparent 60%), var(--bg-primary)}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover)}button{font-family:var(--font-sans);cursor:pointer;border:none;outline:none}code,pre{font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-tertiary)}.app-layout{height:100vh;display:flex;position:relative;overflow:hidden}.sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);background:var(--glass);-webkit-backdrop-filter:blur(20px)saturate(1.4);border-right:1px solid var(--glass-border);transition:transform var(--transition-normal);z-index:100;flex-direction:column;padding:14px 0 12px;display:flex;box-shadow:4px 0 40px #00000040}.sidebar-brand{border-bottom:1px solid var(--border);flex-direction:column;gap:10px;margin-bottom:10px;padding:4px 16px 18px;display:flex}.sidebar-brand-row{align-items:center;gap:12px;display:flex}.sidebar-brand h1{color:var(--text-primary);letter-spacing:-.03em;font-size:15px;font-weight:700;line-height:1.2}.sidebar-brand span{color:var(--text-tertiary);letter-spacing:.02em;font-size:11px;font-weight:500}.sidebar-brand-lockup{background:linear-gradient(#f8fafc 0%,#e8eef9 100%);border-radius:12px;width:100%;padding:10px 12px;overflow:hidden;box-shadow:inset 0 1px #ffffffb3,0 8px 20px #0f172a59,0 0 0 1px #94a3b833}.sidebar-brand-lockup img{object-fit:contain;width:100%;height:auto;display:block}.sidebar-nav{scrollbar-width:thin;flex-direction:column;flex:1;gap:2px;padding:0 10px;display:flex;overflow-y:auto}.nav-section-label{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);opacity:.85;padding:12px 12px 6px;font-size:10px;font-weight:600}.nav-link{color:var(--text-secondary);transition:all var(--transition-fast);border:1px solid #0000;border-radius:10px;align-items:center;gap:10px;padding:9px 12px;font-size:13px;font-weight:500;text-decoration:none;display:flex;position:relative}.nav-link:hover{color:var(--text-primary);background:#94a3b814}.nav-link.active{color:#dbeafe;border-color:var(--border-accent);background:linear-gradient(135deg,#3b82f62e,#1e3a8a38);box-shadow:0 0 0 1px #3b82f614,inset 0 1px #ffffff0a}.nav-link.active:before{content:"";background:linear-gradient(#60a5fa,#2563eb);border-radius:0 3px 3px 0;width:3px;position:absolute;top:20%;bottom:20%;left:0;box-shadow:0 0 12px #3b82f699}.nav-link svg{opacity:.72;flex-shrink:0;width:18px;height:18px}.nav-link.active svg{opacity:1;color:#93c5fd}.nav-link:hover svg{opacity:.95}.sidebar-footer{border-top:1px solid var(--border);background:linear-gradient(#0000,#0f172a66);margin-top:8px;padding:12px 14px}.main-content{background:0 0;flex-direction:column;flex:1;min-width:0;display:flex;overflow:hidden}.page{flex:1;padding:28px 32px 40px;overflow-y:auto}.page-header{margin-bottom:28px}.page-header h1{letter-spacing:-.03em;background:linear-gradient(135deg,#f8fafc 0%,#93c5fd 100%);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;font-size:24px;font-weight:700}.page-header p{color:var(--text-secondary);max-width:56ch;margin-top:6px;font-size:13.5px}.card{border:1px solid var(--border);border-radius:var(--radius-md);transition:border-color var(--transition-normal), box-shadow var(--transition-normal), transform var(--transition-normal);box-shadow:var(--shadow-sm);background:linear-gradient(165deg,#172033e6,#0c1220f2);padding:20px}.card:hover{border-color:var(--border-hover);box-shadow:var(--shadow-md)}.card-grid{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;display:grid}.btn{transition:all var(--transition-fast);letter-spacing:-.01em;border-radius:10px;align-items:center;gap:6px;padding:9px 16px;font-size:13px;font-weight:600;display:inline-flex}.btn-primary{color:#fff;background:linear-gradient(135deg,#3b82f6 0%,#2563eb 55%,#1d4ed8 100%);box-shadow:inset 0 1px #ffffff26,0 8px 20px #2563eb47}.btn-primary:hover{box-shadow:0 0 28px var(--accent-glow), 0 8px 24px #2563eb59;background:linear-gradient(135deg,#60a5fa 0%,#3b82f6 50%,#2563eb 100%);transform:translateY(-1px)}.btn-ghost{color:var(--text-secondary);border:1px solid var(--border);background:#94a3b80f}.btn-ghost:hover{color:var(--text-primary);border-color:var(--border-hover);background:#94a3b81f}.btn-sm{padding:6px 11px;font-size:12px}.alert{border-radius:var(--radius-sm);border:1px solid #0000;padding:12px 16px;font-size:13px;font-weight:500}.alert-error{background:var(--error-muted);color:var(--error);border-color:#f8717140}.alert-success{background:var(--success-muted);color:var(--success);border-color:#34d39940}.alert-warning{background:var(--warning-muted);color:var(--warning);border-color:#fbbf2440}.input,.select{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-family:var(--font-sans);transition:border-color var(--transition-fast), box-shadow var(--transition-fast);outline:none;padding:9px 12px;font-size:13px}.input:focus,.select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-muted)}.input::placeholder{color:var(--text-tertiary)}.select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 10px center;background-repeat:no-repeat;padding-right:32px}.data-table{border-collapse:collapse;width:100%}.data-table th{text-align:left;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);background:var(--bg-secondary);padding:12px 16px;font-size:12px;font-weight:500}.data-table td{border-bottom:1px solid var(--border);color:var(--text-primary);padding:14px 16px;font-size:13px}.data-table tbody tr{transition:background var(--transition-fast)}.data-table tbody tr:hover{background:var(--bg-hover)}.data-table tr.is-leader td{background:var(--accent-soft)}.page-container{max-width:1200px;margin:0 auto;padding:32px}.page-header{justify-content:space-between;align-items:center;margin-bottom:32px;display:flex}.page-header h1{color:var(--text-primary);font-size:24px;font-weight:600}.badge{letter-spacing:.03em;border-radius:4px;align-items:center;padding:3px 8px;font-size:11px;font-weight:600;display:inline-flex}.badge-online,.badge-active{background:var(--success-muted);color:var(--success)}.badge-offline,.badge-inactive{background:var(--error-muted);color:var(--error)}.badge-leader{color:var(--accent-hover);background:#3b82f626}.badge-follower{background:var(--bg-hover);color:var(--text-secondary)}.surface-premium{border:1px solid var(--glass-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-md), inset 0 1px 0 #ffffff0a;background:linear-gradient(165deg,#172033eb,#0c1220f5)}.login-shell{justify-content:center;align-items:center;min-height:100vh;padding:24px;display:flex;position:relative;overflow:hidden}.login-shell:before{content:"";pointer-events:none;background:radial-gradient(50% 40% at 20% 20%,#2563eb47,#0000 55%),radial-gradient(45% 35% at 80% 70%,#1e3a8a59,#0000 50%),radial-gradient(30% 25%,#60a5fa14,#0000 60%);animation:18s ease-in-out infinite alternate meshDrift;position:absolute;inset:-20%}@keyframes meshDrift{0%{transform:translate(0,0)scale(1)}to{transform:translate(-2%,2%)scale(1.05)}}.login-card{z-index:1;border-radius:var(--radius-xl);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);background:linear-gradient(165deg,#121a2beb,#080c16f5);border:1px solid #94a3b824;flex-direction:column;gap:28px;width:min(400px,100%);padding:40px 36px;display:flex;position:relative;box-shadow:0 0 0 1px #3b82f614,0 32px 80px #0000008c,0 0 80px #2563eb1f}.onboarding-shell{max-width:680px;margin:0 auto;padding:36px 22px 72px}.onboarding-progress{background:#94a3b81a;border-radius:99px;height:6px;overflow:hidden;box-shadow:inset 0 1px 2px #0000004d}.onboarding-progress>div{background:linear-gradient(90deg,#1e40af,#3b82f6,#93c5fd);border-radius:99px;height:100%;transition:width .3s cubic-bezier(.23,1,.32,1);box-shadow:0 0 16px #3b82f680}.loop-card{border:1px solid var(--border-accent);border-radius:var(--radius-md);font-family:var(--font-mono);color:#cbd5e1;background:linear-gradient(145deg,#1e3a8a40,#0c1220cc);padding:18px 20px;font-size:12.5px;line-height:1.85;box-shadow:inset 0 1px #ffffff0a}.badge{text-transform:uppercase;letter-spacing:.04em;border-radius:99px;align-items:center;gap:4px;padding:3px 8px;font-size:11px;font-weight:600;display:inline-flex}.badge-success{background:var(--success-muted);color:var(--success)}.badge-warning{background:var(--warning-muted);color:var(--warning)}.badge-error{background:var(--error-muted);color:var(--error)}.badge-accent{background:var(--accent-muted);color:var(--accent)}.chat-container{flex-direction:column;flex:1;height:calc(100vh - 1px);display:flex;overflow:hidden}.chat-messages{flex-direction:column;flex:1;gap:16px;padding:24px 32px;display:flex;overflow-y:auto}.chat-empty{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;display:flex}.chat-empty svg{opacity:.3;width:48px;height:48px}.chat-empty h2{color:var(--text-secondary);font-size:18px;font-weight:600}.chat-empty p{font-size:13px}.message{border-radius:var(--radius-md);white-space:pre-wrap;word-break:break-word;max-width:720px;padding:14px 18px;font-size:14px;line-height:1.7;animation:.2s msgIn}@keyframes msgIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.message-user{color:#fff;background:linear-gradient(135deg,#3b82f6 0%,#2563eb 100%);border-bottom-right-radius:4px;align-self:flex-end;box-shadow:0 8px 24px #2563eb38}.message-assistant{border:1px solid var(--border);box-shadow:var(--shadow-sm);background:linear-gradient(165deg,#172033f2,#0c1220fa);border-bottom-left-radius:4px;align-self:flex-start}.message-assistant code{background:var(--bg-primary);border-radius:4px;padding:1px 5px;font-size:12.5px}.chat-input-bar{border-top:1px solid var(--border);background:var(--bg-secondary);padding:16px 32px 20px}.chat-input-wrapper{align-items:flex-end;gap:8px;max-width:800px;margin:0 auto;display:flex}.chat-input-wrapper textarea{resize:none;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);font-family:var(--font-sans);min-height:44px;max-height:160px;transition:border-color var(--transition-fast);flex:1;padding:12px 16px;font-size:14px;line-height:1.5}.chat-input-wrapper textarea:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.chat-input-wrapper textarea::placeholder{color:var(--text-tertiary)}.chat-send-btn{border-radius:var(--radius-md);color:#fff;width:44px;height:44px;transition:all var(--transition-fast);background:linear-gradient(135deg,#3b82f6,#1d4ed8);flex-shrink:0;justify-content:center;align-items:center;display:flex;box-shadow:0 4px 14px #2563eb59}.chat-send-btn:hover{box-shadow:0 0 24px var(--accent-glow);background:linear-gradient(135deg,#60a5fa,#2563eb);transform:translateY(-1px)}.chat-send-btn:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}.chat-send-btn svg{width:18px;height:18px}.memory-layout{grid-template-columns:220px 1fr;gap:20px;height:calc(100vh - 120px);display:grid}.memory-sidebar{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;overflow-y:auto}.memory-sidebar h3{text-transform:uppercase;letter-spacing:.06em;color:var(--text-tertiary);padding:8px 8px 6px;font-size:11px}.category-btn{border-radius:var(--radius-sm);width:100%;color:var(--text-secondary);text-align:left;transition:all var(--transition-fast);background:0 0;justify-content:space-between;align-items:center;padding:8px 10px;font-size:13px;display:flex}.category-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.category-btn.active{background:var(--accent-muted);color:var(--accent)}.category-count{background:var(--bg-tertiary);color:var(--text-tertiary);border-radius:99px;padding:1px 6px;font-size:11px}.memory-main{flex-direction:column;gap:12px;display:flex;overflow:hidden}.memory-search{gap:8px;display:flex}.memory-search input{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);font-size:13px;font-family:var(--font-sans);transition:border-color var(--transition-fast);flex:1;padding:10px 14px}.memory-search input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.memory-search input::placeholder{color:var(--text-tertiary)}.memory-list{flex-direction:column;flex:1;gap:8px;display:flex;overflow-y:auto}.memory-node-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;transition:all var(--transition-fast);padding:14px 16px}.memory-node-card:hover{border-color:var(--accent);background:var(--bg-tertiary)}.memory-node-card.selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent-glow)}.memory-node-card h4{margin-bottom:4px;font-size:14px;font-weight:600}.memory-node-card .meta{color:var(--text-tertiary);gap:12px;font-size:12px;display:flex}.memory-detail{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:24px;overflow-y:auto}.memory-detail h2{margin-bottom:12px;font-size:18px}.memory-detail pre{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:pre-wrap;color:var(--text-secondary);padding:16px;font-size:12px;line-height:1.7;overflow-x:auto}.memory-detail .frontmatter{background:var(--bg-tertiary);border-radius:var(--radius-sm);grid-template-columns:auto 1fr;gap:4px 16px;margin-bottom:16px;padding:12px 16px;font-size:13px;display:grid}.frontmatter dt{color:var(--text-tertiary);font-weight:500}.frontmatter dd{color:var(--text-primary)}.health-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:24px;display:grid}.stat-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);text-align:center;transition:border-color var(--transition-normal);padding:20px}.stat-card:hover{border-color:var(--border-hover)}.stat-card .value{letter-spacing:-.02em;font-size:28px;font-weight:700}.stat-card .label{color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;margin-top:4px;font-size:12px}.stat-card.healthy .value{color:var(--success)}.stat-card.warning .value{color:var(--warning)}.stat-card.error .value{color:var(--error)}.health-log{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:20px}.health-log h3{margin-bottom:12px;font-size:14px;font-weight:600}.health-log table{border-collapse:collapse;width:100%;font-size:13px}.health-log th{text-align:left;color:var(--text-tertiary);border-bottom:1px solid var(--border);padding:8px 12px;font-weight:500}.health-log td{border-bottom:1px solid var(--border);color:var(--text-secondary);padding:8px 12px}.sandbox-layout{grid-template-rows:1fr auto 1fr;gap:0;height:calc(100vh - 120px);display:grid}.sandbox-editor{flex-direction:column;display:flex;overflow:hidden}.sandbox-editor-header{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-md) var(--radius-md) 0 0;color:var(--text-tertiary);justify-content:space-between;align-items:center;padding:8px 12px;font-size:12px;display:flex}.sandbox-editor textarea{resize:none;background:var(--bg-secondary);border:1px solid var(--border);border-radius:0 0 var(--radius-md) var(--radius-md);width:100%;color:var(--text-primary);font-family:var(--font-mono);tab-size:2;border-top:none;flex:1;padding:16px;font-size:13px;line-height:1.6}.sandbox-editor textarea:focus{border-color:var(--accent);outline:none}.sandbox-toolbar{justify-content:center;align-items:center;padding:8px;display:flex}.sandbox-output{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-md);font-family:var(--font-mono);color:var(--success);white-space:pre-wrap;padding:16px;font-size:13px;line-height:1.6;overflow-y:auto}.sandbox-output.error{color:var(--error)}.pulse{background:var(--success);border-radius:50%;width:8px;height:8px;animation:2s infinite pulseAnim;display:inline-block}@keyframes pulseAnim{0%,to{box-shadow:0 0 #00cec966}50%{box-shadow:0 0 0 6px #00cec900}}.skeleton{background:linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-hover) 50%, var(--bg-tertiary) 75%);border-radius:var(--radius-sm);background-size:200% 100%;animation:1.5s infinite shimmer}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (width<=768px){.sidebar{position:fixed;top:0;bottom:0;left:0;transform:translate(-100%);box-shadow:0 0 40px #00000080}.sidebar.open{transform:translate(0)}.memory-layout{grid-template-columns:1fr;height:auto}.page{padding:16px}.btn,.nav-link,.category-btn{touch-action:manipulation;min-height:44px}.chat-input-wrapper textarea,.memory-search input,.sandbox-editor textarea{font-size:16px}.chat-input-bar{padding:12px 16px 16px}}.chat-layout{width:100%;height:100%;display:flex;overflow:hidden}.chat-threads-sidebar{background:var(--bg-secondary);border-right:1px solid var(--border);flex-direction:column;flex-shrink:0;width:260px;height:100%;display:flex;overflow:hidden}.sidebar-header{border-bottom:1px solid var(--border);padding:16px}.new-chat-btn{background:var(--accent);color:#fff;border-radius:var(--radius-sm);width:100%;transition:all var(--transition-fast);cursor:pointer;border:none;justify-content:center;align-items:center;gap:8px;padding:10px 16px;font-size:13px;font-weight:500;display:flex}.new-chat-btn:hover{background:var(--accent-hover);box-shadow:0 0 16px var(--accent-glow)}.thread-list{flex-direction:column;flex:1;gap:6px;padding:16px 12px;display:flex;overflow-y:auto}.thread-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);border:1px solid #0000;justify-content:space-between;align-items:center;padding:10px 12px;display:flex;position:relative}.thread-item:hover{background:var(--bg-hover)}.thread-item.active{border-color:var(--border);background:#ffffff0d}.thread-info{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.thread-title{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}.thread-meta{color:var(--text-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}.thread-delete-btn{color:var(--text-tertiary);cursor:pointer;opacity:0;transition:all var(--transition-fast);background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;padding:4px;display:flex}.thread-item:hover .thread-delete-btn{opacity:1}.thread-delete-btn:hover{color:var(--error);background:var(--error-muted)}.chat-suggestions-container{width:100%;max-width:900px;margin-top:32px;animation:.4s ease-out fadeIn}.chat-suggestions-container h3{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);text-align:center;margin-bottom:16px;font-size:12px}.chat-suggestions-deck{grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;width:100%;display:grid}.suggestion-card{-webkit-backdrop-filter:blur(12px);border-radius:var(--radius-md);cursor:pointer;text-align:left;transition:all var(--transition-normal);background:#12121a73;border:1px solid #ffffff0d;gap:14px;padding:16px;display:flex;position:relative;overflow:hidden}.suggestion-card:before{content:"";opacity:.8;width:4px;height:100%;transition:width var(--transition-fast) ease;position:absolute;top:0;left:0}.suggestion-card.card-fact:before{background:var(--success)}.suggestion-card.card-concept:before{background:var(--accent)}.suggestion-card.card-question:before{background:var(--warning)}.suggestion-card:hover{background:#1a1a2699;border-color:#ffffff1f;transform:translateY(-2px);box-shadow:0 8px 24px #0003}.suggestion-card:hover:before{width:6px}.suggestion-card.card-fact:hover{border-color:#00cec940;box-shadow:0 8px 24px #00cec90f}.suggestion-card.card-concept:hover{border-color:#6c5ce740;box-shadow:0 8px 24px #6c5ce70f}.suggestion-card.card-question:hover{border-color:#fdcb6e40;box-shadow:0 8px 24px #fdcb6e0f}.suggestion-card-icon{-webkit-user-select:none;user-select:none;align-items:flex-start;padding-top:2px;font-size:22px;display:flex}.suggestion-card-content{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.suggestion-card-content h4{color:var(--text-primary);font-size:13.5px;font-weight:600;line-height:1.4}.suggestion-card-content p{color:var(--text-secondary);font-size:12.5px;line-height:1.5}.suggestion-card-nodes{flex-wrap:wrap;gap:6px;margin-top:8px;display:flex}.suggestion-node-pill{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);white-space:nowrap;text-overflow:ellipsis;border-radius:4px;max-width:140px;padding:2px 6px;font-size:10px;font-weight:500;overflow:hidden}.grounding-pills{border:1px solid var(--border);border-radius:var(--radius-md) var(--radius-md) 0 0;background:#12121a66;border-bottom:none;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 12px;display:flex}.grounding-pill{background:var(--accent-muted);color:var(--text-primary);transition:all var(--transition-fast);border:1px solid #6c5ce74d;border-radius:99px;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:500;display:inline-flex}.grounding-pill:hover{background:#6c5ce733;border-color:#6c5ce780}.grounding-pill svg{color:var(--accent);flex-shrink:0}.grounding-pill-title{text-overflow:ellipsis;white-space:nowrap;max-width:150px;overflow:hidden}.grounding-pill-remove{color:var(--text-tertiary);cursor:pointer;transition:color var(--transition-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0 2px;font-size:14px;line-height:1;display:inline-flex}.grounding-pill-remove:hover{color:var(--error)}.grounding-count-badge{background:var(--accent);color:#fff;border:1.5px solid var(--bg-secondary);min-width:16px;height:16px;box-shadow:0 0 8px var(--accent-glow);border-radius:50%;justify-content:center;align-items:center;font-size:9px;font-weight:700;animation:.2s cubic-bezier(.175,.885,.32,1.275) popIn;display:flex;position:absolute;top:-4px;right:-4px}.node-selector-popover{-webkit-backdrop-filter:blur(20px);border-radius:var(--radius-lg);z-index:50;background:#12121ad9;border:1px solid #ffffff14;flex-direction:column;width:320px;max-height:380px;animation:.25s cubic-bezier(.16,1,.3,1) slideUp;display:flex;position:absolute;bottom:calc(100% + 12px);left:0;overflow:hidden;box-shadow:0 12px 32px #0006}.node-selector-header{border-bottom:1px solid var(--border);flex-direction:column;gap:8px;padding:14px 16px 10px;display:flex}.node-selector-header h4{color:var(--text-primary);letter-spacing:.02em;font-size:13px;font-weight:600}.node-search-input{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-sm);width:100%;color:var(--text-primary);font-size:12px;font-family:var(--font-sans);transition:all var(--transition-fast);padding:6px 10px}.node-search-input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.node-selector-list{flex-direction:column;flex:1;gap:2px;padding:8px;display:flex;overflow-y:auto}.node-selector-empty{text-align:center;color:var(--text-tertiary);padding:24px;font-size:12px}.node-selector-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);align-items:center;gap:10px;padding:8px 12px;display:flex}.node-selector-item:hover{background:var(--bg-hover)}.node-selector-item.selected{background:var(--accent-muted)}.node-selector-item-checkbox{border:1px solid var(--text-tertiary);width:16px;height:16px;color:var(--accent);transition:all var(--transition-fast);background:var(--bg-tertiary);border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;font-size:11px;font-weight:700;display:flex}.node-selector-item.selected .node-selector-item-checkbox{background:var(--accent);border-color:var(--accent);color:#fff}.node-selector-item-info{flex-direction:column;flex:1;min-width:0;display:flex}.node-selector-item-title{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}.node-selector-item-meta{color:var(--text-tertiary);text-transform:capitalize;font-size:11px}.animate-fade-in{animation:.2s ease-out fadeIn}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes popIn{0%{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}}.graph-3d-wrapper{-webkit-user-select:none;user-select:none;background:radial-gradient(circle,#0a0d16 0%,#020408 100%);width:100%;height:100%;position:relative;overflow:hidden}.node-detail-drawer{-webkit-backdrop-filter:blur(25px);border-radius:var(--radius-lg);z-index:100;width:350px;color:var(--text-primary);background:#0a0f1ebf;border:1px solid #ffffff14;flex-direction:column;display:flex;position:absolute;top:20px;bottom:20px;right:20px;overflow:hidden;box-shadow:0 16px 48px #00000080,inset 0 0 0 1px #ffffff0d}.animate-slide-in{animation:.4s cubic-bezier(.16,1,.3,1) forwards slideInRight}@keyframes slideInRight{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.drawer-header{border-bottom:1px solid #ffffff0f;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.drawer-badge{text-transform:uppercase;letter-spacing:.05em;border-radius:4px;padding:4px 8px;font-size:10px;font-weight:700}.drawer-badge.badge-memory{color:#c084fc;background:#8b5cf626;border:1px solid #8b5cf64d}.drawer-badge.badge-thread{color:#22d3ee;background:#06b6d426;border:1px solid #06b6d44d}.drawer-badge.badge-research{color:#fbbf24;background:#f59e0b26;border:1px solid #f59e0b4d}.drawer-close-btn{color:var(--text-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:4px;font-size:20px;transition:color .15s;display:flex}.drawer-close-btn:hover{color:#fff}.drawer-body{flex-direction:column;flex:1;gap:12px;padding:20px;display:flex;overflow-y:auto}.drawer-title{color:#fff;margin:0;font-size:18px;font-weight:700;line-height:1.35}.drawer-subtitle{color:var(--text-tertiary);margin-top:-4px;font-size:11px}.drawer-status{background:#ffffff08;border:1px solid #ffffff0a;border-radius:6px;align-items:center;gap:8px;width:fit-content;padding:6px 12px;font-size:12px;display:flex}.drawer-status .label{color:var(--text-tertiary)}.drawer-status .value{text-transform:capitalize;font-weight:600}.drawer-status .value.status-completed,.drawer-status .value.status-done,.drawer-status .value.status-active{color:#10b981}.drawer-status .value.status-in-progress{color:#f59e0b}.drawer-status .value.status-pending{color:#3b82f6}.drawer-excerpt{color:var(--text-secondary);white-space:pre-wrap;margin-top:8px;font-size:13.5px;line-height:1.6}.drawer-footer{background:#00000026;border-top:1px solid #ffffff0f;padding:16px 20px}.drawer-action-btn{border-radius:var(--radius-sm);cursor:pointer;width:100%;transition:all var(--transition-fast);border:none;justify-content:center;align-items:center;padding:10px 16px;font-size:13px;font-weight:600;display:flex}.drawer-action-btn.btn-cyan{color:#fff;background:#06b6d4}.drawer-action-btn.btn-cyan:hover{background:#0891b2;box-shadow:0 0 16px #06b6d466}.drawer-action-btn.btn-purple{color:#fff;background:#8b5cf6}.drawer-action-btn.btn-purple:hover{background:#7c3aed;box-shadow:0 0 16px #8b5cf666}@media (width<=768px){.node-detail-drawer{width:auto;height:300px;bottom:20px;left:20px}}.chat-messages:has(.graph-3d-wrapper){padding:0;overflow:hidden}.chat-header{-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);z-index:40;background:#12121aeb;flex-shrink:0;justify-content:space-between;align-items:center;padding:12px 24px;display:flex;position:relative;overflow:visible}.chat-header-title{align-items:center;gap:10px;display:flex}.chat-header-title h2{color:var(--text-primary);letter-spacing:-.01em;font-size:15px;font-weight:600}.chat-header-model-selector{align-items:center;gap:8px;display:flex}.chat-header-model-selector span.selector-label{color:var(--text-tertiary);font-size:12px;font-weight:500}.model-selector-dropdown-wrapper{position:relative}.model-selector-dropdown-btn{border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-secondary);transition:all var(--transition-fast);cursor:pointer;background:#ffffff0a;align-items:center;gap:8px;padding:6px 12px;font-size:13px;font-weight:500;display:flex}.model-selector-dropdown-btn:hover{border-color:var(--border-hover);color:var(--text-primary);background:#ffffff14}.model-selector-dropdown-btn.active{border-color:var(--accent);color:var(--text-primary);background:var(--accent-muted)}.model-selector-menu{-webkit-backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--radius-md);z-index:200;pointer-events:auto;background:#12121afa;flex-direction:column;gap:2px;width:240px;margin-top:6px;padding:6px;animation:.15s ease-out popoverIn;display:flex;position:absolute;top:100%;right:0;box-shadow:0 12px 32px #0000008c}@keyframes popoverIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.model-selector-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);text-align:left;justify-content:space-between;align-items:center;padding:8px 12px;display:flex}.model-selector-item:hover{background:var(--bg-hover)}.model-selector-item.selected{background:var(--accent-muted)}.model-selector-item-info{flex-direction:column;gap:2px;display:flex}.model-selector-item-name{color:var(--text-primary);font-size:13px;font-weight:500}.model-selector-item.selected .model-selector-item-name{color:var(--accent)}.model-selector-item-desc{color:var(--text-tertiary);font-size:11px}.model-selector-item-badge{background:var(--bg-tertiary);color:var(--text-tertiary);text-transform:uppercase;border-radius:99px;padding:2px 6px;font-size:9px;font-weight:600}.model-selector-item.selected .model-selector-item-badge{background:var(--accent-muted);color:var(--accent)}@media (width<=768px){button,a,[role=button]{min-width:44px;min-height:44px}.chat-input-area{bottom:0;bottom:env(safe-area-inset-bottom,0);position:sticky}.app-container{padding-left:env(safe-area-inset-left,0);padding-right:env(safe-area-inset-right,0);padding-bottom:env(safe-area-inset-bottom,0)}}@media (display-mode:standalone){body{padding-top:env(safe-area-inset-top,0)}}.network-page{animation:.35s ease-out page-fade-in}@keyframes page-fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.stat-grid{grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:32px;display:grid}.stat-card{text-align:center;flex-direction:column;align-items:center;padding:24px 16px;display:flex}.stat-value{color:var(--text-primary);font-variant-numeric:tabular-nums;font-size:32px;font-weight:700;line-height:1.2;transition:color .3s}.stat-value.error{color:var(--error)}.stat-value.success{color:var(--success)}.stat-value.accent{color:var(--accent)}.stat-label{color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.06em;margin-top:8px;font-size:12px;font-weight:500}.network-sections{grid-template-columns:1fr 1fr;gap:24px;margin-bottom:32px;display:grid}.network-section-full{margin-bottom:32px}.section-title{color:var(--text-primary);align-items:center;gap:8px;margin-bottom:16px;font-size:15px;font-weight:600;display:flex}.blocklist-items{max-height:240px;margin:0 0 12px;padding:0;list-style:none;overflow-y:auto}.blocklist-item{border-bottom:1px solid var(--border);font-family:var(--font-mono);color:var(--text-primary);transition:background var(--transition-fast);justify-content:space-between;align-items:center;padding:8px 12px;font-size:13px;display:flex}.blocklist-item:hover{background:var(--bg-hover)}.blocklist-item:last-child{border-bottom:none}.blocklist-remove{color:var(--error);opacity:.6;transition:opacity var(--transition-fast), background var(--transition-fast);background:0 0;border-radius:4px;padding:4px 8px;font-size:12px}.blocklist-remove:hover{opacity:1;background:var(--error-muted)}.blocklist-add{gap:8px;display:flex}.blocklist-add .input{flex:1}.limits-form{flex-direction:column;gap:12px;display:flex}.limits-row{justify-content:space-between;align-items:center;gap:12px;display:flex}.limits-label{color:var(--text-secondary);flex-shrink:0;font-size:13px}.limits-input{text-align:right;width:100px}.limits-actions{justify-content:flex-end;margin-top:8px;display:flex}.audit-filters{flex-wrap:wrap;gap:12px;margin-bottom:16px;display:flex}.audit-filters .input,.audit-filters .select{flex:1;min-width:140px}.audit-status-badge{border-radius:3px;align-items:center;padding:2px 6px;font-size:11px;font-weight:600;display:inline-flex}.audit-status-badge.success{background:var(--success-muted);color:var(--success)}.audit-status-badge.error{background:var(--error-muted);color:var(--error)}.audit-status-badge.timeout{background:var(--warning-muted);color:var(--warning)}.toggle-row{align-items:center;gap:12px;margin-bottom:16px;display:flex}.toggle-switch{background:var(--bg-hover);cursor:pointer;width:44px;height:24px;transition:background var(--transition-fast);border:1px solid var(--border);border-radius:12px;flex-shrink:0;position:relative}.toggle-switch.active{background:var(--accent);border-color:var(--accent)}.toggle-switch:after{content:"";width:18px;height:18px;transition:transform var(--transition-fast);background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;box-shadow:0 1px 3px #0003}.toggle-switch.active:after{transform:translate(20px)}.toggle-label{color:var(--text-secondary);font-size:13px}.refresh-controls{align-items:center;gap:8px;display:flex}.refresh-controls .select{width:auto;padding:6px 28px 6px 10px;font-size:12px}.empty-state{text-align:center;color:var(--text-tertiary);padding:32px 16px;font-size:13px;font-style:italic}@media (width<=768px){.stat-grid,.network-sections{grid-template-columns:1fr}}.secrets-page{animation:.35s ease-out secrets-fade-in}@keyframes secrets-fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.secrets-stats{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px;display:grid}.stat-box{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-sm);transition:border-color var(--transition-fast);flex-direction:column;gap:4px;padding:16px 20px;display:flex}.stat-box:hover{border-color:var(--border-hover)}.stat-box .label{text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);font-size:12px}.secrets-grid{grid-template-columns:1.8fr 1.2fr;align-items:start;gap:24px;display:grid}@media (width<=1024px){.secrets-grid{grid-template-columns:1fr}}.secrets-actions{flex-direction:column;gap:24px;display:flex}.field-label{color:var(--text-secondary);margin-bottom:6px;font-size:13px;font-weight:500;display:block}.input{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-sm);width:100%;color:var(--text-primary);transition:all var(--transition-fast);padding:10px 14px;font-size:14px}.input:focus{border-color:var(--border-accent);box-shadow:0 0 0 2px var(--accent-muted);outline:none}.btn-reveal-toggle{cursor:pointer;opacity:.6;transition:opacity var(--transition-fast);background:0 0;border:none;font-size:16px;position:absolute;top:50%;right:12px;transform:translateY(-50%)}.btn-reveal-toggle:hover{opacity:1}.selected-row{background:var(--bg-hover)!important;border-left:3px solid var(--accent)!important}.spinner{border:2px solid #ffffff4d;border-top-color:#fff;border-radius:50%;width:16px;height:16px;animation:.8s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.sync-logs-container::-webkit-scrollbar{width:6px}.sync-logs-container::-webkit-scrollbar-track{background:0 0}.sync-logs-container::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}.sync-logs-container::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}.mesh-page{animation:.35s ease-out mesh-fade-in}@keyframes mesh-fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.latency-sparkline{vertical-align:middle;display:inline-block}.latency-sparkline--empty{color:var(--text-secondary);font-size:12px}.latency-matrix-wrap{overflow-x:auto}.latency-matrix{border-collapse:collapse;width:100%;font-size:13px}.latency-matrix th,.latency-matrix td{border:1px solid var(--border,#7f7f7f40);text-align:center;white-space:nowrap;padding:8px 10px}.latency-matrix th{background:var(--surface-2,#7f7f7f14);font-weight:600}.latency-matrix td.is-self{background:var(--surface-2,#7f7f7f0f);color:var(--text-secondary)}.latency-matrix .from-label{text-align:left;font-weight:500}.latency-matrix .rtt-cell{font-variant-numeric:tabular-nums}.webhooks-page{animation:.35s ease-out page-fade-in}.wizard-modal{width:100%;max-width:600px}.wizard-steps{justify-content:space-between;margin-bottom:24px;display:flex;position:relative}.wizard-steps:before{content:"";z-index:0;background:#ffffff1a;height:2px;position:absolute;top:12px;left:0;right:0}.wizard-step{background:var(--bg-surface);color:var(--text-muted);z-index:1;border:2px solid #ffffff1a;border-radius:50%;justify-content:center;align-items:center;width:28px;height:28px;font-size:14px;font-weight:600;transition:all .3s;display:flex}.wizard-step.active{background:var(--primary);color:#fff;border-color:var(--primary);box-shadow:0 0 12px #6366f180}.wizard-step.completed{background:var(--success);color:#fff;border-color:var(--success)}.json-viewer{color:#a5b4fc;background:#0003;border:1px solid #ffffff0d;border-radius:8px;margin-top:8px;padding:12px;font-family:Fira Code,monospace;font-size:13px;overflow-x:auto}.json-toggle{color:var(--primary);cursor:pointer;background:0 0;border:none;margin-top:4px;padding:0;font-size:12px}.json-toggle:hover{text-decoration:underline}.webhook-stats{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px;margin-bottom:24px;display:grid}.stat-box{text-align:center;background:#ffffff08;border:1px solid #ffffff0d;border-radius:12px;padding:16px}.stat-box .label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;font-size:12px}.stat-box .value{color:var(--text-primary);font-size:24px;font-weight:700}.rotate-btn{color:var(--text-primary);cursor:pointer;background:#ffffff0d;border:1px solid #ffffff1a;border-radius:4px;padding:4px 8px;font-size:12px;transition:all .2s}.rotate-btn:hover{background:#ffffff1a}.notifications-page{animation:.35s ease-out notif-fade-in}@keyframes notif-fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.notif-rules-card{padding:0;overflow:hidden}.notif-rules-actions{gap:6px;display:flex}.priority-badge{text-transform:uppercase;letter-spacing:.04em;border-radius:4px;align-items:center;padding:2px 8px;font-size:11px;font-weight:600;display:inline-flex}.priority-badge.critical{background:var(--error-muted);color:var(--error)}.priority-badge.high{background:var(--warning-muted);color:var(--warning)}.priority-badge.low{background:var(--success-muted);color:var(--success)}.notif-toggle{background:var(--bg-hover);cursor:pointer;width:40px;height:22px;transition:background var(--transition-fast);border:1px solid var(--border);border-radius:11px;flex-shrink:0;position:relative}.notif-toggle.active{background:var(--accent);border-color:var(--accent)}.notif-toggle:after{content:"";width:16px;height:16px;transition:transform var(--transition-fast);background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;box-shadow:0 1px 3px #0003}.notif-toggle.active:after{transform:translate(18px)}.notif-history-header{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.notif-history-header h2{color:var(--text-primary);font-size:15px;font-weight:600}.notif-history-list{flex-direction:column;gap:10px;max-height:420px;display:flex;overflow-y:auto}.notif-history-entry{background:var(--bg-hover);border-radius:var(--radius-sm);transition:background var(--transition-fast);justify-content:space-between;align-items:flex-start;padding:14px 16px;display:flex}.notif-history-entry:hover{background:var(--bg-secondary)}.notif-entry-content{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.notif-entry-title{color:var(--text-primary);font-size:14px;font-weight:500}.notif-entry-message{color:var(--text-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;overflow:hidden}.notif-entry-meta{flex-direction:column;flex-shrink:0;align-items:flex-end;gap:4px;margin-left:16px;display:flex}.notif-entry-time{color:var(--text-tertiary);white-space:nowrap;font-size:12px}.notif-status-badge{border-radius:3px;align-items:center;padding:2px 6px;font-size:11px;font-weight:600;display:inline-flex}.notif-status-badge.delivered{background:var(--success-muted);color:var(--success)}.notif-status-badge.failed{background:var(--error-muted);color:var(--error)}.notif-quiet-hours-row{align-items:center;gap:10px;display:flex}.notif-quiet-hours-label{color:var(--text-secondary);cursor:pointer;font-size:13px}.notif-empty{text-align:center;color:var(--text-tertiary);padding:32px 16px;font-size:13px;font-style:italic}@media (width<=768px){.notif-history-entry{flex-direction:column;gap:8px}.notif-entry-meta{flex-direction:row;align-items:center;margin-left:0}}
@@ -17,8 +17,8 @@
17
17
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18
18
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
19
19
  <title>Total Recall — Portable Memory</title>
20
- <script type="module" crossorigin src="/assets/index-D0XeeXhG.js"></script>
21
- <link rel="stylesheet" crossorigin href="/assets/index-B4RASWbV.css">
20
+ <script type="module" crossorigin src="/assets/index-BwUTxKNm.js"></script>
21
+ <link rel="stylesheet" crossorigin href="/assets/index-C7_Xpk27.css">
22
22
  </head>
23
23
  <body>
24
24
  <div id="root"></div>
@@ -19,7 +19,7 @@ vi.mock('./utils/onboarding', () => ({
19
19
 
20
20
  // Mock the components since App contains a lot of them and we just want to test routing/auth logic
21
21
  vi.mock('./pages/LoginPage', () => ({
22
- default: ({ onAuthenticated }: any) => <div data-testid="login-page"><button onClick={onAuthenticated}>Login</button></div>
22
+ default: ({ onAuthenticated }: { onAuthenticated: () => void }) => <div data-testid="login-page"><button onClick={onAuthenticated}>Login</button></div>
23
23
  }));
24
24
  vi.mock('./pages/ChatPage', () => ({ default: () => <div data-testid="chat-page" /> }));
25
25
  vi.mock('./pages/MemoryPage', () => ({ default: () => <div data-testid="memory-page" /> }));
@@ -34,13 +34,13 @@ export async function apiFetch(url: string, options: RequestInit = {}): Promise<
34
34
  return res
35
35
  }
36
36
 
37
- export async function get<T = any>(path: string): Promise<T> {
37
+ export async function get<T = unknown>(path: string): Promise<T> {
38
38
  const res = await apiFetch(API_BASE + path);
39
39
  if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
40
40
  return res.json();
41
41
  }
42
42
 
43
- export async function post<T = any>(path: string, body: any): Promise<T> {
43
+ export async function post<T = unknown>(path: string, body: unknown): Promise<T> {
44
44
  const res = await apiFetch(API_BASE + path, {
45
45
  method: 'POST',
46
46
  headers: { 'Content-Type': 'application/json' },
@@ -50,7 +50,7 @@ export async function post<T = any>(path: string, body: any): Promise<T> {
50
50
  return res.json();
51
51
  }
52
52
 
53
- export async function del<T = any>(path: string): Promise<T> {
53
+ export async function del<T = unknown>(path: string): Promise<T> {
54
54
  const res = await apiFetch(API_BASE + path, {
55
55
  method: 'DELETE'
56
56
  });
@@ -159,6 +159,53 @@ export async function fetchDeviceIo(): Promise<{
159
159
  return get('/api/mesh/io');
160
160
  }
161
161
 
162
+ export type EnrollmentState =
163
+ | 'enrolled'
164
+ | 'needs_login'
165
+ | 'stopped'
166
+ | 'client_unavailable'
167
+ | 'unknown';
168
+
169
+ export interface EnrollmentStatus {
170
+ state: EnrollmentState;
171
+ enrolled: boolean;
172
+ backend_state: string | null;
173
+ auth_url: string | null;
174
+ ips: string[];
175
+ hostname: string;
176
+ login_server: string | null;
177
+ can_auto_enroll: boolean;
178
+ auto_enroll_blocked_reason: string | null;
179
+ auto_enroll_enabled: boolean;
180
+ client_available: boolean;
181
+ checked_at: string;
182
+ }
183
+
184
+ export interface EnrollResult {
185
+ ok: boolean;
186
+ changed: boolean;
187
+ state: EnrollmentState;
188
+ status: EnrollmentStatus;
189
+ method?: 'preauth-key' | 'interactive' | 'resume';
190
+ auth_url?: string | null;
191
+ reason?: string | null;
192
+ hint?: string;
193
+ }
194
+
195
+ /** Enrollment state of this node on the mesh control server. */
196
+ export async function fetchEnrollmentStatus(): Promise<EnrollmentStatus> {
197
+ return get('/api/mesh/enrollment');
198
+ }
199
+
200
+ /** Enroll this node now (mints a pre-auth key when one can be minted). */
201
+ export async function enrollThisNode(opts?: {
202
+ login_server?: string;
203
+ user?: string;
204
+ force?: boolean;
205
+ }): Promise<EnrollResult> {
206
+ return post('/api/mesh/enroll', opts || {});
207
+ }
208
+
162
209
  /** Discover LAN TR peers and upsert mesh_node entities (config:write). */
163
210
  export async function registerLanMeshNodes(opts?: { probe?: boolean; limit?: number }): Promise<{
164
211
  success: boolean;
@@ -3,14 +3,14 @@ import { networkApi } from './network';
3
3
 
4
4
  describe('networkApi', () => {
5
5
  beforeEach(() => {
6
- globalThis.fetch = vi.fn() as any;
6
+ globalThis.fetch = vi.fn() as unknown as typeof fetch;
7
7
  });
8
8
 
9
9
  it('getStats fetches from /api/network/stats', async () => {
10
- (globalThis.fetch as any).mockResolvedValue({
10
+ vi.mocked(globalThis.fetch).mockResolvedValue({
11
11
  ok: true,
12
12
  json: () => Promise.resolve({ stats: { total: 10 }, audit_count: 5 })
13
- });
13
+ } as unknown as Response);
14
14
 
15
15
  const result = await networkApi.getStats();
16
16
  expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/stats', expect.any(Object));
@@ -18,10 +18,10 @@ describe('networkApi', () => {
18
18
  });
19
19
 
20
20
  it('getPolicy fetches from /api/network/policy', async () => {
21
- (globalThis.fetch as any).mockResolvedValue({
21
+ vi.mocked(globalThis.fetch).mockResolvedValue({
22
22
  ok: true,
23
23
  json: () => Promise.resolve({ id: 'network-policy', blocked_domains: [] })
24
- });
24
+ } as unknown as Response);
25
25
 
26
26
  const result = await networkApi.getPolicy();
27
27
  expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/policy', expect.any(Object));
@@ -29,10 +29,10 @@ describe('networkApi', () => {
29
29
  });
30
30
 
31
31
  it('blockDomain POSTs to /api/network/block', async () => {
32
- (globalThis.fetch as any).mockResolvedValue({
32
+ vi.mocked(globalThis.fetch).mockResolvedValue({
33
33
  ok: true,
34
34
  json: () => Promise.resolve({ success: true })
35
- });
35
+ } as unknown as Response);
36
36
 
37
37
  await networkApi.blockDomain('evil.com');
38
38
  expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/block', expect.objectContaining({
@@ -42,10 +42,10 @@ describe('networkApi', () => {
42
42
  });
43
43
 
44
44
  it('unblockDomain DELETEs to /api/network/block/:domain', async () => {
45
- (globalThis.fetch as any).mockResolvedValue({
45
+ vi.mocked(globalThis.fetch).mockResolvedValue({
46
46
  ok: true,
47
47
  json: () => Promise.resolve({ success: true })
48
- });
48
+ } as unknown as Response);
49
49
 
50
50
  await networkApi.unblockDomain('evil.com');
51
51
  expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/block/evil.com', expect.objectContaining({
@@ -51,7 +51,7 @@ export async function getNetworkPolicy(): Promise<NetworkPolicy> {
51
51
  return get('/api/network/policy');
52
52
  }
53
53
 
54
- export async function updateNetworkPolicy(patch: Partial<NetworkPolicy>): Promise<any> {
54
+ export async function updateNetworkPolicy(patch: Partial<NetworkPolicy>): Promise<unknown> {
55
55
  const res = await apiFetch(API_BASE + '/api/network/policy', {
56
56
  method: 'PUT',
57
57
  headers: { 'Content-Type': 'application/json' },
@@ -61,16 +61,16 @@ export async function updateNetworkPolicy(patch: Partial<NetworkPolicy>): Promis
61
61
  return res.json();
62
62
  }
63
63
 
64
- export async function blockDomain(domain: string): Promise<any> {
64
+ export async function blockDomain(domain: string): Promise<unknown> {
65
65
  return post('/api/network/block', { domain });
66
66
  }
67
67
 
68
- export async function unblockDomain(domain: string): Promise<any> {
68
+ export async function unblockDomain(domain: string): Promise<unknown> {
69
69
  return del(`/api/network/block/${encodeURIComponent(domain)}`);
70
70
  }
71
71
 
72
72
  export async function getAuditLog(params?: { domain?: string; status?: string; since?: string }): Promise<{ audit: AuditLogEntry[] }> {
73
- const query = params ? '?' + new URLSearchParams(params as any).toString() : '';
73
+ const query = params ? '?' + new URLSearchParams(params as Record<string, string>).toString() : '';
74
74
  return get(`/api/network/audit${query}`);
75
75
  }
76
76
 
@@ -18,9 +18,9 @@ export interface NotificationEntry {
18
18
  timestamp: string;
19
19
  }
20
20
 
21
- async function parseJsonOrThrow(res: Response, label: string): Promise<any> {
21
+ async function parseJsonOrThrow(res: Response, label: string): Promise<unknown> {
22
22
  const text = await res.text();
23
- let data: any = null;
23
+ let data: unknown = null;
24
24
  if (text) {
25
25
  try {
26
26
  data = JSON.parse(text);
@@ -29,8 +29,9 @@ async function parseJsonOrThrow(res: Response, label: string): Promise<any> {
29
29
  }
30
30
  }
31
31
  if (!res.ok) {
32
+ const obj = data as Record<string, unknown> | null;
32
33
  const msg =
33
- (data && (data.error || data.message)) ||
34
+ (obj && (obj.error || obj.message)) ||
34
35
  `${label} failed: ${res.status}`;
35
36
  throw new Error(typeof msg === 'string' ? msg : `${label} failed: ${res.status}`);
36
37
  }
@@ -62,7 +63,7 @@ export async function createNotificationRule(
62
63
  headers: { 'Content-Type': 'application/json' },
63
64
  body: JSON.stringify(rule),
64
65
  });
65
- return parseJsonOrThrow(res, 'POST /api/notifications/rules');
66
+ return parseJsonOrThrow(res, 'POST /api/notifications/rules') as Promise<NotificationRule>;
66
67
  }
67
68
 
68
69
  export async function deleteNotificationRule(id: string): Promise<void> {
@@ -17,7 +17,7 @@ export interface WebhookEvent {
17
17
  provider: string;
18
18
  event_type: string;
19
19
  received_at: string;
20
- payload?: any;
20
+ payload?: unknown;
21
21
  delivery_status?: string;
22
22
  parent_event_id?: string;
23
23
  delivery_id?: string | null;
@@ -30,7 +30,7 @@ describe('BrainSelector', () => {
30
30
  },
31
31
  ],
32
32
  }),
33
- } as any);
33
+ } as never);
34
34
 
35
35
  const onBrainChange = vi.fn();
36
36
  render(<BrainSelector activeBrainId="global" onBrainChange={onBrainChange} />);
@@ -51,7 +51,7 @@ describe('BrainSelector', () => {
51
51
  { id: 'project:demo', name: 'demo', layer: 'project', exists: true, node_count: 5 },
52
52
  ],
53
53
  }),
54
- } as any);
54
+ } as never);
55
55
 
56
56
  const onBrainChange = vi.fn();
57
57
  render(<BrainSelector activeBrainId="global" onBrainChange={onBrainChange} />);
@@ -71,7 +71,7 @@ describe('BrainSelector', () => {
71
71
  { id: 'project:demo', name: 'demo', layer: 'project', exists: true, node_count: 5 },
72
72
  ],
73
73
  }),
74
- } as any);
74
+ } as never);
75
75
 
76
76
  const onBrainChange = vi.fn();
77
77
  render(<BrainSelector activeBrainId="global" onBrainChange={onBrainChange} />);
@@ -87,7 +87,7 @@ describe('BrainSelector', () => {
87
87
  ok: false,
88
88
  status: 401,
89
89
  json: async () => ({ error: 'Authentication required' }),
90
- } as any);
90
+ } as never);
91
91
 
92
92
  render(<BrainSelector activeBrainId="global" onBrainChange={vi.fn()} />);
93
93
  await waitFor(() => {
@@ -60,7 +60,7 @@ export default function BrainSelector({ activeBrainId, onBrainChange }: BrainSel
60
60
  }
61
61
 
62
62
  useEffect(() => {
63
- void loadBrains()
63
+ setTimeout(() => { void loadBrains() }, 0)
64
64
  }, [])
65
65
 
66
66
  useEffect(() => {
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi } from 'vitest';
1
+ import { describe, it, expect } from 'vitest';
3
2
  import { render, screen, fireEvent } from '@testing-library/react';
4
3
  import { MemoryRouter } from 'react-router-dom';
5
4
  import ContextualHelp from './ContextualHelp';
@@ -20,42 +19,43 @@ describe('ContextualHelp', () => {
20
19
  expect(btn).toHaveTextContent('?');
21
20
  });
22
21
 
23
- it('opens overlay when help button is clicked', () => {
22
+ it('opens overlay when help button is clicked', async () => {
24
23
  renderWithRouter('/');
25
24
  fireEvent.click(screen.getByTitle('Page Help'));
26
- expect(screen.getByText(/Chat Interface/)).toBeInTheDocument();
25
+ expect(await screen.findByText(/Chat Interface/)).toBeInTheDocument();
27
26
  });
28
27
 
29
- it('shows memory vault help for /memory route', () => {
28
+ it('shows memory vault help for /memory route', async () => {
30
29
  renderWithRouter('/memory');
31
30
  fireEvent.click(screen.getByTitle('Page Help'));
32
- expect(screen.getByText(/Memory Vault/)).toBeInTheDocument();
31
+ expect(await screen.findByText(/Memory Vault/)).toBeInTheDocument();
33
32
  });
34
33
 
35
- it('shows graph help for /graph route', () => {
34
+ it('shows graph help for /graph route', async () => {
36
35
  renderWithRouter('/graph');
37
36
  fireEvent.click(screen.getByTitle('Page Help'));
38
- expect(screen.getByText(/Concept Graph/)).toBeInTheDocument();
37
+ expect(await screen.findByText(/Concept Graph/)).toBeInTheDocument();
39
38
  });
40
39
 
41
- it('shows fallback for unknown routes', () => {
40
+ it('shows fallback for unknown routes', async () => {
42
41
  renderWithRouter('/unknown-route');
43
42
  fireEvent.click(screen.getByTitle('Page Help'));
44
- expect(screen.getByText(/No specific documentation/)).toBeInTheDocument();
43
+ expect(await screen.findByText(/No specific documentation/)).toBeInTheDocument();
45
44
  });
46
45
 
47
- it('closes overlay when close button is clicked', () => {
46
+ it('closes overlay when close button is clicked', async () => {
48
47
  renderWithRouter('/');
49
48
  fireEvent.click(screen.getByTitle('Page Help'));
50
- expect(screen.getByText(/Chat Interface/)).toBeInTheDocument();
49
+ expect(await screen.findByText(/Chat Interface/)).toBeInTheDocument();
51
50
  fireEvent.click(screen.getByText('✕'));
52
51
  expect(screen.queryByText(/Chat Interface/)).not.toBeInTheDocument();
53
52
  });
54
53
 
55
- it('closes overlay when backdrop is clicked', () => {
54
+ it('closes overlay when backdrop is clicked', async () => {
56
55
  renderWithRouter('/');
57
56
  fireEvent.click(screen.getByTitle('Page Help'));
58
- const overlay = screen.getByText(/Chat Interface/).parentElement?.parentElement;
57
+ const text = await screen.findByText(/Chat Interface/);
58
+ const overlay = text.parentElement?.parentElement;
59
59
  if (overlay) fireEvent.click(overlay);
60
60
  expect(screen.queryByText(/Chat Interface/)).not.toBeInTheDocument();
61
61
  });
@@ -23,7 +23,9 @@ export default function ContextualHelp() {
23
23
  useEffect(() => {
24
24
  // Determine content based on current path
25
25
  const path = location.pathname;
26
- setContent(helpContent[path] || `### ${path}\nNo specific documentation is available for this route yet.`);
26
+ setTimeout(() => {
27
+ setContent(helpContent[path] || `### ${path}\nNo specific documentation is available for this route yet.`);
28
+ }, 0);
27
29
  }, [location.pathname]);
28
30
 
29
31
  return (
@@ -28,7 +28,7 @@ describe('DocumentTable', () => {
28
28
  it('renders custom column render correctly', () => {
29
29
  const customCols = [
30
30
  ...columns,
31
- { key: 'action', header: 'Action', render: (row: any) => <button>Edit {row.name}</button> }
31
+ { key: 'action', header: 'Action', render: (row: { name: string }) => <button>Edit {row.name}</button> }
32
32
  ];
33
33
  const data = [{ id: 1, name: 'Doc 1', status: 'Active' }];
34
34
 
@@ -29,7 +29,7 @@ describe('Graph3D', () => {
29
29
  });
30
30
 
31
31
  // Mock requestAnimationFrame
32
- vi.spyOn(window, 'requestAnimationFrame').mockImplementation((_cb) => {
32
+ vi.spyOn(window, 'requestAnimationFrame').mockImplementation(() => {
33
33
  // Don't actually loop, just call once for coverage
34
34
  return 1;
35
35
  });
@@ -66,9 +66,9 @@ describe('Graph3D', () => {
66
66
  render(
67
67
  <Graph3D
68
68
  {...defaultProps}
69
- threads={[{ id: 't1', title: 'Thread 1', turns: 2, lastUpdated: new Date().toISOString() } as any]}
70
- memoryNodes={[{ slug: 'm1', title: 'Memory 1', category: 'facts', content: 'test', confidence: 0.9 } as any]}
71
- researchItems={[{ id: 'r1', topic: 'Research 1', status: 'active', created_at: new Date().toISOString() } as any]}
69
+ threads={[{ id: 't1', title: 'Thread 1', turns: 2, lastUpdated: new Date().toISOString() } as never]}
70
+ memoryNodes={[{ slug: 'm1', title: 'Memory 1', category: 'facts', content: 'test', confidence: 0.9 } as never]}
71
+ researchItems={[{ id: 'r1', topic: 'Research 1', status: 'active', created_at: new Date().toISOString() } as never]}
72
72
  />
73
73
  );
74
74
 
@@ -341,9 +341,9 @@ export default function Graph3D({
341
341
  const isFocus = !hoverNode || srcId === hoverNode?.id || tgtId === hoverNode?.id;
342
342
 
343
343
  const hex = link.colorHex || '#475569';
344
- let r = parseInt(hex.slice(1, 3), 16) || 71;
345
- let g = parseInt(hex.slice(3, 5), 16) || 85;
346
- let b = parseInt(hex.slice(5, 7), 16) || 105;
344
+ const r = parseInt(hex.slice(1, 3), 16) || 71;
345
+ const g = parseInt(hex.slice(3, 5), 16) || 85;
346
+ const b = parseInt(hex.slice(5, 7), 16) || 105;
347
347
 
348
348
  const baseOpacity = link.type === 'implicit' ? 0.2 : 0.6;
349
349
  const opacity = isFocus ? baseOpacity : 0.02;
@@ -42,7 +42,7 @@ describe('MobilePairing', () => {
42
42
  },
43
43
  ],
44
44
  }),
45
- } as any);
45
+ } as never);
46
46
 
47
47
  render(<MobilePairing />);
48
48
  await waitFor(() => {
@@ -74,7 +74,7 @@ export function MobilePairing() {
74
74
  };
75
75
 
76
76
  useEffect(() => {
77
- load();
77
+ setTimeout(() => { void load(); }, 0);
78
78
  }, []);
79
79
 
80
80
  const selected = info?.endpoints?.find((e) => e.url === selectedUrl) || null;
@@ -7,8 +7,8 @@ vi.mock('recharts', () => {
7
7
  const OriginalRecharts = vi.importActual('recharts');
8
8
  return {
9
9
  ...OriginalRecharts,
10
- ResponsiveContainer: ({ children }: any) => <div>{children}</div>,
11
- BarChart: ({ children, data }: any) => (
10
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
11
+ BarChart: ({ children, data }: { children: React.ReactNode, data: unknown }) => (
12
12
  <div data-testid="bar-chart" data-data={JSON.stringify(data)}>
13
13
  {children}
14
14
  </div>
@@ -39,7 +39,7 @@ describe('UsageChart', () => {
39
39
  const { container } = render(<UsageChart {...defaultProps} usageData={{
40
40
  breakdown: { gemini: { cost: 0 }, claude: { cost: 0 }, codex: { cost: 0 } },
41
41
  timeseries: {}
42
- } as any} />);
42
+ } as never} />);
43
43
  expect(container).toBeEmptyDOMElement();
44
44
  });
45
45
 
@@ -57,7 +57,7 @@ describe('UsageChart', () => {
57
57
  }
58
58
  };
59
59
 
60
- render(<UsageChart {...defaultProps} usageData={usageData as any} />);
60
+ render(<UsageChart {...defaultProps} usageData={usageData as never} />);
61
61
 
62
62
  expect(screen.getByText('API Costs Over Time')).toBeInTheDocument();
63
63
 
@@ -63,7 +63,7 @@ export const UsageChart: React.FC<UsageChartProps> = ({ usageData }) => {
63
63
  <Tooltip
64
64
  contentStyle={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 12 }}
65
65
  itemStyle={{ fontSize: 12 }}
66
- formatter={(value: any) => [`$${Number(value).toFixed(4)}`, undefined]}
66
+ formatter={(value: unknown) => [`$${Number(value).toFixed(4)}`, undefined]}
67
67
  />
68
68
  <Legend wrapperStyle={{ fontSize: 11 }} />
69
69
  <Bar isAnimationActive={false} dataKey="gemini" name="Gemini" stackId="a" fill="#10b981" radius={[0,0,0,0]} />
@@ -1,16 +1,16 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
3
  import { VoiceInput } from './VoiceInput';
4
4
 
5
5
  describe('VoiceInput', () => {
6
- it('renders disabled button if SpeechRecognition is not supported', () => {
6
+ it('renders disabled button if SpeechRecognition is not supported', async () => {
7
7
  // Delete any mock to simulate unsupported browser
8
- delete (window as any).SpeechRecognition;
9
- delete (window as any).webkitSpeechRecognition;
8
+ delete (window as typeof window & { SpeechRecognition?: unknown }).SpeechRecognition;
9
+ delete (window as typeof window & { webkitSpeechRecognition?: unknown }).webkitSpeechRecognition;
10
10
 
11
11
  render(<VoiceInput onTranscript={() => {}} />);
12
12
  const button = screen.getByRole('button');
13
- expect(button).toBeDisabled();
13
+ await waitFor(() => expect(button).toBeDisabled());
14
14
  expect(button.title).toBe('Voice input not supported');
15
15
  });
16
16
 
@@ -25,7 +25,7 @@ describe('VoiceInput', () => {
25
25
  stop = mockStop;
26
26
  }
27
27
 
28
- (window as any).SpeechRecognition = MockSpeechRecognition;
28
+ (window as typeof window & { SpeechRecognition?: unknown }).SpeechRecognition = MockSpeechRecognition;
29
29
 
30
30
  render(<VoiceInput onTranscript={() => {}} />);
31
31
  const button = screen.getByRole('button');