nexo-brain 5.3.13 → 5.3.15

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 (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -1
  3. package/package.json +1 -1
  4. package/src/crons/sync.py +18 -4
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_entities.py +1 -1
  43. package/src/db/_episodic 2.py +762 -0
  44. package/src/db/_evolution 2.py +54 -0
  45. package/src/db/_fts 2.py +406 -0
  46. package/src/db/_goal_profiles 2.py +376 -0
  47. package/src/db/_hot_context 2.py +660 -0
  48. package/src/db/_outcomes 2.py +800 -0
  49. package/src/db/_personal_scripts 2.py +582 -0
  50. package/src/db/_sessions 2.py +330 -0
  51. package/src/db/_tasks 2.py +91 -0
  52. package/src/db/_watchers 2.py +173 -0
  53. package/src/doctor/formatters 2.py +52 -0
  54. package/src/doctor/models 2.py +69 -0
  55. package/src/doctor/planes 2.py +87 -0
  56. package/src/doctor/providers/__init__ 2.py +1 -0
  57. package/src/doctor/providers/deep 2.py +367 -0
  58. package/src/evolution_cycle 2.py +519 -0
  59. package/src/hooks/auto_capture 2.py +208 -0
  60. package/src/hooks/caffeinate-guard 2.sh +8 -0
  61. package/src/hooks/capture-session 2.sh +21 -0
  62. package/src/hooks/capture-tool-logs 2.sh +158 -0
  63. package/src/hooks/daily-briefing-check 2.sh +33 -0
  64. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  65. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  66. package/src/hooks/inbox-hook 2.sh +76 -0
  67. package/src/hooks/post-compact 2.sh +152 -0
  68. package/src/hooks/pre-compact 2.sh +169 -0
  69. package/src/hooks/protocol-guardrail 2.sh +10 -0
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  71. package/src/hooks/session-stop 2.sh +52 -0
  72. package/src/kg_populate 2.py +292 -0
  73. package/src/maintenance 2.py +53 -0
  74. package/src/memory_backends 2.py +71 -0
  75. package/src/migrate_embeddings 2.py +124 -0
  76. package/src/nexo_sdk 2.py +103 -0
  77. package/src/observability 2.py +199 -0
  78. package/src/plugin_loader 2.py +217 -0
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/agents.py +10 -3
  81. package/src/plugins/artifact_registry 2.py +450 -0
  82. package/src/plugins/backup 2.py +127 -0
  83. package/src/plugins/claims_tools 2.py +119 -0
  84. package/src/plugins/cognitive_memory 2.py +609 -0
  85. package/src/plugins/core_rules 2.py +252 -0
  86. package/src/plugins/cortex 2.py +1155 -0
  87. package/src/plugins/entities 2.py +67 -0
  88. package/src/plugins/episodic_memory 2.py +560 -0
  89. package/src/plugins/evolution 2.py +167 -0
  90. package/src/plugins/goal_engine 2.py +142 -0
  91. package/src/plugins/guard 2.py +862 -0
  92. package/src/plugins/impact 2.py +29 -0
  93. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  94. package/src/plugins/media_memory_tools 2.py +98 -0
  95. package/src/plugins/memory_export 2.py +196 -0
  96. package/src/plugins/outcomes 2.py +130 -0
  97. package/src/plugins/personal_scripts 2.py +117 -0
  98. package/src/plugins/preferences 2.py +47 -0
  99. package/src/plugins/protocol 2.py +1449 -0
  100. package/src/plugins/schedule.py +2 -1
  101. package/src/plugins/simple_api 2.py +106 -0
  102. package/src/plugins/skills 2.py +341 -0
  103. package/src/plugins/state_watchers 2.py +79 -0
  104. package/src/plugins/update 2.py +986 -0
  105. package/src/plugins/user_state_tools 2.py +43 -0
  106. package/src/plugins/workflow 2.py +588 -0
  107. package/src/protocol_settings 2.py +59 -0
  108. package/src/public_contribution 2.py +466 -0
  109. package/src/public_evolution_queue 2.py +241 -0
  110. package/src/requirements 2.txt +14 -0
  111. package/src/requirements.txt +1 -1
  112. package/src/retroactive_learnings 2.py +373 -0
  113. package/src/rules/__init__ 2.py +0 -0
  114. package/src/rules/core-rules 2.json +331 -0
  115. package/src/rules/migrate 2.py +207 -0
  116. package/src/runtime_power 2.py +874 -0
  117. package/src/runtime_power.py +18 -1
  118. package/src/script_registry 2.py +1559 -0
  119. package/src/scripts/check-context 2.py +272 -0
  120. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  121. package/src/scripts/deep-sleep/collect 2.py +928 -0
  122. package/src/scripts/deep-sleep/extract 2.py +330 -0
  123. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  124. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  125. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  126. package/src/scripts/nexo-agent-run 2.py +75 -0
  127. package/src/scripts/nexo-auto-update 2.py +6 -0
  128. package/src/scripts/nexo-backup 2.sh +25 -0
  129. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  130. package/src/scripts/nexo-catchup 2.py +300 -0
  131. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  132. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  133. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  134. package/src/scripts/nexo-cron-wrapper.sh +7 -0
  135. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  136. package/src/scripts/nexo-dashboard 2.sh +29 -0
  137. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  138. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  139. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  140. package/src/scripts/nexo-hook-record 2.py +42 -0
  141. package/src/scripts/nexo-immune 2.py +936 -0
  142. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  143. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  144. package/src/scripts/nexo-install 2.py +6 -0
  145. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  146. package/src/scripts/nexo-learning-validator 2.py +266 -0
  147. package/src/scripts/nexo-migrate 2.py +260 -0
  148. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  149. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  150. package/src/scripts/nexo-pre-commit 2.py +120 -0
  151. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  152. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  153. package/src/scripts/nexo-reflection 2.py +256 -0
  154. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  155. package/src/scripts/nexo-sleep 2.py +631 -0
  156. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  157. package/src/scripts/nexo-sync-clients 2.py +16 -0
  158. package/src/scripts/nexo-synthesis 2.py +475 -0
  159. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  160. package/src/scripts/nexo-update 2.sh +306 -0
  161. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  162. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  163. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  164. package/src/server 2.py +1296 -0
  165. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  166. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  167. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  168. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  169. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  170. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  171. package/src/skills/run-release-final-audit/script 2.py +259 -0
  172. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  173. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  174. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  175. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  176. package/src/skills_runtime 2.py +932 -0
  177. package/src/state_watchers_runtime 2.py +475 -0
  178. package/src/storage_router 2.py +32 -0
  179. package/src/system_catalog 2.py +786 -0
  180. package/src/tools_coordination 2.py +103 -0
  181. package/src/tools_credentials 2.py +68 -0
  182. package/src/tools_drive 2.py +487 -0
  183. package/src/tools_hot_context 2.py +163 -0
  184. package/src/tools_learnings 2.py +612 -0
  185. package/src/tools_menu 2.py +229 -0
  186. package/src/tools_reminders 2.py +88 -0
  187. package/src/tools_reminders_crud 2.py +363 -0
  188. package/src/tools_sessions 2.py +1054 -0
  189. package/src/tools_system_catalog 2.py +19 -0
  190. package/src/tools_task_history 2.py +57 -0
  191. package/src/tools_transcripts 2.py +98 -0
  192. package/src/transcript_utils 2.py +412 -0
  193. package/src/user_context 2.py +46 -0
  194. package/src/user_data_portability 2.py +328 -0
  195. package/src/user_state_model 2.py +170 -0
  196. package/templates/CLAUDE.md 2.template +108 -0
  197. package/templates/CODEX.AGENTS.md 2.template +66 -0
  198. package/templates/launchagents/README 2.md +132 -0
  199. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  200. package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
  201. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  202. package/templates/launchagents/com.nexo.catchup.plist +1 -1
  203. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  204. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  205. package/templates/launchagents/com.nexo.dashboard.plist +1 -1
  206. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  207. package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
  208. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  209. package/templates/launchagents/com.nexo.evolution.plist +1 -1
  210. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  211. package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
  212. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  213. package/templates/launchagents/com.nexo.immune.plist +1 -1
  214. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  215. package/templates/launchagents/com.nexo.postmortem.plist +1 -1
  216. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  217. package/templates/launchagents/com.nexo.self-audit.plist +1 -1
  218. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  219. package/templates/launchagents/com.nexo.synthesis.plist +1 -1
  220. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  221. package/templates/launchagents/com.nexo.watchdog.plist +1 -1
  222. package/templates/nexo_helper 2.py +301 -0
  223. package/templates/openclaw 2.json +13 -0
  224. package/templates/plugin-template 2.py +40 -0
  225. package/templates/script-template 2.py +59 -0
  226. package/templates/script-template 2.sh +13 -0
  227. package/templates/script-template.py +5 -4
  228. package/templates/skill-script-template 2.py +48 -0
  229. package/templates/skill-script-template.py +2 -1
  230. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,185 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Plugins{% endblock %}
4
+
5
+ {% block page_title %}Plugin Manager{% endblock %}
6
+ {% block page_subtitle %}<span class="text-xs text-slate-500 ml-2">Overview of all loaded plugins</span>{% endblock %}
7
+
8
+ {% block header_actions %}
9
+ <div class="relative">
10
+ <svg class="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
11
+ <input type="text" id="search-input" placeholder="Search plugins or tools..." onkeyup="applyFilters()"
12
+ class="text-xs bg-slate-800 border border-slate-700 rounded-md pl-8 pr-3 py-1.5 text-slate-300 placeholder-slate-500 w-48 focus:outline-none focus:border-nexo-500 focus:w-64 transition-all">
13
+ </div>
14
+ <button onclick="loadData()" class="text-xs text-slate-400 hover:text-slate-200 transition-colors px-2 py-1.5 rounded hover:bg-slate-800">
15
+ <svg class="w-3.5 h-3.5 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
16
+ Refresh
17
+ </button>
18
+ {% endblock %}
19
+
20
+ {% block content %}
21
+ <!-- Stats row -->
22
+ <div class="grid grid-cols-3 gap-4 mb-6">
23
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
24
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Total Plugins</p>
25
+ <div class="flex items-baseline gap-2">
26
+ <p class="text-2xl font-display font-bold text-slate-100" id="stat-plugins">--</p>
27
+ <span class="text-xs text-slate-500">loaded</span>
28
+ </div>
29
+ </div>
30
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
31
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Total Tools</p>
32
+ <div class="flex items-baseline gap-2">
33
+ <p class="text-2xl font-display font-bold text-nexo-400" id="stat-tools">--</p>
34
+ <span class="text-xs text-slate-500">available</span>
35
+ </div>
36
+ </div>
37
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
38
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Avg Tools/Plugin</p>
39
+ <p class="text-2xl font-display font-bold text-slate-100" id="stat-avg">--</p>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Plugin grid -->
44
+ <div id="plugins-grid" class="grid grid-cols-2 gap-4">
45
+ <div class="col-span-2 py-16 text-center text-sm text-slate-500">Loading plugins...</div>
46
+ </div>
47
+
48
+ <!-- Empty state -->
49
+ <div id="no-results" class="hidden py-16 text-center">
50
+ <svg class="w-12 h-12 mx-auto text-slate-700 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/></svg>
51
+ <p class="text-sm text-slate-500">No plugins match your search</p>
52
+ </div>
53
+ {% endblock %}
54
+
55
+ {% block scripts %}
56
+ <style>
57
+ @keyframes pluginFadeIn {
58
+ from { opacity: 0; transform: translateY(12px); }
59
+ to { opacity: 1; transform: translateY(0); }
60
+ }
61
+ .plugin-card {
62
+ animation: pluginFadeIn 0.4s ease-out both;
63
+ }
64
+ </style>
65
+ <script>
66
+ let allPlugins = [];
67
+
68
+ function freshnessBadge(loadedAt) {
69
+ if (!loadedAt) return '<span class="w-2 h-2 rounded-full bg-slate-600"></span>';
70
+ const d = new Date(loadedAt);
71
+ const diffH = (Date.now() - d.getTime()) / 3600000;
72
+ if (diffH < 1) return '<span class="w-2 h-2 rounded-full bg-emerald-400 glow-dot" style="color:#34D399" title="Loaded recently"></span>';
73
+ if (diffH < 24) return '<span class="w-2 h-2 rounded-full bg-amber-400" title="Loaded today"></span>';
74
+ return '<span class="w-2 h-2 rounded-full bg-slate-500" title="Loaded ' + Math.floor(diffH / 24) + 'd ago"></span>';
75
+ }
76
+
77
+ function createdByBadge(author) {
78
+ if (!author) return '';
79
+ const colors = {
80
+ 'system': 'bg-slate-700/50 text-slate-400',
81
+ 'user': 'bg-nexo-500/15 text-nexo-400',
82
+ 'nexo': 'bg-nexo-500/15 text-nexo-400',
83
+ 'plugin': 'bg-blue-500/15 text-blue-400',
84
+ };
85
+ const c = colors[(author || '').toLowerCase()] || 'bg-slate-700/50 text-slate-400';
86
+ return `<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold ${c}">${escapeHtml(author)}</span>`;
87
+ }
88
+
89
+ function renderPlugins(plugins) {
90
+ const grid = document.getElementById('plugins-grid');
91
+ const noResults = document.getElementById('no-results');
92
+
93
+ if (!plugins.length) {
94
+ grid.innerHTML = '';
95
+ noResults.classList.remove('hidden');
96
+ return;
97
+ }
98
+
99
+ noResults.classList.add('hidden');
100
+
101
+ grid.innerHTML = plugins.map((p, i) => {
102
+ const tools = p.tool_names || [];
103
+ const toolCount = p.tools_count || tools.length || 0;
104
+ const filename = p.filename || 'unknown';
105
+ const ext = filename.split('.').pop();
106
+
107
+ return `
108
+ <div class="plugin-card bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card hover:border-nexo-500/30" style="animation-delay: ${i * 60}ms">
109
+ <!-- Header -->
110
+ <div class="flex items-start justify-between mb-3">
111
+ <div class="flex items-center gap-3 min-w-0">
112
+ <div class="w-10 h-10 rounded-lg bg-nexo-500/10 border border-nexo-500/20 flex items-center justify-center flex-shrink-0">
113
+ <svg class="w-5 h-5 text-nexo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/></svg>
114
+ </div>
115
+ <div class="min-w-0">
116
+ <p class="text-sm font-display font-semibold text-slate-100 truncate">${escapeHtml(filename)}</p>
117
+ <div class="flex items-center gap-2 mt-0.5">
118
+ <span class="text-[10px] font-mono text-slate-500 uppercase">.${escapeHtml(ext)}</span>
119
+ ${createdByBadge(p.created_by)}
120
+ </div>
121
+ </div>
122
+ </div>
123
+ <div class="flex items-center gap-2 flex-shrink-0">
124
+ ${freshnessBadge(p.loaded_at)}
125
+ <span class="inline-flex items-center justify-center min-w-[28px] h-6 px-2 rounded-full bg-nexo-500/15 text-nexo-400 text-xs font-bold">${toolCount}</span>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Tool tags -->
130
+ ${tools.length ? `
131
+ <div class="flex flex-wrap gap-1.5 mb-3">
132
+ ${tools.slice(0, 12).map(t => `
133
+ <span class="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-mono bg-slate-800/80 text-slate-400 border border-slate-700/30 hover:border-nexo-500/30 hover:text-nexo-400 transition-colors">${escapeHtml(t)}</span>
134
+ `).join('')}
135
+ ${tools.length > 12 ? `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] text-slate-500">+${tools.length - 12} more</span>` : ''}
136
+ </div>
137
+ ` : '<p class="text-xs text-slate-600 mb-3">No tools exported</p>'}
138
+
139
+ <!-- Footer -->
140
+ <div class="flex items-center justify-between pt-3 border-t border-slate-800/30">
141
+ <span class="text-[10px] text-slate-600">${p.loaded_at ? 'Loaded ' + relativeTime(p.loaded_at) : 'Load time unknown'}</span>
142
+ <span class="text-[10px] text-slate-500 font-mono">${toolCount} tool${toolCount !== 1 ? 's' : ''}</span>
143
+ </div>
144
+ </div>
145
+ `;
146
+ }).join('');
147
+ }
148
+
149
+ function applyFilters() {
150
+ const search = (document.getElementById('search-input').value || '').toLowerCase().trim();
151
+
152
+ let filtered = allPlugins;
153
+ if (search) {
154
+ filtered = filtered.filter(p =>
155
+ (p.filename || '').toLowerCase().includes(search) ||
156
+ (p.created_by || '').toLowerCase().includes(search) ||
157
+ (p.tool_names || []).some(t => t.toLowerCase().includes(search))
158
+ );
159
+ }
160
+
161
+ renderPlugins(filtered);
162
+ }
163
+
164
+ async function loadData() {
165
+ const data = await fetchJSON('/api/plugins');
166
+ if (!data) return;
167
+
168
+ allPlugins = data.plugins || [];
169
+
170
+ // Stats
171
+ const totalPlugins = data.total || allPlugins.length;
172
+ const totalTools = data.total_tools || allPlugins.reduce((sum, p) => sum + (p.tools_count || (p.tool_names || []).length || 0), 0);
173
+ const avg = totalPlugins > 0 ? (totalTools / totalPlugins).toFixed(1) : '0';
174
+
175
+ document.getElementById('stat-plugins').textContent = formatNumber(totalPlugins);
176
+ document.getElementById('stat-tools').textContent = formatNumber(totalTools);
177
+ document.getElementById('stat-avg').textContent = avg;
178
+
179
+ applyFilters();
180
+ }
181
+
182
+ loadData();
183
+ setInterval(loadData, 60000);
184
+ </script>
185
+ {% endblock %}
@@ -0,0 +1,199 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Protocol Explainability{% endblock %}
4
+
5
+ {% block page_title %}Protocol Explainability{% endblock %}
6
+ {% block page_subtitle %}
7
+ <span class="text-xs text-slate-500 ml-2">Why the runtime acted, blocked, drifted, or asked for evidence</span>
8
+ {% endblock %}
9
+
10
+ {% block header_actions %}
11
+ <a href="/api/protocol" class="text-xs text-slate-400 hover:text-slate-200 transition-colors px-2 py-1 rounded hover:bg-slate-800 font-mono">
12
+ /api/protocol
13
+ </a>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ {% set compliance = snapshot.protocol_summary or {} %}
18
+ {% set overall = compliance.overall_compliance_pct or 0 %}
19
+ {% set debt = snapshot.debt_summary or {} %}
20
+ {% set workflow = snapshot.workflow_summary or {} %}
21
+ {% set goals = snapshot.goal_summary or {} %}
22
+ {% set conditioned = snapshot.conditioned_learnings or [] %}
23
+
24
+ <div class="grid grid-cols-5 gap-4 mb-6">
25
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
26
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Compliance 7d</p>
27
+ <p class="text-2xl font-display font-bold {{ 'text-emerald-400' if overall >= 80 else 'text-amber-400' if overall >= 60 else 'text-red-400' }}">{{ "%.1f"|format(overall) }}%</p>
28
+ </div>
29
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
30
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Open Debt</p>
31
+ <p class="text-2xl font-display font-bold {{ 'text-emerald-400' if (debt.open_total or 0) == 0 else 'text-red-400' }}">{{ debt.open_total or 0 }}</p>
32
+ </div>
33
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
34
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Open Runs</p>
35
+ <p class="text-2xl font-display font-bold text-slate-100">{{ workflow.open_runs or 0 }}</p>
36
+ </div>
37
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
38
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Blocked Goals</p>
39
+ <p class="text-2xl font-display font-bold {{ 'text-red-400' if (goals.blocked or 0) else 'text-emerald-400' }}">{{ goals.blocked or 0 }}</p>
40
+ </div>
41
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
42
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Conditioned Files</p>
43
+ <p class="text-2xl font-display font-bold text-nexo-400">{{ conditioned|length }}</p>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="grid grid-cols-2 gap-5 mb-6">
48
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
49
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Protocol Requirements</h2>
50
+ <div class="space-y-3">
51
+ {% for key, label in [('guard_check', 'Guard before action'), ('heartbeat', 'Heartbeat with context'), ('change_log', 'Change log after edits'), ('learning_capture', 'Learning capture after correction'), ('done_evidence', 'Evidence before done')] %}
52
+ {% set item = compliance.get(key, {}) %}
53
+ <div class="flex items-center justify-between gap-4">
54
+ <div>
55
+ <p class="text-sm text-slate-200">{{ label }}</p>
56
+ <p class="text-[11px] text-slate-500 font-mono">
57
+ {% if item.required is defined %}required {{ item.required }}{% endif %}
58
+ {% if item.total is defined %}total {{ item.total }}{% endif %}
59
+ {% if item.executed is defined %} • executed {{ item.executed }}{% endif %}
60
+ {% if item.logged is defined %} • logged {{ item.logged }}{% endif %}
61
+ {% if item.captured is defined %} • captured {{ item.captured }}{% endif %}
62
+ {% if item.done_tasks is defined %} • done {{ item.done_tasks }}{% endif %}
63
+ </p>
64
+ </div>
65
+ <span class="text-sm font-mono {{ 'text-emerald-400' if (item.compliance_pct or 0) >= 80 else 'text-amber-400' if (item.compliance_pct or 0) >= 60 else 'text-red-400' }}">
66
+ {{ "%.1f"|format(item.compliance_pct or 0) }}%
67
+ </span>
68
+ </div>
69
+ {% endfor %}
70
+ </div>
71
+ </section>
72
+
73
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
74
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Debt Pressure</h2>
75
+ <div class="grid grid-cols-3 gap-3 mb-4">
76
+ {% for severity in ['error', 'warn', 'info'] %}
77
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
78
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">{{ severity }}</p>
79
+ <p class="text-xl font-display font-bold text-slate-100">{{ debt.by_severity.get(severity, 0) }}</p>
80
+ </div>
81
+ {% endfor %}
82
+ </div>
83
+ <div class="space-y-2 max-h-56 overflow-y-auto pr-1">
84
+ {% for item in snapshot.recent_debts[:10] %}
85
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
86
+ <div class="flex items-center justify-between gap-3 mb-1">
87
+ <span class="text-xs font-semibold {{ 'text-red-400' if item.severity == 'error' else 'text-amber-400' if item.severity == 'warn' else 'text-slate-400' }}">{{ item.debt_type }}</span>
88
+ <span class="text-[10px] text-slate-500 font-mono">{{ item.status }}</span>
89
+ </div>
90
+ <p class="text-xs text-slate-300">{{ item.evidence or 'No evidence summary recorded.' }}</p>
91
+ </div>
92
+ {% else %}
93
+ <p class="text-sm text-slate-500">No protocol debt recorded.</p>
94
+ {% endfor %}
95
+ </div>
96
+ </section>
97
+ </div>
98
+
99
+ <div class="grid grid-cols-2 gap-5 mb-6">
100
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
101
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Recent Protocol Tasks</h2>
102
+ <div class="space-y-3 max-h-[420px] overflow-y-auto pr-1">
103
+ {% for item in snapshot.recent_tasks[:12] %}
104
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
105
+ <div class="flex items-center justify-between gap-3 mb-1">
106
+ <p class="text-sm text-slate-100">{{ item.goal }}</p>
107
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">{{ item.status }}</span>
108
+ </div>
109
+ <p class="text-[11px] text-slate-500 font-mono mb-2">{{ item.task_type }} • cortex={{ item.cortex_mode or 'n/a' }} • response={{ item.response_mode or 'n/a' }}</p>
110
+ <div class="flex flex-wrap gap-2 text-[10px]">
111
+ {% if item.guarded_open %}<span class="px-2 py-0.5 rounded bg-nexo-500/15 text-nexo-300">guarded open</span>{% endif %}
112
+ {% if item.must_verify %}<span class="px-2 py-0.5 rounded bg-amber-500/15 text-amber-300">must verify</span>{% endif %}
113
+ {% if item.must_change_log %}<span class="px-2 py-0.5 rounded bg-blue-500/15 text-blue-300">must change-log</span>{% endif %}
114
+ {% if item.has_evidence %}<span class="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-300">evidence attached</span>{% endif %}
115
+ {% if item.guard_has_blocking %}<span class="px-2 py-0.5 rounded bg-red-500/15 text-red-300">blocking risk</span>{% endif %}
116
+ </div>
117
+ </div>
118
+ {% else %}
119
+ <p class="text-sm text-slate-500">No protocol tasks yet.</p>
120
+ {% endfor %}
121
+ </div>
122
+ </section>
123
+
124
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
125
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Durable Workflow State</h2>
126
+ <div class="grid grid-cols-3 gap-3 mb-4">
127
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
128
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Open Runs</p>
129
+ <p class="text-xl font-display font-bold text-slate-100">{{ workflow.open_runs or 0 }}</p>
130
+ </div>
131
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
132
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Blocked Runs</p>
133
+ <p class="text-xl font-display font-bold {{ 'text-red-400' if (workflow.blocked_runs or 0) else 'text-slate-100' }}">{{ workflow.blocked_runs or 0 }}</p>
134
+ </div>
135
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
136
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Waiting Approval</p>
137
+ <p class="text-xl font-display font-bold {{ 'text-amber-400' if (workflow.waiting_approval or 0) else 'text-slate-100' }}">{{ workflow.waiting_approval or 0 }}</p>
138
+ </div>
139
+ </div>
140
+ <div class="space-y-3 max-h-[420px] overflow-y-auto pr-1">
141
+ {% for item in snapshot.recent_runs[:10] %}
142
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
143
+ <div class="flex items-center justify-between gap-3 mb-1">
144
+ <p class="text-sm text-slate-100">{{ item.goal }}</p>
145
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">{{ item.status }}</span>
146
+ </div>
147
+ <p class="text-[11px] text-slate-500 font-mono">{{ item.workflow_kind }}{% if item.current_step_key %} • step={{ item.current_step_key }}{% endif %}{% if item.next_action %} • next={{ item.next_action }}{% endif %}</p>
148
+ </div>
149
+ {% else %}
150
+ <p class="text-sm text-slate-500">No workflow runs recorded.</p>
151
+ {% endfor %}
152
+ </div>
153
+ </section>
154
+ </div>
155
+
156
+ <div class="grid grid-cols-2 gap-5">
157
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
158
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Conditioned Learnings</h2>
159
+ <div class="space-y-3 max-h-[420px] overflow-y-auto pr-1">
160
+ {% for item in conditioned[:12] %}
161
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
162
+ <div class="flex items-center justify-between gap-3 mb-1">
163
+ <p class="text-sm text-slate-100">{{ item.title }}</p>
164
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">{{ item.priority or 'normal' }}</span>
165
+ </div>
166
+ <p class="text-xs text-slate-400 break-all">{{ item.applies_to }}</p>
167
+ <p class="text-[11px] text-slate-500 font-mono mt-1">guard_hits={{ item.guard_hits or 0 }} • weight={{ "%.2f"|format(item.weight or 0) }}</p>
168
+ </div>
169
+ {% else %}
170
+ <p class="text-sm text-slate-500">No conditioned learnings active.</p>
171
+ {% endfor %}
172
+ </div>
173
+ </section>
174
+
175
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
176
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Guard Pressure</h2>
177
+ <div class="grid grid-cols-2 gap-3 mb-4">
178
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
179
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Recent Checks</p>
180
+ <p class="text-xl font-display font-bold text-slate-100">{{ snapshot.guard_summary.recent_checks or 0 }}</p>
181
+ </div>
182
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
183
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Blocking Hits</p>
184
+ <p class="text-xl font-display font-bold {{ 'text-red-400' if (snapshot.guard_summary.blocking_hits or 0) else 'text-slate-100' }}">{{ snapshot.guard_summary.blocking_hits or 0 }}</p>
185
+ </div>
186
+ </div>
187
+ <div class="space-y-2">
188
+ {% for area, count in snapshot.guard_summary.areas.items() %}
189
+ <div class="flex items-center justify-between rounded-lg border border-slate-800/60 bg-slate-950/50 px-3 py-2">
190
+ <span class="text-sm text-slate-300">{{ area }}</span>
191
+ <span class="text-xs font-mono text-slate-500">{{ count }} checks</span>
192
+ </div>
193
+ {% else %}
194
+ <p class="text-sm text-slate-500">No guard pressure recorded.</p>
195
+ {% endfor %}
196
+ </div>
197
+ </section>
198
+ </div>
199
+ {% endblock %}
@@ -0,0 +1,246 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Core Rules{% endblock %}
4
+
5
+ {% block page_title %}Core Rules Engine{% endblock %}
6
+ {% block page_subtitle %}<span class="text-xs text-slate-500 ml-2">View and understand system rules</span>{% endblock %}
7
+
8
+ {% block header_actions %}
9
+ <div class="relative">
10
+ <svg class="w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
11
+ <input type="text" id="search-input" placeholder="Search rules..." onkeyup="applyFilters()"
12
+ class="text-xs bg-slate-800 border border-slate-700 rounded-md pl-8 pr-3 py-1.5 text-slate-300 placeholder-slate-500 w-48 focus:outline-none focus:border-nexo-500 focus:w-64 transition-all">
13
+ </div>
14
+ <select id="filter-type" onchange="applyFilters()" class="text-xs bg-slate-800 border border-slate-700 rounded-md px-2 py-1.5 text-slate-300 focus:outline-none focus:border-nexo-500">
15
+ <option value="">All Types</option>
16
+ <option value="blocking">Blocking</option>
17
+ <option value="advisory">Advisory</option>
18
+ </select>
19
+ <select id="filter-importance" onchange="applyFilters()" class="text-xs bg-slate-800 border border-slate-700 rounded-md px-2 py-1.5 text-slate-300 focus:outline-none focus:border-nexo-500">
20
+ <option value="">All Importance</option>
21
+ <option value="5">5 Stars</option>
22
+ <option value="4">4+ Stars</option>
23
+ <option value="3">3+ Stars</option>
24
+ </select>
25
+ <button onclick="loadData()" class="text-xs text-slate-400 hover:text-slate-200 transition-colors px-2 py-1.5 rounded hover:bg-slate-800">
26
+ <svg class="w-3.5 h-3.5 inline -mt-0.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
27
+ Refresh
28
+ </button>
29
+ {% endblock %}
30
+
31
+ {% block content %}
32
+ <!-- Stats row -->
33
+ <div class="grid grid-cols-4 gap-4 mb-6">
34
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
35
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Total Rules</p>
36
+ <p class="text-2xl font-display font-bold text-slate-100" id="stat-total">--</p>
37
+ </div>
38
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
39
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Active</p>
40
+ <p class="text-2xl font-display font-bold text-emerald-400" id="stat-active">--</p>
41
+ </div>
42
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
43
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Blocking</p>
44
+ <p class="text-2xl font-display font-bold text-red-400" id="stat-blocking">--</p>
45
+ </div>
46
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
47
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Categories</p>
48
+ <p class="text-2xl font-display font-bold text-nexo-400" id="stat-categories">--</p>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Importance distribution -->
53
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card mb-6">
54
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-3">Importance Distribution</h2>
55
+ <div class="flex items-end gap-3 h-20" id="importance-chart"></div>
56
+ </div>
57
+
58
+ <!-- Rules by category -->
59
+ <div id="rules-container" class="space-y-4">
60
+ <div class="py-16 text-center text-sm text-slate-500">Loading rules...</div>
61
+ </div>
62
+
63
+ <!-- Empty state -->
64
+ <div id="no-results" class="hidden py-16 text-center">
65
+ <svg class="w-12 h-12 mx-auto text-slate-700 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
66
+ <p class="text-sm text-slate-500">No rules match your search</p>
67
+ </div>
68
+ {% endblock %}
69
+
70
+ {% block scripts %}
71
+ <script>
72
+ let allRules = [];
73
+ let categoriesData = {};
74
+
75
+ function importanceStars(n) {
76
+ const level = n || 1;
77
+ let stars = '';
78
+ for (let i = 1; i <= 5; i++) {
79
+ stars += `<svg class="w-3.5 h-3.5 ${i <= level ? 'text-amber-400' : 'text-slate-700'}" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>`;
80
+ }
81
+ return `<div class="flex items-center gap-0.5">${stars}</div>`;
82
+ }
83
+
84
+ function typeBadge(type) {
85
+ if (type === 'blocking') {
86
+ return '<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase bg-red-500/15 text-red-400 border border-red-500/20"><span class="w-1.5 h-1.5 rounded-full bg-red-400"></span>Blocking</span>';
87
+ }
88
+ return '<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold uppercase bg-blue-500/15 text-blue-400 border border-blue-500/20"><span class="w-1.5 h-1.5 rounded-full bg-blue-400"></span>Advisory</span>';
89
+ }
90
+
91
+ function toggleCategory(btn) {
92
+ const section = btn.closest('.rule-category');
93
+ section.classList.toggle('collapsed');
94
+ const items = section.querySelector('.category-items');
95
+ const chevron = section.querySelector('.cat-chevron');
96
+ if (section.classList.contains('collapsed')) {
97
+ items.style.maxHeight = '0';
98
+ chevron.style.transform = 'rotate(0deg)';
99
+ } else {
100
+ items.style.maxHeight = items.scrollHeight + 'px';
101
+ chevron.style.transform = 'rotate(90deg)';
102
+ }
103
+ }
104
+
105
+ function renderRules(rules, categories) {
106
+ const container = document.getElementById('rules-container');
107
+ const noResults = document.getElementById('no-results');
108
+
109
+ // Group rules by category
110
+ const grouped = {};
111
+ rules.forEach(r => {
112
+ const cat = r.category || 'Uncategorized';
113
+ if (!grouped[cat]) grouped[cat] = [];
114
+ grouped[cat].push(r);
115
+ });
116
+
117
+ const catNames = Object.keys(grouped).sort();
118
+
119
+ if (!catNames.length) {
120
+ container.innerHTML = '';
121
+ noResults.classList.remove('hidden');
122
+ return;
123
+ }
124
+
125
+ noResults.classList.add('hidden');
126
+
127
+ container.innerHTML = catNames.map(cat => {
128
+ const catRules = grouped[cat].sort((a, b) => (b.importance || 0) - (a.importance || 0));
129
+ const blockingCount = catRules.filter(r => r.type === 'blocking').length;
130
+ return `
131
+ <div class="rule-category bg-slate-900/50 border border-slate-800/50 rounded-xl overflow-hidden card">
132
+ <button onclick="toggleCategory(this)"
133
+ class="w-full flex items-center gap-3 px-5 py-4 hover:bg-slate-800/30 transition-colors text-left">
134
+ <svg class="w-4 h-4 cat-chevron text-slate-500 transition-transform flex-shrink-0" style="transform:rotate(90deg)" fill="currentColor" viewBox="0 0 20 20">
135
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/>
136
+ </svg>
137
+ <span class="text-sm font-display font-semibold text-slate-100">${escapeHtml(cat)}</span>
138
+ <span class="text-[10px] font-mono text-slate-500 ml-1">${catRules.length} rule${catRules.length !== 1 ? 's' : ''}</span>
139
+ ${blockingCount > 0 ? `<span class="text-[10px] font-mono text-red-400 ml-auto">${blockingCount} blocking</span>` : ''}
140
+ </button>
141
+ <div class="category-items border-t border-slate-800/30 overflow-hidden transition-all duration-300" style="max-height: 2000px">
142
+ <div class="divide-y divide-slate-800/20">
143
+ ${catRules.map(r => {
144
+ const isBlocking = r.type === 'blocking';
145
+ const borderClass = isBlocking ? 'border-l-2 border-l-red-500/40' : 'border-l-2 border-l-blue-500/20';
146
+ const inactiveClass = r.is_active === false ? 'opacity-50' : '';
147
+ return `
148
+ <div class="px-5 py-3.5 hover:bg-slate-800/20 transition-colors ${borderClass} ${inactiveClass}">
149
+ <div class="flex items-start justify-between gap-4">
150
+ <div class="flex-1 min-w-0">
151
+ <div class="flex items-center gap-2 mb-1.5">
152
+ ${typeBadge(r.type)}
153
+ ${importanceStars(r.importance)}
154
+ ${r.is_active === false ? '<span class="text-[10px] px-1.5 py-0.5 rounded bg-slate-700/50 text-slate-500">inactive</span>' : ''}
155
+ <span class="text-[10px] font-mono text-slate-600 ml-auto">#${r.id || '--'}</span>
156
+ </div>
157
+ <p class="text-sm text-slate-200 leading-relaxed">${escapeHtml(r.rule || '--')}</p>
158
+ ${r.why ? `<p class="text-xs text-slate-500 mt-1 leading-relaxed">${escapeHtml(r.why)}</p>` : ''}
159
+ </div>
160
+ <div class="flex-shrink-0 mt-1">
161
+ <div class="w-8 h-4 rounded-full ${r.is_active !== false ? 'bg-emerald-500/30' : 'bg-slate-700/50'} relative">
162
+ <div class="absolute top-0.5 ${r.is_active !== false ? 'right-0.5 bg-emerald-400' : 'left-0.5 bg-slate-500'} w-3 h-3 rounded-full transition-all"></div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ `;
168
+ }).join('')}
169
+ </div>
170
+ </div>
171
+ </div>
172
+ `;
173
+ }).join('');
174
+ }
175
+
176
+ function renderImportanceChart(rules) {
177
+ const chart = document.getElementById('importance-chart');
178
+ const counts = [0, 0, 0, 0, 0];
179
+ rules.forEach(r => {
180
+ const imp = Math.min(Math.max((r.importance || 1) - 1, 0), 4);
181
+ counts[imp]++;
182
+ });
183
+ const max = Math.max(...counts, 1);
184
+ const labels = ['1', '2', '3', '4', '5'];
185
+ const colors = ['bg-slate-600', 'bg-blue-500/60', 'bg-nexo-500/60', 'bg-amber-500/60', 'bg-red-500/60'];
186
+
187
+ chart.innerHTML = counts.map((count, i) => {
188
+ const height = (count / max * 100).toFixed(0);
189
+ return `
190
+ <div class="flex-1 flex flex-col items-center gap-1">
191
+ <span class="text-[10px] font-mono text-slate-400">${count}</span>
192
+ <div class="w-full rounded-t ${colors[i]} transition-all duration-500 hover:opacity-80" style="height: ${Math.max(height, 6)}%"></div>
193
+ <div class="flex items-center gap-0.5">
194
+ <svg class="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
195
+ <span class="text-[10px] text-slate-500">${labels[i]}</span>
196
+ </div>
197
+ </div>
198
+ `;
199
+ }).join('');
200
+ }
201
+
202
+ function applyFilters() {
203
+ const search = (document.getElementById('search-input').value || '').toLowerCase().trim();
204
+ const typeFilter = document.getElementById('filter-type').value;
205
+ const impFilter = parseInt(document.getElementById('filter-importance').value) || 0;
206
+
207
+ let filtered = allRules;
208
+
209
+ if (search) {
210
+ filtered = filtered.filter(r =>
211
+ (r.rule || '').toLowerCase().includes(search) ||
212
+ (r.category || '').toLowerCase().includes(search) ||
213
+ (r.why || '').toLowerCase().includes(search)
214
+ );
215
+ }
216
+ if (typeFilter) {
217
+ filtered = filtered.filter(r => r.type === typeFilter);
218
+ }
219
+ if (impFilter) {
220
+ filtered = filtered.filter(r => (r.importance || 1) >= impFilter);
221
+ }
222
+
223
+ renderRules(filtered, categoriesData);
224
+ }
225
+
226
+ async function loadData() {
227
+ const data = await fetchJSON('/api/rules');
228
+ if (!data) return;
229
+
230
+ allRules = data.rules || [];
231
+ categoriesData = data.categories || {};
232
+
233
+ // Stats
234
+ document.getElementById('stat-total').textContent = formatNumber(data.total || allRules.length);
235
+ document.getElementById('stat-active').textContent = formatNumber(data.active || allRules.filter(r => r.is_active !== false).length);
236
+ document.getElementById('stat-blocking').textContent = formatNumber(allRules.filter(r => r.type === 'blocking').length);
237
+ document.getElementById('stat-categories').textContent = Object.keys(categoriesData).length || [...new Set(allRules.map(r => r.category))].filter(Boolean).length;
238
+
239
+ renderImportanceChart(allRules);
240
+ applyFilters();
241
+ }
242
+
243
+ loadData();
244
+ setInterval(loadData, 60000);
245
+ </script>
246
+ {% endblock %}