nexo-brain 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
@@ -1,629 +1,434 @@
1
- <!DOCTYPE html>
2
- <html lang="en" class="h-full">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>NEXO Brain Dashboard</title>
7
- <link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <script>
10
- tailwind.config = {
11
- theme: {
12
- extend: {
13
- fontFamily: {
14
- display: ['Space Grotesk', 'system-ui', 'sans-serif'],
15
- mono: ['JetBrains Mono', 'monospace'],
16
- },
17
- }
18
- }
19
- }
20
- </script>
21
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
22
- </head>
23
- <body class="h-full bg-gray-950 text-slate-200 antialiased">
24
-
25
- <!-- Sidebar: 56px icon-only, fixed left -->
26
- <aside class="fixed left-0 top-0 bottom-0 w-14 bg-slate-900 border-r border-slate-800 flex flex-col items-center py-4 z-50">
27
- <a href="/" class="mb-6">
28
- <img src="/static/nexo-logo.png" alt="NEXO" class="w-8 h-8">
29
- </a>
30
- <nav class="flex flex-col items-center gap-1 flex-1">
31
- <!-- Dashboard (active) -->
32
- <a href="/" class="w-10 h-10 rounded-lg flex items-center justify-center bg-violet-500/10 text-violet-400 hover:bg-violet-500/20 transition-colors" title="Dashboard">
33
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
34
- </a>
35
- <!-- Operations -->
36
- <a href="/ops" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Operations">
37
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
38
- </a>
39
- <!-- Calendar -->
40
- <a href="/calendar" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Calendar">
41
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
42
- </a>
43
- <!-- Inbox -->
44
- <a href="/inbox" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors relative" title="Inbox">
45
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
46
- <span id="inbox-badge" class="hidden absolute -top-0.5 -right-0.5 w-4 h-4 bg-pink-500 rounded-full text-xs font-medium items-center justify-center text-white">0</span>
47
- </a>
48
-
49
- <div class="w-6 border-t border-slate-700 my-2"></div>
50
-
51
- <!-- Memory -->
52
- <a href="/memory" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Memory">
53
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
54
- </a>
55
- <!-- Graph -->
56
- <a href="/graph" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Graph">
57
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
58
- </a>
59
- <!-- Sessions -->
60
- <a href="/sessions" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Sessions">
61
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
62
- </a>
63
- <!-- Somatic -->
64
- <a href="/somatic" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Somatic">
65
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
66
- </a>
67
- <!-- Adaptive -->
68
- <a href="/adaptive" class="w-10 h-10 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors" title="Adaptive">
69
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/></svg>
70
- </a>
71
- </nav>
72
-
73
- <!-- Trust score footer -->
74
- <div class="mt-auto pt-4 border-t border-slate-800 w-full flex flex-col items-center" id="sidebar-trust">
75
- <div class="text-xs text-slate-400 uppercase tracking-wider">Trust</div>
76
- <div class="text-sm font-mono font-medium text-violet-400" id="sidebar-trust-value">--</div>
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Cognitive Pulse{% endblock %}
4
+ {% block page_title %}Cognitive Pulse{% endblock %}
5
+
6
+ {% block header_actions %}
7
+ <button onclick="openQuickCreate('reminder')" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors">+ Reminder</button>
8
+ <button onclick="openQuickCreate('followup')" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors">+ Followup</button>
9
+ <button onclick="openQuickCreate('note')" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors">+ Note</button>
10
+ {% endblock %}
11
+
12
+ {% block content %}
13
+ <div class="space-y-4">
14
+ <!-- Row 1: 4 stat cards -->
15
+ <div class="grid grid-cols-4 gap-4">
16
+ <!-- Trust Score -->
17
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
18
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Trust Score</div>
19
+ <div class="flex items-center gap-4">
20
+ <div class="relative">
21
+ <svg id="trust-gauge" viewBox="0 0 80 80" class="w-16 h-16">
22
+ <circle cx="40" cy="40" r="32" fill="none" stroke="rgba(51,65,85,0.4)" stroke-width="6"/>
23
+ <circle id="trust-arc" cx="40" cy="40" r="32" fill="none" stroke="#7C3AED" stroke-width="6"
24
+ stroke-dasharray="0 201" stroke-dashoffset="50.3" stroke-linecap="round"
25
+ style="transition: stroke-dasharray 1s ease-out, stroke 0.5s;"/>
26
+ <text id="trust-center" x="40" y="44" text-anchor="middle" fill="#7C3AED" class="font-mono font-bold" style="font-size:16px">--</text>
27
+ </svg>
28
+ </div>
29
+ <div>
30
+ <div class="text-xs text-slate-500" id="trust-label">loading...</div>
31
+ <div class="mt-1 h-1 w-20 bg-slate-800 rounded-full overflow-hidden">
32
+ <div id="trust-bar" class="h-full bg-gradient-to-r from-violet-500 to-pink-500 rounded-full transition-all duration-700" style="width:0%"></div>
33
+ </div>
34
+ </div>
35
+ </div>
77
36
  </div>
78
- </aside>
79
37
 
80
- <!-- Main content -->
81
- <main class="ml-14 min-h-screen bg-gray-950 text-slate-200">
82
-
83
- <!-- Top bar -->
84
- <header class="h-12 border-b border-slate-800/50 flex items-center justify-between px-6 sticky top-0 bg-gray-950/95 backdrop-blur z-40">
85
- <h1 class="text-sm font-display font-semibold text-slate-100">Operations Center</h1>
38
+ <!-- Active Sessions -->
39
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
40
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Sessions</div>
86
41
  <div class="flex items-center gap-2">
87
- <button onclick="openQuickCreate('reminder')" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors">+ Reminder</button>
88
- <button onclick="openQuickCreate('followup')" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors">+ Followup</button>
89
- <button onclick="openQuickCreate('note')" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors">+ Note</button>
90
- <div class="w-px h-4 bg-slate-700 mx-1"></div>
91
- <span class="text-xs text-slate-500 font-mono tabular-nums" id="current-time">--:--:--</span>
42
+ <span class="relative flex h-2.5 w-2.5">
43
+ <span id="session-pulse" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
44
+ <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
45
+ </span>
46
+ <span class="text-2xl font-mono font-semibold text-slate-200" id="session-count">0</span>
92
47
  </div>
93
- </header>
48
+ <div class="mt-2 text-xs text-slate-500" id="session-label">active terminals</div>
49
+ <div class="mt-1 text-xs text-slate-600 font-mono" id="session-detail">--</div>
50
+ </div>
94
51
 
95
- <!-- Dashboard content -->
96
- <div class="p-5 space-y-4">
52
+ <!-- Overdue Items -->
53
+ <a href="/ops" class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card block hover:border-slate-700/50 transition-colors">
54
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Overdue</div>
55
+ <div class="flex items-end gap-2">
56
+ <span class="text-2xl font-mono font-semibold" id="overdue-count">0</span>
57
+ </div>
58
+ <div class="mt-2 text-xs text-slate-500" id="overdue-detail">reminders &amp; followups</div>
59
+ <div class="mt-1 text-xs text-violet-500 font-medium">view in ops &rarr;</div>
60
+ </a>
97
61
 
98
- <!-- Row 1: 4 stat cards -->
99
- <div class="grid grid-cols-4 gap-4">
62
+ <!-- Watchdog -->
63
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
64
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Watchdog</div>
65
+ <div class="flex items-center gap-2 mb-3">
66
+ <span id="watchdog-badge" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium bg-slate-700 text-slate-300">--</span>
67
+ </div>
68
+ <div id="watchdog-services" class="space-y-1.5">
69
+ <div class="text-xs text-slate-500">loading...</div>
70
+ </div>
71
+ </div>
72
+ </div>
100
73
 
101
- <!-- Trust Score -->
102
- <div class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4">
103
- <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Trust Score</div>
104
- <div class="flex items-end gap-3">
105
- <span class="text-2xl font-mono font-semibold text-violet-400" id="trust-value">--</span>
106
- <span class="text-xs text-slate-500 mb-0.5">/ 100</span>
107
- </div>
108
- <div class="mt-3 h-1.5 bg-slate-800 rounded-full overflow-hidden">
109
- <div id="trust-bar" class="h-full bg-gradient-to-r from-violet-500 to-pink-500 rounded-full transition-all duration-700" style="width:0%"></div>
110
- </div>
111
- <div class="mt-2 flex items-center gap-1.5">
112
- <span class="text-xs text-slate-500" id="trust-label">loading...</span>
113
- </div>
114
- </div>
74
+ <!-- Row 2: 3 detail cards -->
75
+ <div class="grid grid-cols-3 gap-4">
76
+ <!-- Today's Agenda -->
77
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
78
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Today's Agenda</div>
79
+ <ul id="agenda-list" class="space-y-1.5">
80
+ <li class="text-xs text-slate-600 py-1">No items due today</li>
81
+ </ul>
82
+ </div>
115
83
 
116
- <!-- Active Sessions -->
117
- <div class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4">
118
- <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Sessions</div>
119
- <div class="flex items-center gap-2">
120
- <span class="relative flex h-2 w-2">
121
- <span id="session-pulse" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
122
- <span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
123
- </span>
124
- <span class="text-2xl font-mono font-semibold text-slate-200" id="session-count">0</span>
84
+ <!-- Cognitive Memory -->
85
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
86
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Cognitive Memory</div>
87
+ <div class="space-y-3">
88
+ <div>
89
+ <div class="flex items-center justify-between mb-1">
90
+ <span class="text-xs text-slate-400 uppercase tracking-wide">STM</span>
91
+ <span class="text-xs font-mono text-slate-300" id="cog-stm">--</span>
125
92
  </div>
126
- <div class="mt-2 text-xs text-slate-500" id="session-label">active terminals</div>
127
- <div class="mt-2 text-xs text-slate-500 font-mono" id="session-detail">--</div>
128
- </div>
129
-
130
- <!-- Overdue Items -->
131
- <a href="/ops" class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4 hover:border-slate-700 transition-colors block">
132
- <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Overdue</div>
133
- <div class="flex items-end gap-2">
134
- <span class="text-2xl font-mono font-semibold" id="overdue-count">0</span>
93
+ <div class="h-1 bg-slate-800 rounded-full overflow-hidden">
94
+ <div id="cog-stm-bar" class="h-full bg-violet-500/60 rounded-full transition-all duration-500" style="width:0%"></div>
135
95
  </div>
136
- <div class="mt-2 text-xs text-slate-500" id="overdue-detail">reminders &amp; followups</div>
137
- <div class="mt-2 text-xs text-violet-500 font-medium">view in ops →</div>
138
- </a>
139
-
140
- <!-- Watchdog -->
141
- <div class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4">
142
- <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Watchdog</div>
143
- <div class="flex items-center gap-2 mb-3">
144
- <span id="watchdog-badge" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium bg-slate-700 text-slate-300">--</span>
96
+ </div>
97
+ <div>
98
+ <div class="flex items-center justify-between mb-1">
99
+ <span class="text-xs text-slate-400 uppercase tracking-wide">LTM</span>
100
+ <span class="text-xs font-mono text-slate-300" id="cog-ltm">--</span>
145
101
  </div>
146
- <div id="watchdog-services" class="space-y-1.5">
147
- <div class="text-xs text-slate-500">loading...</div>
102
+ <div class="h-1 bg-slate-800 rounded-full overflow-hidden">
103
+ <div id="cog-ltm-bar" class="h-full bg-pink-500/60 rounded-full transition-all duration-500" style="width:0%"></div>
148
104
  </div>
149
105
  </div>
150
- </div>
151
-
152
- <!-- Row 2: 3 detail cards -->
153
- <div class="grid grid-cols-3 gap-4">
154
-
155
- <!-- Today's Agenda -->
156
- <div class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4">
157
- <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Today's Agenda</div>
158
- <ul id="agenda-list" class="space-y-1.5">
159
- <li class="text-xs text-slate-600 py-1">No items due today</li>
160
- </ul>
106
+ <div class="flex items-center justify-between pt-1 border-t border-slate-800">
107
+ <span class="text-xs text-slate-500">Avg strength</span>
108
+ <span class="text-xs font-mono text-slate-400" id="cog-strength">--</span>
161
109
  </div>
162
-
163
- <!-- Cognitive Memory -->
164
- <div class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4">
165
- <div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-3">Cognitive Memory</div>
166
- <div class="space-y-3">
167
- <!-- STM -->
110
+ <div class="pt-1 border-t border-slate-800">
111
+ <div class="text-xs text-slate-400 uppercase tracking-wide mb-2">Knowledge Graph</div>
112
+ <div class="grid grid-cols-3 gap-2 text-center">
168
113
  <div>
169
- <div class="flex items-center justify-between mb-1">
170
- <span class="text-xs text-slate-400 uppercase tracking-wide">STM</span>
171
- <span class="text-xs font-mono text-slate-300" id="cog-stm">--</span>
172
- </div>
173
- <div class="h-1 bg-slate-800 rounded-full overflow-hidden">
174
- <div id="cog-stm-bar" class="h-full bg-violet-500/60 rounded-full transition-all duration-500" style="width:0%"></div>
175
- </div>
114
+ <div class="text-sm font-mono font-medium text-slate-200" id="kg-nodes">--</div>
115
+ <div class="text-xs text-slate-500 mt-0.5">nodes</div>
176
116
  </div>
177
- <!-- LTM -->
178
117
  <div>
179
- <div class="flex items-center justify-between mb-1">
180
- <span class="text-xs text-slate-400 uppercase tracking-wide">LTM</span>
181
- <span class="text-xs font-mono text-slate-300" id="cog-ltm">--</span>
182
- </div>
183
- <div class="h-1 bg-slate-800 rounded-full overflow-hidden">
184
- <div id="cog-ltm-bar" class="h-full bg-pink-500/60 rounded-full transition-all duration-500" style="width:0%"></div>
185
- </div>
118
+ <div class="text-sm font-mono font-medium text-slate-200" id="kg-edges">--</div>
119
+ <div class="text-xs text-slate-500 mt-0.5">edges</div>
186
120
  </div>
187
- <!-- Avg Strength -->
188
- <div class="flex items-center justify-between pt-1 border-t border-slate-800">
189
- <span class="text-xs text-slate-500">Avg strength</span>
190
- <span class="text-xs font-mono text-slate-400" id="cog-strength">--</span>
191
- </div>
192
- <!-- KG divider -->
193
- <div class="pt-1 border-t border-slate-800">
194
- <div class="text-xs text-slate-400 uppercase tracking-wide mb-2">Knowledge Graph</div>
195
- <div class="grid grid-cols-3 gap-2 text-center">
196
- <div>
197
- <div class="text-sm font-mono font-medium text-slate-200" id="kg-nodes">--</div>
198
- <div class="text-xs text-slate-500 mt-0.5">nodes</div>
199
- </div>
200
- <div>
201
- <div class="text-sm font-mono font-medium text-slate-200" id="kg-edges">--</div>
202
- <div class="text-xs text-slate-500 mt-0.5">edges</div>
203
- </div>
204
- <div>
205
- <div class="text-sm font-mono font-medium text-slate-200" id="kg-historical">--</div>
206
- <div class="text-xs text-slate-500 mt-0.5">hist.</div>
207
- </div>
208
- </div>
121
+ <div>
122
+ <div class="text-sm font-mono font-medium text-slate-200" id="kg-historical">--</div>
123
+ <div class="text-xs text-slate-500 mt-0.5">hist.</div>
209
124
  </div>
210
125
  </div>
211
126
  </div>
212
-
213
- <!-- Recent Sessions -->
214
- <div class="bg-slate-900/50 border border-slate-800/50 rounded-lg p-4">
215
- <div class="flex items-center justify-between mb-3">
216
- <div class="text-xs uppercase tracking-wider text-slate-500 font-medium">Recent Sessions</div>
217
- <a href="/sessions" class="text-xs text-violet-400 hover:text-violet-300 transition-colors">view all →</a>
218
- </div>
219
- <div id="recent-sessions" class="space-y-3">
220
- <div class="text-xs text-slate-600">loading...</div>
221
- </div>
222
- </div>
223
127
  </div>
224
-
225
128
  </div>
226
- </main>
227
-
228
- <!-- Quick Create Modal -->
229
- <div id="modal-overlay" class="hidden fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center" onclick="if(event.target===this)closeModal()">
230
- <div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md shadow-2xl">
231
- <div class="flex items-center justify-between mb-4">
232
- <h2 id="modal-title" class="text-sm font-display font-semibold text-slate-100">New Item</h2>
233
- <button onclick="closeModal()" class="text-slate-400 hover:text-white text-lg leading-none">&times;</button>
129
+
130
+ <!-- Recent Sessions -->
131
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
132
+ <div class="flex items-center justify-between mb-3">
133
+ <div class="text-xs uppercase tracking-wider text-slate-400 font-medium">Recent Sessions</div>
134
+ <a href="/sessions" class="text-xs text-violet-400 hover:text-violet-300 transition-colors">view all &rarr;</a>
135
+ </div>
136
+ <div id="recent-sessions" class="space-y-3">
137
+ <div class="text-xs text-slate-600">loading...</div>
234
138
  </div>
235
- <form id="modal-form" onsubmit="submitQuickCreate(event)" class="space-y-3">
236
- <div>
237
- <label class="block text-xs text-slate-400 mb-1">Description</label>
238
- <textarea name="description" rows="3" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500 resize-none placeholder-slate-600"></textarea>
239
- </div>
240
- <div>
241
- <label class="block text-xs text-slate-400 mb-1">Date <span class="text-slate-600">(optional)</span></label>
242
- <input type="date" name="date" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
243
- </div>
244
- <div id="category-group" class="hidden">
245
- <label class="block text-xs text-slate-400 mb-1">Category</label>
246
- <select name="category" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
247
- <option value="general">General</option>
248
- <option value="ideas">Ideas</option>
249
- <option value="my-project">My Project</option>
250
- <option value="project-a">Project A</option>
251
- <option value="server">Server</option>
252
- </select>
253
- </div>
254
- <div id="note-direction-group" class="hidden">
255
- <label class="block text-xs text-slate-400 mb-1">Direction</label>
256
- <select name="direction" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
257
- <option value="to_nexo">To NEXO</option>
258
- <option value="to_user">To User</option>
259
- </select>
260
- </div>
261
- <input type="hidden" name="type" id="modal-type">
262
- <div class="flex justify-end gap-2 pt-2">
263
- <button type="button" onclick="closeModal()" class="px-3 py-1.5 text-xs rounded-lg text-slate-400 hover:text-slate-200 transition-colors">Cancel</button>
264
- <button type="submit" class="px-3 py-1.5 text-xs rounded-lg bg-violet-600 text-white hover:bg-violet-500 transition-colors font-medium">Create</button>
265
- </div>
266
- </form>
267
139
  </div>
268
140
  </div>
269
-
270
- <!-- Toast -->
271
- <div id="toast" class="hidden fixed bottom-4 right-4 z-50 bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-sm text-slate-200 shadow-lg flex items-center gap-2">
272
- <span id="toast-msg"></span>
141
+ </div>
142
+
143
+ <!-- Quick Create Modal -->
144
+ <div id="modal-overlay" class="hidden fixed inset-0 bg-black/60 backdrop-blur-sm z-[60] flex items-center justify-center" onclick="if(event.target===this)closeModal()">
145
+ <div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md shadow-2xl animate-slide-in">
146
+ <div class="flex items-center justify-between mb-4">
147
+ <h2 id="modal-title" class="text-sm font-display font-semibold text-slate-100">New Item</h2>
148
+ <button onclick="closeModal()" class="text-slate-400 hover:text-white text-lg leading-none">&times;</button>
149
+ </div>
150
+ <form id="modal-form" onsubmit="submitQuickCreate(event)" class="space-y-3">
151
+ <div>
152
+ <label class="block text-xs text-slate-400 mb-1">Description</label>
153
+ <textarea name="description" rows="3" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500 resize-none placeholder-slate-600"></textarea>
154
+ </div>
155
+ <div>
156
+ <label class="block text-xs text-slate-400 mb-1">Date <span class="text-slate-600">(optional)</span></label>
157
+ <input type="date" name="date" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
158
+ </div>
159
+ <div id="category-group" class="hidden">
160
+ <label class="block text-xs text-slate-400 mb-1">Category</label>
161
+ <select name="category" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
162
+ <option value="general">General</option>
163
+ <option value="ideas">Ideas</option>
164
+ <option value="my-project">My Project</option>
165
+ <option value="project-a">Project A</option>
166
+ <option value="server">Server</option>
167
+ </select>
168
+ </div>
169
+ <div id="note-direction-group" class="hidden">
170
+ <label class="block text-xs text-slate-400 mb-1">Direction</label>
171
+ <select name="direction" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
172
+ <option value="to_nexo">To NEXO</option>
173
+ <option value="to_user">To User</option>
174
+ </select>
175
+ </div>
176
+ <input type="hidden" name="type" id="modal-type">
177
+ <div class="flex justify-end gap-2 pt-2">
178
+ <button type="button" onclick="closeModal()" class="px-3 py-1.5 text-xs rounded-lg text-slate-400 hover:text-slate-200 transition-colors">Cancel</button>
179
+ <button type="submit" class="px-3 py-1.5 text-xs rounded-lg bg-violet-600 text-white hover:bg-violet-500 transition-colors font-medium">Create</button>
180
+ </div>
181
+ </form>
273
182
  </div>
183
+ </div>
184
+ {% endblock %}
185
+
186
+ {% block scripts %}
187
+ <script>
188
+ function getToday() { return new Date().toISOString().split('T')[0]; }
189
+
190
+ // -----------------------------------------------------------------------
191
+ // Modal
192
+ // -----------------------------------------------------------------------
193
+ function openQuickCreate(type) {
194
+ const overlay = document.getElementById('modal-overlay');
195
+ const title = document.getElementById('modal-title');
196
+ const typeInput = document.getElementById('modal-type');
197
+ document.getElementById('modal-form').reset();
198
+
199
+ typeInput.value = type;
200
+ document.getElementById('category-group').classList.toggle('hidden', type !== 'reminder');
201
+ document.getElementById('note-direction-group').classList.toggle('hidden', type !== 'note');
202
+
203
+ if (type === 'reminder') title.textContent = 'New Reminder';
204
+ else if (type === 'followup') title.textContent = 'New Followup';
205
+ else title.textContent = 'New Note';
206
+
207
+ overlay.classList.remove('hidden');
208
+ }
209
+
210
+ function closeModal() {
211
+ document.getElementById('modal-overlay').classList.add('hidden');
212
+ }
213
+
214
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
215
+
216
+ async function submitQuickCreate(e) {
217
+ e.preventDefault();
218
+ const fd = new FormData(document.getElementById('modal-form'));
219
+ const type = fd.get('type');
220
+ let url, body;
221
+
222
+ if (type === 'reminder') {
223
+ url = '/api/reminders';
224
+ body = { description: fd.get('description'), date: fd.get('date') || null, category: fd.get('category') || 'general' };
225
+ } else if (type === 'followup') {
226
+ url = '/api/followups';
227
+ body = { description: fd.get('description'), date: fd.get('date') || null };
228
+ } else {
229
+ url = '/api/inbox';
230
+ body = { direction: fd.get('direction') || 'to_nexo', content: fd.get('description') };
231
+ }
274
232
 
275
- <script>
276
- // -----------------------------------------------------------------------
277
- // Utilities
278
- // -----------------------------------------------------------------------
279
- function escapeHtml(str) {
280
- const div = document.createElement('div');
281
- div.textContent = str || '';
282
- return div.innerHTML;
233
+ try {
234
+ const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
235
+ const data = await res.json();
236
+ if (data.success) { showToast('Created successfully'); closeModal(); loadDashboardData(); }
237
+ else showToast(data.error || data.detail || 'Create failed');
238
+ } catch (err) { showToast('Error: ' + err.message); }
239
+ }
240
+
241
+ // -----------------------------------------------------------------------
242
+ // Dashboard data
243
+ // -----------------------------------------------------------------------
244
+ async function loadDashboardData() {
245
+ const today = getToday();
246
+
247
+ const [trustData, statsData, remindersData, followupsData, sessionsData, watchdogData, inboxData] =
248
+ await Promise.all([
249
+ fetchJSON('/api/trust'),
250
+ fetchJSON('/api/stats'),
251
+ fetchJSON('/api/reminders'),
252
+ fetchJSON('/api/followups'),
253
+ fetchJSON('/api/sessions?limit=3'),
254
+ fetchJSON('/api/watchdog'),
255
+ fetchJSON('/api/inbox/unread'),
256
+ ]);
257
+
258
+ // --- Trust Score (animated gauge) ---
259
+ if (trustData) {
260
+ const score = trustData.current_score ?? 0;
261
+ const pct = Math.max(0, Math.min(100, score));
262
+ const circumference = 2 * Math.PI * 32;
263
+ const arc = (pct / 100) * circumference;
264
+
265
+ const trustArc = document.getElementById('trust-arc');
266
+ const trustCenter = document.getElementById('trust-center');
267
+ trustArc.setAttribute('stroke-dasharray', `${arc} ${circumference - arc}`);
268
+ trustCenter.textContent = Math.round(score);
269
+
270
+ // Color by score
271
+ const color = pct >= 75 ? '#7C3AED' : pct >= 50 ? '#F59E0B' : '#EF4444';
272
+ trustArc.setAttribute('stroke', color);
273
+ trustCenter.setAttribute('fill', color);
274
+
275
+ document.getElementById('trust-bar').style.width = pct + '%';
276
+ document.getElementById('sidebar-trust-value').textContent = score.toFixed(1);
277
+
278
+ const label = pct >= 75 ? 'high alignment' : pct >= 50 ? 'moderate' : pct >= 25 ? 'low -- more caution' : 'critical';
279
+ document.getElementById('trust-label').textContent = label;
283
280
  }
284
281
 
285
- function showToast(msg, duration = 2500) {
286
- const t = document.getElementById('toast');
287
- document.getElementById('toast-msg').textContent = msg;
288
- t.classList.remove('hidden');
289
- t.classList.add('flex');
290
- clearTimeout(t._timer);
291
- t._timer = setTimeout(() => {
292
- t.classList.add('hidden');
293
- t.classList.remove('flex');
294
- }, duration);
282
+ // --- Active Sessions ---
283
+ if (sessionsData && sessionsData.sessions) {
284
+ const cutoff = Date.now() - 15 * 60 * 1000;
285
+ const active = sessionsData.sessions.filter(s => {
286
+ const ts = new Date(s.last_heartbeat || s.created_at || 0).getTime();
287
+ return ts > cutoff;
288
+ });
289
+ document.getElementById('session-count').textContent = active.length;
290
+ document.getElementById('session-label').textContent = active.length === 1 ? 'active terminal' : 'active terminals';
291
+ if (active.length > 0) {
292
+ const names = active.slice(0, 2).map(s => s.session_id ? s.session_id.substring(0, 8) : '??').join(', ');
293
+ document.getElementById('session-detail').textContent = names + (active.length > 2 ? ' +' + (active.length - 2) : '');
294
+ } else {
295
+ document.getElementById('session-detail').textContent = 'none active';
296
+ document.getElementById('session-pulse').classList.remove('animate-ping');
297
+ }
295
298
  }
296
299
 
297
- function relativeDate(dateStr) {
298
- if (!dateStr) return 'no date';
299
- const d = new Date(dateStr + 'T00:00:00');
300
- const now = new Date();
301
- now.setHours(0, 0, 0, 0);
302
- const diff = Math.round((d - now) / 86400000);
303
- if (diff < -1) return Math.abs(diff) + 'd overdue';
304
- if (diff === -1) return 'yesterday';
305
- if (diff === 0) return 'today';
306
- if (diff === 1) return 'tomorrow';
307
- return d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
300
+ // --- Overdue Items ---
301
+ if (remindersData || followupsData) {
302
+ const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED', 'blocked', 'waiting'];
303
+ const reminders = (remindersData?.reminders || []).filter(r =>
304
+ !excludeStatus.includes(r.status) && r.date && r.date <= today);
305
+ const followups = (followupsData?.followups || []).filter(f =>
306
+ !excludeStatus.includes(f.status) && f.date && f.date <= today);
307
+ const total = reminders.length + followups.length;
308
+ const el = document.getElementById('overdue-count');
309
+ el.textContent = total;
310
+ el.className = 'text-2xl font-mono font-semibold ' + (total > 0 ? 'text-red-400' : 'text-slate-200');
311
+ document.getElementById('overdue-detail').textContent = reminders.length + ' reminders, ' + followups.length + ' followups';
308
312
  }
309
313
 
310
- async function fetchJSON(url) {
311
- try {
312
- const res = await fetch(url);
313
- if (!res.ok) {
314
- let detail = `HTTP ${res.status}`;
315
- try { const b = await res.json(); detail = b.error || b.detail || detail; } catch {}
316
- throw new Error(detail);
314
+ // --- Watchdog ---
315
+ const watchdogBadge = document.getElementById('watchdog-badge');
316
+ const watchdogServices = document.getElementById('watchdog-services');
317
+ if (watchdogData && !watchdogData.error) {
318
+ const overall = watchdogData.summary?.overall || 'UNKNOWN';
319
+ const isPass = overall === 'PASS' || overall === 'ok';
320
+ watchdogBadge.textContent = overall;
321
+ watchdogBadge.className = 'inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium ' +
322
+ (isPass ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400');
323
+
324
+ const services = watchdogData.services || watchdogData.checks || [];
325
+ if (Array.isArray(services) && services.length > 0) {
326
+ watchdogServices.innerHTML = services.map(svc => {
327
+ const ok = svc.status === 'ok' || svc.status === 'PASS' || svc.pass === true;
328
+ const dot = ok ? 'bg-emerald-500' : 'bg-red-500';
329
+ const name = svc.name || svc.service || 'unknown';
330
+ return `<div class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full ${dot} flex-shrink-0"></span><span class="text-xs text-slate-400 truncate">${escapeHtml(name)}</span></div>`;
331
+ }).join('');
332
+ } else {
333
+ const entries = Object.entries(watchdogData).filter(([k]) => k !== 'summary' && k !== 'timestamp');
334
+ if (entries.length > 0) {
335
+ watchdogServices.innerHTML = entries.map(([key, val]) => {
336
+ if (typeof val !== 'object' || val === null) return '';
337
+ const ok = val.status === 'PASS' || val.status === 'ok' || val.pass === true;
338
+ const dot = ok ? 'bg-emerald-500' : 'bg-red-500';
339
+ return `<div class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full ${dot} flex-shrink-0"></span><span class="text-xs text-slate-400 truncate">${escapeHtml(key)}</span></div>`;
340
+ }).filter(Boolean).join('');
317
341
  }
318
- return await res.json();
319
- } catch (err) {
320
- console.error(`fetchJSON(${url}):`, err);
321
- return null;
322
342
  }
343
+ } else {
344
+ watchdogBadge.textContent = 'N/A';
345
+ watchdogBadge.className = 'inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium bg-slate-700 text-slate-400';
346
+ watchdogServices.innerHTML = '<div class="text-xs text-slate-600">no watchdog data</div>';
323
347
  }
324
348
 
325
- function getToday() {
326
- return new Date().toISOString().split('T')[0];
349
+ // --- Agenda ---
350
+ const agendaList = document.getElementById('agenda-list');
351
+ const agendaItems = [];
352
+ if (remindersData?.reminders) {
353
+ const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED'];
354
+ remindersData.reminders.filter(r => !excludeStatus.includes(r.status) && r.date && r.date <= today)
355
+ .forEach(r => agendaItems.push({ text: r.description, type: 'reminder', date: r.date }));
327
356
  }
328
-
329
- // -----------------------------------------------------------------------
330
- // Clock
331
- // -----------------------------------------------------------------------
332
- function updateClock() {
333
- const now = new Date();
334
- document.getElementById('current-time').textContent =
335
- now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
336
- }
337
- updateClock();
338
- setInterval(updateClock, 1000);
339
-
340
- // -----------------------------------------------------------------------
341
- // Modal
342
- // -----------------------------------------------------------------------
343
- function openQuickCreate(type) {
344
- const overlay = document.getElementById('modal-overlay');
345
- const title = document.getElementById('modal-title');
346
- const typeInput = document.getElementById('modal-type');
347
- const categoryGroup = document.getElementById('category-group');
348
- const noteDirectionGroup = document.getElementById('note-direction-group');
349
- document.getElementById('modal-form').reset();
350
-
351
- typeInput.value = type;
352
- categoryGroup.classList.toggle('hidden', type !== 'reminder');
353
- noteDirectionGroup.classList.toggle('hidden', type !== 'note');
354
-
355
- if (type === 'reminder') title.textContent = 'New Reminder';
356
- else if (type === 'followup') title.textContent = 'New Followup';
357
- else title.textContent = 'New Note';
358
-
359
- overlay.classList.remove('hidden');
357
+ if (followupsData?.followups) {
358
+ const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED'];
359
+ followupsData.followups.filter(f => !excludeStatus.includes(f.status) && f.date && f.date <= today)
360
+ .forEach(f => agendaItems.push({ text: f.description, type: 'followup', date: f.date }));
360
361
  }
361
-
362
- function closeModal() {
363
- document.getElementById('modal-overlay').classList.add('hidden');
362
+ if (agendaItems.length > 0) {
363
+ agendaList.innerHTML = agendaItems.slice(0, 8).map(item => {
364
+ const color = item.type === 'reminder' ? 'text-amber-400' : 'text-violet-400';
365
+ const badge = item.type === 'reminder' ? 'bg-amber-500/10 text-amber-400' : 'bg-violet-500/10 text-violet-400';
366
+ return `<li class="flex items-start gap-2 py-1">
367
+ <span class="px-1.5 py-0.5 rounded text-[9px] font-medium ${badge} mt-0.5">${item.type}</span>
368
+ <span class="text-xs text-slate-300 leading-relaxed">${escapeHtml(item.text || '--')}</span>
369
+ </li>`;
370
+ }).join('');
371
+ } else {
372
+ agendaList.innerHTML = '<li class="text-xs text-slate-600 py-1">No items due today</li>';
364
373
  }
365
374
 
366
- document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
367
-
368
- async function submitQuickCreate(e) {
369
- e.preventDefault();
370
- const fd = new FormData(document.getElementById('modal-form'));
371
- const type = fd.get('type');
372
- let url, body;
373
-
374
- if (type === 'reminder') {
375
- url = '/api/reminders';
376
- body = { description: fd.get('description'), date: fd.get('date') || null, category: fd.get('category') || 'general' };
377
- } else if (type === 'followup') {
378
- url = '/api/followups';
379
- body = { description: fd.get('description'), date: fd.get('date') || null };
380
- } else {
381
- url = '/api/inbox';
382
- body = { direction: fd.get('direction') || 'to_nexo', content: fd.get('description') };
383
- }
384
-
385
- try {
386
- const res = await fetch(url, {
387
- method: 'POST',
388
- headers: { 'Content-Type': 'application/json' },
389
- body: JSON.stringify(body)
390
- });
391
- const data = await res.json();
392
- if (data.success) {
393
- showToast('Created successfully');
394
- closeModal();
395
- loadDashboardData();
396
- } else {
397
- showToast(data.error || data.detail || 'Create failed (HTTP ' + res.status + ')');
398
- }
399
- } catch (err) {
400
- showToast('Error: ' + err.message);
375
+ // --- Cognitive Memory ---
376
+ if (statsData) {
377
+ const cog = statsData.cognitive || statsData;
378
+ const stm = cog.stm_total ?? cog.stm_active ?? cog.stm_count ?? cog.stm ?? 0;
379
+ const ltm = cog.ltm_active ?? cog.ltm_count ?? cog.ltm ?? 0;
380
+ const strength = cog.avg_stm_strength ?? cog.avg_strength ?? 0;
381
+ const stmCap = cog.stm_capacity || 500;
382
+
383
+ document.getElementById('cog-stm').textContent = stm;
384
+ document.getElementById('cog-ltm').textContent = ltm;
385
+ document.getElementById('cog-strength').textContent = typeof strength === 'number' ? strength.toFixed(2) : strength;
386
+ document.getElementById('cog-stm-bar').style.width = Math.min(100, (stm / stmCap) * 100) + '%';
387
+ document.getElementById('cog-ltm-bar').style.width = Math.min(100, ltm > 0 ? 60 : 0) + '%';
388
+
389
+ // KG stats
390
+ const kg = statsData.knowledge_graph || statsData.kg || {};
391
+ if (kg.nodes !== undefined || kg.entities !== undefined || kg.total_nodes !== undefined) {
392
+ document.getElementById('kg-nodes').textContent = formatNumber(kg.nodes ?? kg.total_nodes ?? kg.entities ?? 0);
393
+ document.getElementById('kg-edges').textContent = formatNumber(kg.edges ?? kg.edges_active ?? kg.relations ?? 0);
394
+ document.getElementById('kg-historical').textContent = formatNumber(kg.historical ?? kg.edges_historical ?? 0);
401
395
  }
402
396
  }
403
397
 
404
- // -----------------------------------------------------------------------
405
- // Dashboard data
406
- // -----------------------------------------------------------------------
407
- async function loadDashboardData() {
408
- const today = getToday();
409
-
410
- const [trustData, statsData, remindersData, followupsData, sessionsData, watchdogData, inboxData] =
411
- await Promise.all([
412
- fetchJSON('/api/trust'),
413
- fetchJSON('/api/stats'),
414
- fetchJSON('/api/reminders'),
415
- fetchJSON('/api/followups'),
416
- fetchJSON('/api/sessions?limit=3'),
417
- fetchJSON('/api/watchdog'),
418
- fetchJSON('/api/inbox/unread'),
419
- ]);
420
-
421
- // --- Trust Score ---
422
- if (trustData) {
423
- const score = trustData.current_score ?? 0;
424
- const pct = Math.max(0, Math.min(100, score));
425
- document.getElementById('trust-value').textContent = score.toFixed(1);
426
- document.getElementById('trust-bar').style.width = pct + '%';
427
- document.getElementById('sidebar-trust-value').textContent = score.toFixed(1);
428
- const label = pct >= 75 ? 'high alignment' : pct >= 50 ? 'moderate' : pct >= 25 ? 'low — more caution' : 'critical';
429
- document.getElementById('trust-label').textContent = label;
430
- // Color trust value by score
431
- const trustEl = document.getElementById('trust-value');
432
- trustEl.className = 'text-2xl font-mono font-semibold ' +
433
- (pct >= 75 ? 'text-violet-400' : pct >= 50 ? 'text-amber-400' : 'text-red-400');
434
- }
435
-
436
- // --- Active Sessions ---
437
- if (sessionsData && sessionsData.sessions) {
438
- const cutoff = Date.now() - 15 * 60 * 1000;
439
- const active = sessionsData.sessions.filter(s => {
440
- const ts = new Date(s.last_heartbeat || s.created_at || 0).getTime();
441
- return ts > cutoff;
442
- });
443
- document.getElementById('session-count').textContent = active.length;
444
- document.getElementById('session-label').textContent = active.length === 1 ? 'active terminal' : 'active terminals';
445
- if (active.length > 0) {
446
- const names = active.slice(0, 2).map(s => s.session_id ? s.session_id.substring(0, 8) : '??').join(', ');
447
- document.getElementById('session-detail').textContent = names + (active.length > 2 ? ' +' + (active.length - 2) : '');
448
- } else {
449
- document.getElementById('session-detail').textContent = 'none active';
450
- document.getElementById('session-pulse').classList.remove('animate-ping');
451
- }
452
- }
453
-
454
- // --- Overdue Items ---
455
- if (remindersData || followupsData) {
456
- const excludeStatus = ['completed', 'COMPLETED', 'archived', 'deleted', 'DELETED', 'blocked', 'waiting'];
457
- const reminders = (remindersData?.reminders || []).filter(r =>
458
- !excludeStatus.includes(r.status) && r.date && r.date <= today
459
- );
460
- const followups = (followupsData?.followups || []).filter(f =>
461
- !excludeStatus.includes(f.status) && f.date && f.date <= today
462
- );
463
- const total = reminders.length + followups.length;
464
- const el = document.getElementById('overdue-count');
465
- el.textContent = total;
466
- el.className = 'text-2xl font-mono font-semibold ' + (total > 0 ? 'text-red-400' : 'text-slate-200');
467
- document.getElementById('overdue-detail').textContent =
468
- reminders.length + ' reminders, ' + followups.length + ' followups';
469
- }
470
-
471
- // --- Watchdog ---
472
- const watchdogBadge = document.getElementById('watchdog-badge');
473
- const watchdogServices = document.getElementById('watchdog-services');
474
- if (watchdogData && !watchdogData.error) {
475
- const overall = watchdogData.summary?.overall || 'UNKNOWN';
476
- const isPass = overall === 'PASS' || overall === 'ok';
477
- watchdogBadge.textContent = overall;
478
- watchdogBadge.className = 'inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium ' +
479
- (isPass ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400');
480
-
481
- const services = watchdogData.services || watchdogData.checks || [];
482
- if (Array.isArray(services) && services.length > 0) {
483
- watchdogServices.innerHTML = services.map(svc => {
484
- const ok = svc.status === 'ok' || svc.status === 'PASS' || svc.pass === true;
485
- const dot = ok ? 'bg-emerald-500' : 'bg-red-500';
486
- const name = svc.name || svc.service || 'unknown';
487
- return `<div class="flex items-center gap-1.5">
488
- <span class="w-1.5 h-1.5 rounded-full ${dot} flex-shrink-0"></span>
489
- <span class="text-xs text-slate-400 truncate">${escapeHtml(name)}</span>
490
- </div>`;
491
- }).join('');
492
- } else {
493
- // Try to parse from object keys
494
- const entries = Object.entries(watchdogData).filter(([k]) => k !== 'summary' && k !== 'timestamp');
495
- if (entries.length > 0) {
496
- watchdogServices.innerHTML = entries.map(([key, val]) => {
497
- if (typeof val !== 'object' || val === null) return '';
498
- const ok = val.status === 'PASS' || val.status === 'ok' || val.pass === true;
499
- const dot = ok ? 'bg-emerald-500' : 'bg-red-500';
500
- return `<div class="flex items-center gap-1.5">
501
- <span class="w-1.5 h-1.5 rounded-full ${dot} flex-shrink-0"></span>
502
- <span class="text-xs text-slate-400 truncate">${escapeHtml(key)}</span>
503
- </div>`;
504
- }).filter(Boolean).join('');
505
- } else {
506
- watchdogServices.innerHTML = '<div class="text-xs text-slate-500">no services</div>';
507
- }
508
- }
398
+ // --- Recent Sessions ---
399
+ if (sessionsData) {
400
+ const diaries = sessionsData.diaries || sessionsData.sessions || [];
401
+ const container = document.getElementById('recent-sessions');
402
+ if (diaries.length > 0) {
403
+ container.innerHTML = diaries.slice(0, 3).map(d => {
404
+ return `<div class="border-b border-slate-800/30 pb-2.5 last:border-0 last:pb-0">
405
+ <div class="flex items-center gap-2 mb-1">
406
+ <span class="text-[10px] font-mono text-violet-400">${escapeHtml(String(d.session_id || d.id || '').substring(0, 8))}</span>
407
+ <span class="text-[10px] text-slate-600">${relativeTime(d.created_at)}</span>
408
+ ${d.domain ? `<span class="text-[10px] px-1 py-0.5 rounded bg-slate-800 text-slate-400">${escapeHtml(d.domain)}</span>` : ''}
409
+ </div>
410
+ ${d.summary ? `<p class="text-xs text-slate-400 leading-relaxed line-clamp-2">${escapeHtml(d.summary)}</p>` : ''}
411
+ ${d.mental_state ? `<div class="text-[10px] text-violet-400/60 mt-1 italic">${escapeHtml(d.mental_state)}</div>` : ''}
412
+ </div>`;
413
+ }).join('');
509
414
  } else {
510
- watchdogBadge.textContent = 'N/A';
511
- watchdogBadge.className = 'inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-medium bg-slate-700 text-slate-500';
512
- watchdogServices.innerHTML = '<div class="text-xs text-slate-500">unavailable</div>';
513
- }
514
-
515
- // --- Cognitive Memory ---
516
- if (statsData) {
517
- const cog = statsData.cognitive || {};
518
- const stm = cog.stm_count ?? cog.stm ?? 0;
519
- const ltm = cog.ltm_count ?? cog.ltm ?? 0;
520
- const maxMem = Math.max(stm + ltm, 100);
521
-
522
- document.getElementById('cog-stm').textContent = stm;
523
- document.getElementById('cog-ltm').textContent = ltm;
524
- document.getElementById('cog-stm-bar').style.width = Math.min(100, (stm / maxMem) * 100) + '%';
525
- document.getElementById('cog-ltm-bar').style.width = Math.min(100, (ltm / maxMem) * 100) + '%';
526
-
527
- const strength = cog.avg_strength;
528
- document.getElementById('cog-strength').textContent =
529
- typeof strength === 'number' ? strength.toFixed(3) : (strength ?? '--');
530
-
531
- const kg = statsData.knowledge_graph || {};
532
- document.getElementById('kg-nodes').textContent = kg.nodes ?? '--';
533
- document.getElementById('kg-edges').textContent = kg.edges_active ?? '--';
534
- document.getElementById('kg-historical').textContent = kg.edges_historical ?? '--';
535
- }
536
-
537
- // --- Today's Agenda ---
538
- if (remindersData || followupsData) {
539
- const items = [];
540
- (remindersData?.reminders || []).forEach(r => {
541
- if (r.status === 'PENDING' && r.date && r.date <= today) {
542
- items.push({ ...r, _type: 'R', _overdue: r.date < today });
543
- }
544
- });
545
- (followupsData?.followups || []).forEach(f => {
546
- if (f.status === 'PENDING' && f.date && f.date <= today) {
547
- items.push({ ...f, _type: 'F', _overdue: f.date < today });
548
- }
549
- });
550
-
551
- // Sort: overdue first, then by date
552
- items.sort((a, b) => {
553
- if (a._overdue && !b._overdue) return -1;
554
- if (!a._overdue && b._overdue) return 1;
555
- return (a.date || '').localeCompare(b.date || '');
556
- });
557
-
558
- const list = document.getElementById('agenda-list');
559
- if (items.length === 0) {
560
- list.innerHTML = '<li class="text-xs text-slate-600 py-1">No items due today</li>';
561
- } else {
562
- list.innerHTML = items.slice(0, 6).map(item => {
563
- const badgeColor = item._type === 'R'
564
- ? (item._overdue ? 'bg-red-500/15 text-red-400' : 'bg-violet-500/15 text-violet-400')
565
- : (item._overdue ? 'bg-orange-500/15 text-orange-400' : 'bg-pink-500/15 text-pink-400');
566
- const desc = (item.description || '').length > 72
567
- ? item.description.substring(0, 72) + '…'
568
- : item.description;
569
- const dateLabel = item._overdue ? relativeDate(item.date) : 'today';
570
- return `<li class="flex items-start gap-2 py-1 border-b border-slate-800/50 last:border-0">
571
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono font-medium flex-shrink-0 mt-0.5 ${badgeColor}">${item._type}</span>
572
- <span class="text-xs text-slate-300 leading-relaxed flex-1 min-w-0">${escapeHtml(desc)}</span>
573
- <span class="text-xs text-slate-500 flex-shrink-0 font-mono">${dateLabel}</span>
574
- </li>`;
575
- }).join('');
576
-
577
- if (items.length > 6) {
578
- list.innerHTML += `<li class="text-xs text-slate-500 pt-1">+${items.length - 6} more in <a href="/ops" class="text-violet-500 hover:text-violet-400">ops</a></li>`;
579
- }
580
- }
581
- }
582
-
583
- // --- Recent Sessions ---
584
- if (sessionsData && sessionsData.sessions) {
585
- const container = document.getElementById('recent-sessions');
586
- const diaries = sessionsData.sessions.slice(0, 3);
587
- if (diaries.length === 0) {
588
- container.innerHTML = '<div class="text-xs text-slate-600">No session diaries yet</div>';
589
- } else {
590
- container.innerHTML = diaries.map(s => {
591
- const summary = (s.summary || s.mental_state || 'No summary').substring(0, 160);
592
- const sid = (s.session_id || s.id || '--').toString().substring(0, 10);
593
- const dateStr = s.created_at
594
- ? new Date(s.created_at).toLocaleDateString('en-GB', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
595
- : '';
596
- return `<div class="pb-3 border-b border-slate-800/50 last:border-0 last:pb-0">
597
- <div class="flex items-center justify-between mb-1">
598
- <span class="text-xs font-mono text-slate-500">${escapeHtml(sid)}</span>
599
- <span class="text-xs text-slate-500">${escapeHtml(dateStr)}</span>
600
- </div>
601
- <p class="text-xs text-slate-400 leading-relaxed">${escapeHtml(summary)}</p>
602
- </div>`;
603
- }).join('');
604
- }
415
+ container.innerHTML = '<div class="text-xs text-slate-600">No recent sessions</div>';
605
416
  }
417
+ }
606
418
 
607
- // --- Inbox Badge ---
608
- if (inboxData) {
609
- const total = inboxData.total || 0;
610
- const badge = document.getElementById('inbox-badge');
611
- if (total > 0) {
612
- badge.textContent = total > 9 ? '9+' : total;
613
- badge.classList.remove('hidden');
614
- badge.classList.add('flex');
615
- } else {
616
- badge.classList.add('hidden');
617
- badge.classList.remove('flex');
618
- }
419
+ // --- Inbox badge ---
420
+ if (inboxData) {
421
+ const count = inboxData.count ?? inboxData.unread ?? (Array.isArray(inboxData) ? inboxData.length : 0);
422
+ const badge = document.getElementById('inbox-badge');
423
+ if (badge && count > 0) {
424
+ badge.textContent = count;
425
+ badge.classList.remove('hidden');
426
+ badge.classList.add('flex');
619
427
  }
620
428
  }
429
+ }
621
430
 
622
- // -----------------------------------------------------------------------
623
- // Init
624
- // -----------------------------------------------------------------------
625
- loadDashboardData();
626
- setInterval(loadDashboardData, 60000);
627
- </script>
628
- </body>
629
- </html>
431
+ loadDashboardData();
432
+ setInterval(loadDashboardData, 60000);
433
+ </script>
434
+ {% endblock %}