@jungjaehoon/mama-os 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent/agent-loop.d.ts +1 -8
  3. package/dist/agent/agent-loop.d.ts.map +1 -1
  4. package/dist/agent/agent-loop.js +44 -159
  5. package/dist/agent/agent-loop.js.map +1 -1
  6. package/dist/agent/claude-cli-wrapper.d.ts +6 -0
  7. package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
  8. package/dist/agent/claude-cli-wrapper.js +6 -0
  9. package/dist/agent/claude-cli-wrapper.js.map +1 -1
  10. package/dist/agent/codex-mcp-process.d.ts +85 -0
  11. package/dist/agent/codex-mcp-process.d.ts.map +1 -0
  12. package/dist/agent/codex-mcp-process.js +357 -0
  13. package/dist/agent/codex-mcp-process.js.map +1 -0
  14. package/dist/agent/session-pool.d.ts +17 -2
  15. package/dist/agent/session-pool.d.ts.map +1 -1
  16. package/dist/agent/session-pool.js +51 -26
  17. package/dist/agent/session-pool.js.map +1 -1
  18. package/dist/agent/types.d.ts +9 -24
  19. package/dist/agent/types.d.ts.map +1 -1
  20. package/dist/agent/types.js.map +1 -1
  21. package/dist/api/graph-api.d.ts.map +1 -1
  22. package/dist/api/graph-api.js +133 -45
  23. package/dist/api/graph-api.js.map +1 -1
  24. package/dist/cli/commands/init.d.ts +1 -1
  25. package/dist/cli/commands/init.d.ts.map +1 -1
  26. package/dist/cli/commands/init.js +14 -25
  27. package/dist/cli/commands/init.js.map +1 -1
  28. package/dist/cli/commands/run.d.ts.map +1 -1
  29. package/dist/cli/commands/run.js +3 -10
  30. package/dist/cli/commands/run.js.map +1 -1
  31. package/dist/cli/commands/start.d.ts.map +1 -1
  32. package/dist/cli/commands/start.js +143 -54
  33. package/dist/cli/commands/start.js.map +1 -1
  34. package/dist/cli/commands/status.d.ts.map +1 -1
  35. package/dist/cli/commands/status.js +2 -7
  36. package/dist/cli/commands/status.js.map +1 -1
  37. package/dist/cli/config/config-manager.d.ts.map +1 -1
  38. package/dist/cli/config/config-manager.js +9 -17
  39. package/dist/cli/config/config-manager.js.map +1 -1
  40. package/dist/cli/config/types.d.ts +19 -25
  41. package/dist/cli/config/types.d.ts.map +1 -1
  42. package/dist/cli/config/types.js.map +1 -1
  43. package/dist/cli/index.js +2 -2
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/gateways/context-injector.d.ts.map +1 -1
  46. package/dist/gateways/context-injector.js +6 -3
  47. package/dist/gateways/context-injector.js.map +1 -1
  48. package/dist/gateways/discord.d.ts +4 -0
  49. package/dist/gateways/discord.d.ts.map +1 -1
  50. package/dist/gateways/discord.js +39 -16
  51. package/dist/gateways/discord.js.map +1 -1
  52. package/dist/gateways/message-router.d.ts +6 -1
  53. package/dist/gateways/message-router.d.ts.map +1 -1
  54. package/dist/gateways/message-router.js +92 -7
  55. package/dist/gateways/message-router.js.map +1 -1
  56. package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
  57. package/dist/multi-agent/agent-process-manager.js +36 -9
  58. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  59. package/dist/multi-agent/runtime-process.d.ts +4 -4
  60. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  61. package/dist/multi-agent/runtime-process.js +9 -20
  62. package/dist/multi-agent/runtime-process.js.map +1 -1
  63. package/dist/multi-agent/types.d.ts +13 -8
  64. package/dist/multi-agent/types.d.ts.map +1 -1
  65. package/dist/multi-agent/types.js.map +1 -1
  66. package/dist/setup/setup-prompt.d.ts +1 -1
  67. package/dist/setup/setup-prompt.d.ts.map +1 -1
  68. package/dist/setup/setup-prompt.js +19 -0
  69. package/dist/setup/setup-prompt.js.map +1 -1
  70. package/dist/setup/setup-server.d.ts.map +1 -1
  71. package/dist/setup/setup-server.js +39 -16
  72. package/dist/setup/setup-server.js.map +1 -1
  73. package/dist/skills/skill-registry.d.ts.map +1 -1
  74. package/dist/skills/skill-registry.js +5 -2
  75. package/dist/skills/skill-registry.js.map +1 -1
  76. package/package.json +5 -3
  77. package/public/setup.html +12 -1
  78. package/public/viewer/js/modules/chat.js +1760 -1976
  79. package/public/viewer/js/modules/dashboard.js +613 -695
  80. package/public/viewer/js/modules/graph.js +857 -970
  81. package/public/viewer/js/modules/memory.js +357 -312
  82. package/public/viewer/js/modules/settings.js +1009 -1026
  83. package/public/viewer/js/modules/skills.js +336 -355
  84. package/public/viewer/js/utils/api.js +255 -255
  85. package/public/viewer/js/utils/debug-logger.js +20 -26
  86. package/public/viewer/js/utils/dom.js +73 -60
  87. package/public/viewer/js/utils/format.js +182 -228
  88. package/public/viewer/js/utils/markdown.js +40 -0
  89. package/public/viewer/src/modules/chat.ts +2258 -0
  90. package/public/viewer/src/modules/dashboard.ts +1052 -0
  91. package/public/viewer/src/modules/graph.ts +1080 -0
  92. package/public/viewer/src/modules/memory.ts +453 -0
  93. package/public/viewer/src/modules/settings.ts +1398 -0
  94. package/public/viewer/src/modules/skills.ts +457 -0
  95. package/public/viewer/src/types/global.d.ts +168 -0
  96. package/public/viewer/src/utils/api.ts +650 -0
  97. package/public/viewer/src/utils/debug-logger.ts +36 -0
  98. package/public/viewer/src/utils/dom.ts +138 -0
  99. package/public/viewer/src/utils/format.ts +331 -0
  100. package/public/viewer/src/utils/markdown.ts +46 -0
  101. package/public/viewer/tsconfig.viewer.json +18 -0
  102. package/public/viewer/viewer.html +214 -311
  103. package/dist/agent/codex-cli-wrapper.d.ts +0 -85
  104. package/dist/agent/codex-cli-wrapper.d.ts.map +0 -1
  105. package/dist/agent/codex-cli-wrapper.js +0 -295
  106. package/dist/agent/codex-cli-wrapper.js.map +0 -1
@@ -9,210 +9,200 @@
9
9
  * - Agent configuration display
10
10
  * - Top topics
11
11
  */
12
-
13
12
  /* eslint-env browser */
14
-
15
- import { escapeHtml } from '../utils/dom.js';
13
+ import { escapeAttr, escapeHtml, getElementByIdOrNull, getErrorMessage } from '../utils/dom.js';
16
14
  import { formatModelName } from '../utils/format.js';
17
- import { API } from '../utils/api.js';
15
+ import { API, } from '../utils/api.js';
18
16
  import { DebugLogger } from '../utils/debug-logger.js';
19
-
20
17
  const logger = new DebugLogger('Dashboard');
21
-
22
18
  /**
23
19
  * Dashboard Module Class
24
20
  */
25
21
  export class DashboardModule {
26
- constructor() {
27
- this.data = null;
28
- this.updateInterval = null;
29
- this.initialized = false;
30
- this.onCronClick = null;
31
- this.mcpServers = [];
32
- }
33
-
34
- /**
35
- * Initialize dashboard
36
- */
37
- async init() {
38
- if (this.initialized) {
39
- return;
22
+ data = null;
23
+ updateInterval = null;
24
+ initialized = false;
25
+ onCronClick = null;
26
+ mcpServers = [];
27
+ multiAgentData = { enabled: false, agents: [] };
28
+ delegationsData = { delegations: [], count: 0 };
29
+ cronData = null;
30
+ tokenData = null;
31
+ constructor() {
32
+ this.data = null;
33
+ this.updateInterval = null;
34
+ this.initialized = false;
35
+ this.onCronClick = null;
36
+ this.mcpServers = [];
40
37
  }
41
- this.initialized = true;
42
-
43
- // Event delegation for cron job buttons
44
- this.onCronClick = (e) => {
45
- const btn = e.target.closest('[data-cron-id]');
46
- if (btn) {
47
- const jobId = btn.getAttribute('data-cron-id');
48
- this.runCronJob(jobId);
49
- }
50
- };
51
- document.addEventListener('click', this.onCronClick);
52
-
53
- await this.loadStatus();
54
-
55
- // Auto-refresh every 30 seconds
56
- this.updateInterval = setInterval(() => this.loadStatus(), 30000);
57
- }
58
-
59
- /**
60
- * Load dashboard status from API
61
- */
62
- async loadStatus() {
63
- try {
64
- const response = await fetch('/api/dashboard/status');
65
- if (!response.ok) {
66
- throw new Error(`HTTP ${response.status}`);
67
- }
68
-
69
- this.data = await response.json();
70
-
71
- // Load multi-agent status (Sprint 3 F2)
72
- try {
73
- const multiAgentResponse = await fetch('/api/multi-agent/status');
74
- if (multiAgentResponse.ok) {
75
- this.multiAgentData = await multiAgentResponse.json();
76
- } else {
77
- this.multiAgentData = { enabled: false, agents: [] };
78
- }
79
- } catch (e) {
80
- logger.warn('[Dashboard] Multi-agent status unavailable:', e);
81
- this.multiAgentData = { enabled: false, agents: [] };
82
- }
83
-
84
- // Load delegations (F4 endpoint)
85
- try {
86
- const delegationsResponse = await fetch('/api/multi-agent/delegations?limit=10');
87
- if (delegationsResponse.ok) {
88
- this.delegationsData = await delegationsResponse.json();
89
- } else {
90
- this.delegationsData = { delegations: [], count: 0 };
91
- }
92
- } catch (e) {
93
- logger.warn('[Dashboard] Delegations unavailable:', e);
94
- this.delegationsData = { delegations: [], count: 0 };
95
- }
96
-
97
- // Load cron jobs
98
- try {
99
- this.cronData = await API.getCronJobs();
100
- } catch (e) {
101
- logger.warn('[Dashboard] Cron data unavailable:', e);
102
- this.cronData = null;
103
- }
104
-
105
- // Load token summary
106
- try {
107
- const [summary, byAgent] = await Promise.all([
108
- API.getTokenSummary(),
109
- API.getTokensByAgent(),
110
- ]);
111
- this.tokenData = { summary, byAgent };
112
- } catch (e) {
113
- logger.warn('[Dashboard] Token data unavailable:', e);
114
- this.tokenData = null;
115
- }
116
-
117
- // Load MCP servers
118
- await this.loadMCPServers();
119
-
120
- this.render();
121
- this.setStatus(`Last updated: ${new Date().toLocaleTimeString()}`);
122
- } catch (error) {
123
- logger.error('[Dashboard] Load error:', error);
124
- this.setStatus(`Error: ${error.message}`, 'error');
125
- }
126
- }
127
-
128
- /**
129
- * Load MCP servers from API
130
- */
131
- async loadMCPServers() {
132
- try {
133
- const response = await fetch('/api/mcp-servers');
134
- if (response.ok) {
135
- const data = await response.json();
136
- this.mcpServers = data.servers || [];
137
- this.renderMCPServers();
138
- }
139
- } catch (error) {
140
- logger.error('Failed to load MCP servers:', error);
38
+ /**
39
+ * Initialize dashboard
40
+ */
41
+ async init() {
42
+ if (this.initialized) {
43
+ return;
44
+ }
45
+ this.initialized = true;
46
+ // Event delegation for dashboard actions
47
+ this.onCronClick = (e) => {
48
+ const target = e.target;
49
+ if (!target) {
50
+ return;
51
+ }
52
+ const cronButton = target.closest('[data-action="run-cron"]');
53
+ if (cronButton) {
54
+ const jobId = cronButton.getAttribute('data-cron-id');
55
+ if (jobId) {
56
+ this.runCronJob(jobId);
57
+ }
58
+ return;
59
+ }
60
+ const settingsLink = target.closest('[data-action="open-settings"]');
61
+ if (settingsLink) {
62
+ const settingsTab = document.querySelector('[data-tab="settings"]');
63
+ if (settingsTab) {
64
+ settingsTab.click();
65
+ }
66
+ }
67
+ };
68
+ document.addEventListener('click', this.onCronClick);
69
+ await this.loadStatus();
70
+ // Auto-refresh every 30 seconds
71
+ this.updateInterval = setInterval(() => this.loadStatus(), 30000);
141
72
  }
142
- }
143
-
144
- /**
145
- * Render all dashboard sections
146
- */
147
- render() {
148
- if (!this.data) {
149
- return;
73
+ /**
74
+ * Load dashboard status from API
75
+ */
76
+ async loadStatus() {
77
+ try {
78
+ this.data = await API.get('/api/dashboard/status');
79
+ // Load multi-agent status (Sprint 3 F2)
80
+ try {
81
+ this.multiAgentData = await API.get('/api/multi-agent/status');
82
+ }
83
+ catch (e) {
84
+ logger.warn('[Dashboard] Multi-agent status unavailable:', e);
85
+ this.multiAgentData = { enabled: false, agents: [] };
86
+ }
87
+ // Load delegations (F4 endpoint)
88
+ try {
89
+ this.delegationsData = await API.get('/api/multi-agent/delegations?limit=10');
90
+ }
91
+ catch (e) {
92
+ logger.warn('[Dashboard] Delegations unavailable:', e);
93
+ this.delegationsData = { delegations: [], count: 0 };
94
+ }
95
+ // Load cron jobs
96
+ try {
97
+ this.cronData = await API.getCronJobs();
98
+ }
99
+ catch (e) {
100
+ logger.warn('[Dashboard] Cron data unavailable:', e);
101
+ this.cronData = null;
102
+ }
103
+ // Load token summary
104
+ try {
105
+ const [summary, byAgent] = await Promise.all([
106
+ API.getTokenSummary(),
107
+ API.getTokensByAgent(),
108
+ ]);
109
+ this.tokenData = { summary, byAgent };
110
+ }
111
+ catch (e) {
112
+ logger.warn('[Dashboard] Token data unavailable:', e);
113
+ this.tokenData = null;
114
+ }
115
+ // Load MCP servers
116
+ await this.loadMCPServers();
117
+ this.render();
118
+ this.setStatus(`Last updated: ${new Date().toLocaleTimeString()}`);
119
+ }
120
+ catch (error) {
121
+ logger.error('[Dashboard] Load error:', error);
122
+ this.setStatus(`Error: ${getErrorMessage(error)}`, 'error');
123
+ }
150
124
  }
151
-
152
- this.renderGateways();
153
- this.renderMCPServers();
154
- this.renderSessions();
155
- this.renderAgentSwarm();
156
- this.renderMemoryStats();
157
- this.renderAgentConfig();
158
- this.renderCronJobs();
159
- this.renderTokenSummary();
160
- this.renderTopTopics();
161
- }
162
-
163
- /**
164
- * Render gateway status cards
165
- */
166
- renderGateways() {
167
- const container = document.getElementById('dashboard-gateways');
168
- if (!container || !this.data.gateways) {
169
- return;
125
+ /**
126
+ * Load MCP servers from API
127
+ */
128
+ async loadMCPServers() {
129
+ try {
130
+ const data = await API.get('/api/mcp-servers');
131
+ if (data && 'servers' in data) {
132
+ this.mcpServers = data.servers || [];
133
+ this.renderMCPServers();
134
+ }
135
+ }
136
+ catch (error) {
137
+ logger.error('Failed to load MCP servers:', error);
138
+ }
170
139
  }
171
-
172
- const gateways = [
173
- { key: 'discord', name: 'Discord', icon: '💬', color: 'indigo' },
174
- { key: 'slack', name: 'Slack', icon: '📱', color: 'green' },
175
- { key: 'telegram', name: 'Telegram', icon: '✈️', color: 'blue' },
176
- { key: 'chatwork', name: 'Chatwork', icon: '💼', color: 'orange' },
177
- ];
178
-
179
- // Count active bots
180
- const enabledCount = gateways.filter((gw) => this.data.gateways[gw.key]?.enabled).length;
181
- const configuredCount = gateways.filter((gw) => this.data.gateways[gw.key]?.configured).length;
182
-
183
- // Update header with bot count
184
- const header = container.previousElementSibling;
185
- if (header && header.tagName === 'H2') {
186
- header.innerHTML = `Gateway Status <span class="text-sm font-normal text-gray-500">(${enabledCount}/${configuredCount} active)</span>`;
140
+ /**
141
+ * Render all dashboard sections
142
+ */
143
+ render() {
144
+ if (!this.data) {
145
+ return;
146
+ }
147
+ this.renderGateways();
148
+ this.renderMCPServers();
149
+ this.renderSessions();
150
+ this.renderAgentSwarm();
151
+ this.renderMemoryStats();
152
+ this.renderAgentConfig();
153
+ this.renderCronJobs();
154
+ this.renderTokenSummary();
155
+ this.renderTopTopics();
187
156
  }
188
-
189
- const html = gateways
190
- .map((gw) => {
191
- const status = this.data.gateways[gw.key] || {};
192
- const isConfigured = status.configured;
193
- const isEnabled = status.enabled;
194
-
195
- const statusBadge = isConfigured
196
- ? isEnabled
197
- ? `<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400">Enabled</span>`
198
- : `<span class="text-xs px-2 py-0.5 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400">Disabled</span>`
199
- : `<span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500">Not Configured</span>`;
200
-
201
- // Get channel info based on gateway type
202
- let channelInfo = '';
203
- if (isConfigured) {
204
- if (gw.key === 'discord' && status.channel) {
205
- channelInfo = `<span class="text-[10px] bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded">#${escapeHtml(status.channel)}</span>`;
206
- } else if (gw.key === 'telegram' && status.chats?.length > 0) {
207
- channelInfo = `<span class="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded">${status.chats.length} chat(s)</span>`;
208
- } else if (gw.key === 'slack' && status.channel) {
209
- channelInfo = `<span class="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded">#${escapeHtml(status.channel)}</span>`;
210
- } else if (gw.key === 'chatwork' && status.rooms?.length > 0) {
211
- channelInfo = `<span class="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded">${status.rooms.length} room(s)</span>`;
212
- }
213
- }
214
-
215
- return `
157
+ /**
158
+ * Render gateway status cards
159
+ */
160
+ renderGateways() {
161
+ const container = getElementByIdOrNull('dashboard-gateways');
162
+ if (!container || !this.data.gateways) {
163
+ return;
164
+ }
165
+ const gateways = [
166
+ { key: 'discord', name: 'Discord', icon: '💬', color: 'indigo' },
167
+ { key: 'slack', name: 'Slack', icon: '📱', color: 'green' },
168
+ { key: 'telegram', name: 'Telegram', icon: '✈️', color: 'blue' },
169
+ { key: 'chatwork', name: 'Chatwork', icon: '💼', color: 'orange' },
170
+ ];
171
+ // Count active bots
172
+ const enabledCount = gateways.filter((gw) => this.data.gateways[gw.key]?.enabled).length;
173
+ const configuredCount = gateways.filter((gw) => this.data.gateways[gw.key]?.configured).length;
174
+ // Update header with bot count
175
+ const header = container.previousElementSibling;
176
+ if (header && header.tagName === 'H2') {
177
+ header.innerHTML = `Gateway Status <span class="text-sm font-normal text-gray-500">(${enabledCount}/${configuredCount} active)</span>`;
178
+ }
179
+ const html = gateways
180
+ .map((gw) => {
181
+ const status = this.data.gateways[gw.key] || {};
182
+ const isConfigured = status.configured;
183
+ const isEnabled = status.enabled;
184
+ const statusBadge = isConfigured
185
+ ? isEnabled
186
+ ? `<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400">Enabled</span>`
187
+ : `<span class="text-xs px-2 py-0.5 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400">Disabled</span>`
188
+ : `<span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500">Not Configured</span>`;
189
+ // Get channel info based on gateway type
190
+ let channelInfo = '';
191
+ if (isConfigured) {
192
+ if (gw.key === 'discord' && status.channel) {
193
+ channelInfo = `<span class="text-[10px] bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded">#${escapeHtml(status.channel)}</span>`;
194
+ }
195
+ else if (gw.key === 'telegram' && status.chats?.length > 0) {
196
+ channelInfo = `<span class="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded">${status.chats.length} chat(s)</span>`;
197
+ }
198
+ else if (gw.key === 'slack' && status.channel) {
199
+ channelInfo = `<span class="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded">#${escapeHtml(status.channel)}</span>`;
200
+ }
201
+ else if (gw.key === 'chatwork' && status.rooms?.length > 0) {
202
+ channelInfo = `<span class="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded">${status.rooms.length} room(s)</span>`;
203
+ }
204
+ }
205
+ return `
216
206
  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow">
217
207
  <div class="flex items-center justify-between mb-2">
218
208
  <span class="text-2xl">${gw.icon}</span>
@@ -227,54 +217,48 @@ export class DashboardModule {
227
217
  </div>
228
218
  </div>
229
219
  `;
230
- })
231
- .join('');
232
-
233
- container.innerHTML = html;
234
- }
235
-
236
- /**
237
- * Render MCP servers
238
- */
239
- renderMCPServers() {
240
- const container = document.getElementById('dashboard-mcp');
241
- if (!container) {
242
- return;
220
+ })
221
+ .join('');
222
+ container.innerHTML = html;
243
223
  }
244
-
245
- if (this.mcpServers.length === 0) {
246
- container.innerHTML = `
224
+ /**
225
+ * Render MCP servers
226
+ */
227
+ renderMCPServers() {
228
+ const container = getElementByIdOrNull('dashboard-mcp');
229
+ if (!container) {
230
+ return;
231
+ }
232
+ if (this.mcpServers.length === 0) {
233
+ container.innerHTML = `
247
234
  <p class="text-gray-500 text-sm col-span-full py-4 text-center">
248
235
  No MCP servers configured
249
236
  </p>
250
237
  `;
251
- return;
252
- }
253
-
254
- const icons = {
255
- 'brave-devtools': '🌐',
256
- 'brave-search': '🔍',
257
- mama: '🧠',
258
- slack: '💬',
259
- notion: '📝',
260
- linear: '📊',
261
- asana: '',
262
- atlassian: '🔷',
263
- ms365: '📧',
264
- monday: '📅',
265
- clickup: '✓',
266
- };
267
-
268
- const html = this.mcpServers
269
- .map((server) => {
270
- const icon = icons[server.name] || '🔌';
271
- const isHttp = server.type === 'http';
272
- const statusBadge = isHttp
273
- ? 'bg-yellow-100 text-yellow-700'
274
- : 'bg-green-100 text-green-700';
275
- const statusText = isHttp ? 'OAuth' : 'Ready';
276
-
277
- return `
238
+ return;
239
+ }
240
+ const icons = {
241
+ 'brave-devtools': '🌐',
242
+ 'brave-search': '🔍',
243
+ mama: '🧠',
244
+ slack: '💬',
245
+ notion: '📝',
246
+ linear: '📊',
247
+ asana: '',
248
+ atlassian: '🔷',
249
+ ms365: '📧',
250
+ monday: '📅',
251
+ clickup: '',
252
+ };
253
+ const html = this.mcpServers
254
+ .map((server) => {
255
+ const icon = icons[server.name] || '🔌';
256
+ const isHttp = server.type === 'http';
257
+ const statusBadge = isHttp
258
+ ? 'bg-yellow-100 text-yellow-700'
259
+ : 'bg-green-100 text-green-700';
260
+ const statusText = isHttp ? 'OAuth' : 'Ready';
261
+ return `
278
262
  <div class="bg-white border border-gray-200 rounded-lg p-2.5 hover:shadow-md transition-shadow">
279
263
  <div class="flex items-center justify-between mb-1">
280
264
  <div class="flex items-center gap-2">
@@ -286,88 +270,83 @@ export class DashboardModule {
286
270
  <p class="text-[10px] text-gray-500 truncate">${escapeHtml(server.type === 'http' ? server.url : server.command)}</p>
287
271
  </div>
288
272
  `;
289
- })
290
- .join('');
291
-
292
- container.innerHTML = html;
293
- }
294
-
295
- /**
296
- * Render session statistics
297
- */
298
- renderSessions() {
299
- const container = document.getElementById('dashboard-sessions');
300
- if (!container) {
301
- return;
273
+ })
274
+ .join('');
275
+ container.innerHTML = html;
302
276
  }
303
-
304
- const sessions = this.data.sessions || { total: 0, bySource: {}, channels: [] };
305
-
306
- if (sessions.total === 0) {
307
- container.innerHTML = `
277
+ /**
278
+ * Render session statistics
279
+ */
280
+ renderSessions() {
281
+ const container = getElementByIdOrNull('dashboard-sessions');
282
+ if (!container) {
283
+ return;
284
+ }
285
+ const sessions = this.data.sessions || { total: 0, bySource: {}, channels: [] };
286
+ if (sessions.total === 0) {
287
+ container.innerHTML = `
308
288
  <p class="text-gray-500 dark:text-gray-400 text-sm text-center py-4">
309
289
  No active sessions yet. Start chatting to create sessions.
310
290
  </p>
311
291
  `;
312
- return;
313
- }
314
-
315
- // Source icons and labels
316
- const sourceInfo = {
317
- discord: { icon: '🎮', label: 'Discord', color: 'bg-indigo-100 text-indigo-700' },
318
- telegram: { icon: '✈️', label: 'Telegram', color: 'bg-sky-100 text-sky-700' },
319
- slack: { icon: '📱', label: 'Slack', color: 'bg-purple-100 text-purple-700' },
320
- chatwork: { icon: '💼', label: 'Chatwork', color: 'bg-green-100 text-green-700' },
321
- viewer: { icon: '🖥️', label: 'OS Viewer', color: 'bg-gray-100 text-gray-700' },
322
- mobile: { icon: '📲', label: 'Mobile', color: 'bg-orange-100 text-orange-700' },
323
- };
324
-
325
- // Build source summary
326
- const sourceSummary = Object.entries(sessions.bySource)
327
- .map(([source, count]) => {
328
- const info = sourceInfo[source] || {
329
- icon: '📝',
330
- label: source,
331
- color: 'bg-gray-100 text-gray-700',
292
+ return;
293
+ }
294
+ // Source icons and labels
295
+ const sourceInfo = {
296
+ discord: { icon: '🎮', label: 'Discord', color: 'bg-indigo-100 text-indigo-700' },
297
+ telegram: { icon: '✈️', label: 'Telegram', color: 'bg-sky-100 text-sky-700' },
298
+ slack: { icon: '📱', label: 'Slack', color: 'bg-purple-100 text-purple-700' },
299
+ chatwork: { icon: '💼', label: 'Chatwork', color: 'bg-green-100 text-green-700' },
300
+ viewer: { icon: '🖥️', label: 'OS Viewer', color: 'bg-gray-100 text-gray-700' },
301
+ mobile: { icon: '📲', label: 'Mobile', color: 'bg-orange-100 text-orange-700' },
332
302
  };
333
- return `<span class="inline-flex items-center gap-1 ${info.color} px-2 py-1 rounded text-xs font-medium">
334
- ${info.icon} ${info.label}: ${count}
303
+ // Build source summary
304
+ const sourceSummary = Object.entries(sessions.bySource)
305
+ .map(([source, count]) => {
306
+ const info = sourceInfo[source] || {
307
+ icon: '📝',
308
+ label: source,
309
+ color: 'bg-gray-100 text-gray-700',
310
+ };
311
+ const safeLabel = escapeHtml(info.label);
312
+ return `<span class="inline-flex items-center gap-1 ${info.color} px-2 py-1 rounded text-xs font-medium">
313
+ ${info.icon} ${safeLabel}: ${count}
335
314
  </span>`;
336
- })
337
- .join('');
338
-
339
- // Build recent channels list
340
- const channelList = sessions.channels
341
- .slice(0, 5)
342
- .map((ch) => {
343
- const info = sourceInfo[ch.source] || {
344
- icon: '📝',
345
- label: ch.source,
346
- color: 'bg-gray-100 text-gray-700',
347
- };
348
- const lastActive = this.formatRelativeTime(ch.lastActive);
349
-
350
- // Use channel name if available, otherwise use meaningful fallbacks
351
- let channelDisplay;
352
- if (ch.channelName) {
353
- // Show channel name (already human-readable)
354
- channelDisplay =
355
- ch.channelName.length > 25 ? ch.channelName.slice(0, 22) + '...' : ch.channelName;
356
- } else if (ch.source === 'viewer' || ch.channelId === 'mama_os_main') {
357
- // OS Viewer - shared channel
358
- channelDisplay = 'MAMA OS';
359
- } else if (ch.source === 'mobile') {
360
- // Mobile app - show user-friendly name
361
- channelDisplay = 'Mobile App';
362
- } else {
363
- // Fallback: truncate channel ID (Discord channels before update)
364
- channelDisplay =
365
- ch.channelId.length > 12
366
- ? ch.channelId.slice(0, 6) + '...' + ch.channelId.slice(-4)
367
- : ch.channelId;
368
- }
369
-
370
- return `
315
+ })
316
+ .join('');
317
+ // Build recent channels list
318
+ const channelList = sessions.channels
319
+ .slice(0, 5)
320
+ .map((ch) => {
321
+ const info = sourceInfo[ch.source] || {
322
+ icon: '📝',
323
+ label: ch.source,
324
+ color: 'bg-gray-100 text-gray-700',
325
+ };
326
+ const lastActive = this.formatRelativeTime(ch.lastActive);
327
+ // Use channel name if available, otherwise use meaningful fallbacks
328
+ let channelDisplay;
329
+ if (ch.channelName) {
330
+ // Show channel name (already human-readable)
331
+ channelDisplay =
332
+ ch.channelName.length > 25 ? ch.channelName.slice(0, 22) + '...' : ch.channelName;
333
+ }
334
+ else if (ch.source === 'viewer' || ch.channelId === 'mama_os_main') {
335
+ // OS Viewer - shared channel
336
+ channelDisplay = 'MAMA OS';
337
+ }
338
+ else if (ch.source === 'mobile') {
339
+ // Mobile app - show user-friendly name
340
+ channelDisplay = 'Mobile App';
341
+ }
342
+ else {
343
+ // Fallback: truncate channel ID (Discord channels before update)
344
+ channelDisplay =
345
+ ch.channelId.length > 12
346
+ ? ch.channelId.slice(0, 6) + '...' + ch.channelId.slice(-4)
347
+ : ch.channelId;
348
+ }
349
+ return `
371
350
  <div class="flex items-center justify-between py-1.5 border-b border-gray-100 dark:border-gray-700 last:border-0">
372
351
  <div class="flex items-center gap-2">
373
352
  <span class="${info.color} px-1.5 py-0.5 rounded text-[10px] font-medium" title="${escapeHtml(info.label)}">${info.icon}</span>
@@ -379,10 +358,9 @@ export class DashboardModule {
379
358
  </div>
380
359
  </div>
381
360
  `;
382
- })
383
- .join('');
384
-
385
- container.innerHTML = `
361
+ })
362
+ .join('');
363
+ container.innerHTML = `
386
364
  <div class="mb-3">
387
365
  <div class="flex items-center justify-between mb-2">
388
366
  <span class="text-sm font-medium text-gray-900 dark:text-gray-100">Sessions by Platform</span>
@@ -397,81 +375,69 @@ export class DashboardModule {
397
375
  ${channelList || '<p class="text-xs text-gray-400">No recent activity</p>'}
398
376
  </div>
399
377
  `;
400
- }
401
-
402
- /**
403
- * Format relative time (e.g., "2h ago", "3d ago")
404
- */
405
- formatRelativeTime(timestamp) {
406
- if (!timestamp) {
407
- return 'Never';
408
378
  }
409
-
410
- const now = Date.now();
411
- const diff = now - timestamp;
412
- const minutes = Math.floor(diff / 60000);
413
- const hours = Math.floor(diff / 3600000);
414
- const days = Math.floor(diff / 86400000);
415
-
416
- if (minutes < 1) {
417
- return 'Just now';
418
- }
419
- if (minutes < 60) {
420
- return `${minutes}m ago`;
421
- }
422
- if (hours < 24) {
423
- return `${hours}h ago`;
424
- }
425
- return `${days}d ago`;
426
- }
427
-
428
- /**
429
- * Render memory statistics
430
- */
431
- renderMemoryStats() {
432
- const container = document.getElementById('dashboard-memory');
433
- if (!container || !this.data.memory) {
434
- return;
379
+ /**
380
+ * Format relative time (e.g., "2h ago", "3d ago")
381
+ */
382
+ formatRelativeTime(timestamp) {
383
+ if (timestamp === undefined || timestamp === null || timestamp === '') {
384
+ return 'Never';
385
+ }
386
+ const now = Date.now();
387
+ const target = typeof timestamp === 'number' ? timestamp : new Date(timestamp).getTime();
388
+ const diff = now - target;
389
+ const minutes = Math.floor(diff / 60000);
390
+ const hours = Math.floor(diff / 3600000);
391
+ const days = Math.floor(diff / 86400000);
392
+ if (minutes < 1) {
393
+ return 'Just now';
394
+ }
395
+ if (minutes < 60) {
396
+ return `${minutes}m ago`;
397
+ }
398
+ if (hours < 24) {
399
+ return `${hours}h ago`;
400
+ }
401
+ return `${days}d ago`;
435
402
  }
436
-
437
- const memory = this.data.memory;
438
-
439
- const stats = [
440
- { label: 'Total Decisions', value: memory.total || 0, icon: '🧠' },
441
- { label: 'This Week', value: memory.thisWeek || 0, icon: '📅' },
442
- { label: 'This Month', value: memory.thisMonth || 0, icon: '📆' },
443
- { label: 'Checkpoints', value: memory.checkpoints || 0, icon: '💾' },
444
- ];
445
-
446
- const html = stats
447
- .map(
448
- (stat) => `
403
+ /**
404
+ * Render memory statistics
405
+ */
406
+ renderMemoryStats() {
407
+ const container = getElementByIdOrNull('dashboard-memory');
408
+ if (!container || !this.data.memory) {
409
+ return;
410
+ }
411
+ const memory = this.data.memory;
412
+ const stats = [
413
+ { label: 'Total Decisions', value: memory.total || 0, icon: '🧠' },
414
+ { label: 'This Week', value: memory.thisWeek || 0, icon: '📅' },
415
+ { label: 'This Month', value: memory.thisMonth || 0, icon: '📆' },
416
+ { label: 'Checkpoints', value: memory.checkpoints || 0, icon: '💾' },
417
+ ];
418
+ const html = stats
419
+ .map((stat) => `
449
420
  <div class="bg-white border border-gray-200 rounded-lg p-2.5 text-center">
450
421
  <span class="text-lg">${stat.icon}</span>
451
422
  <p class="text-xl font-bold text-gray-900 mt-1">${stat.value}</p>
452
423
  <p class="text-[10px] text-gray-500">${stat.label}</p>
453
424
  </div>
454
- `
455
- )
456
- .join('');
457
-
458
- container.innerHTML = html;
459
- }
460
-
461
- /**
462
- * Render agent configuration
463
- */
464
- renderAgentConfig() {
465
- const container = document.getElementById('dashboard-agent');
466
- if (!container || !this.data.agent) {
467
- return;
425
+ `)
426
+ .join('');
427
+ container.innerHTML = html;
468
428
  }
469
-
470
- const agent = this.data.agent;
471
- const heartbeat = this.data.heartbeat || {};
472
- const friendlyModel = formatModelName(agent.model) || 'Not Set';
473
-
474
- container.innerHTML = `
429
+ /**
430
+ * Render agent configuration
431
+ */
432
+ renderAgentConfig() {
433
+ const container = getElementByIdOrNull('dashboard-agent');
434
+ if (!container || !this.data.agent) {
435
+ return;
436
+ }
437
+ const agent = this.data.agent;
438
+ const heartbeat = this.data.heartbeat || {};
439
+ const friendlyModel = formatModelName(agent.model) || 'Not Set';
440
+ container.innerHTML = `
475
441
  <div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
476
442
  <div class="flex items-center justify-between">
477
443
  <div>
@@ -498,90 +464,78 @@ export class DashboardModule {
498
464
  </p>
499
465
  </div>
500
466
  </div>
501
- ${
502
- heartbeat.enabled
503
- ? `
467
+ ${heartbeat.enabled
468
+ ? `
504
469
  <div class="mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
505
470
  <p class="text-[10px] text-gray-500">
506
- Quiet hours: ${heartbeat.quietStart || 23}:00 - ${heartbeat.quietEnd || 8}:00
471
+ Quiet hours: ${heartbeat.quiet_start ?? heartbeat.quietStart ?? 23}:00 - ${heartbeat.quiet_end ?? heartbeat.quietEnd ?? 8}:00
507
472
  </p>
508
473
  </div>
509
474
  `
510
- : ''
511
- }
475
+ : ''}
512
476
  `;
513
- }
514
-
515
- /**
516
- * Render agent swarm section
517
- * Sprint 3 F2: Multi-agent dashboard
518
- */
519
- renderAgentSwarm() {
520
- const container = document.getElementById('dashboard-agent-swarm');
521
- if (!container) {
522
- return;
523
477
  }
524
-
525
- const multiAgent = this.multiAgentData || { enabled: false, agents: [] };
526
-
527
- if (!multiAgent.enabled) {
528
- container.innerHTML = `
478
+ /**
479
+ * Render agent swarm section
480
+ * Sprint 3 F2: Multi-agent dashboard
481
+ */
482
+ renderAgentSwarm() {
483
+ const container = getElementByIdOrNull('dashboard-agent-swarm');
484
+ if (!container) {
485
+ return;
486
+ }
487
+ const multiAgent = this.multiAgentData || { enabled: false, agents: [] };
488
+ if (!multiAgent.enabled) {
489
+ container.innerHTML = `
529
490
  <p class="text-gray-500 dark:text-gray-400 text-sm text-center py-4">
530
- Multi-agent is not enabled. Enable in <a href="#" class="text-indigo-600 hover:underline" onclick="document.querySelector('[data-tab=\\'settings\\']').click(); return false;">Settings</a>.
491
+ Multi-agent is not enabled. Enable in <a href="/viewer?tab=settings" class="text-indigo-600 hover:underline" data-action="open-settings">Settings</a>.
531
492
  </p>
532
493
  `;
533
- return;
534
- }
535
-
536
- const agents = multiAgent.agents || [];
537
-
538
- if (agents.length === 0) {
539
- container.innerHTML = `
494
+ return;
495
+ }
496
+ const agents = multiAgent.agents || [];
497
+ if (agents.length === 0) {
498
+ container.innerHTML = `
540
499
  <p class="text-gray-500 dark:text-gray-400 text-sm text-center py-4">
541
500
  No agents configured yet.
542
501
  </p>
543
502
  `;
544
- return;
545
- }
546
-
547
- // Tier badge colors
548
- const tierColors = {
549
- 1: { bg: 'bg-indigo-100', text: 'text-indigo-700', label: 'T1' },
550
- 2: { bg: 'bg-green-100', text: 'text-green-700', label: 'T2' },
551
- 3: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'T3' },
552
- };
553
-
554
- // Status icons (F2 enhanced)
555
- const statusIcons = {
556
- idle: '🟢', // 대기
557
- online: '🟢', // 온라인 (fallback)
558
- busy: '🟡', // 작업
559
- starting: '🔵', // 시작 중
560
- dead: '🔴', // 비정상 종료
561
- offline: '🔴', // 오프라인
562
- disabled: '⚪', // 비활성
563
- };
564
-
565
- // Status text labels
566
- const statusLabels = {
567
- idle: 'Ready',
568
- online: 'Ready',
569
- busy: 'Working...',
570
- starting: 'Starting...',
571
- dead: 'Error',
572
- offline: 'Offline',
573
- disabled: 'Disabled',
574
- };
575
-
576
- // Agent cards
577
- const agentCards = agents
578
- .map((agent) => {
579
- const tier = tierColors[agent.tier] || tierColors[1];
580
- const statusIcon = statusIcons[agent.status] || statusIcons.offline;
581
- const statusLabel = statusLabels[agent.status] || 'Unknown';
582
- const friendlyModel = formatModelName(agent.model) || agent.model || 'Default';
583
-
584
- return `
503
+ return;
504
+ }
505
+ // Tier badge colors
506
+ const tierColors = {
507
+ 1: { bg: 'bg-indigo-100', text: 'text-indigo-700', label: 'T1' },
508
+ 2: { bg: 'bg-green-100', text: 'text-green-700', label: 'T2' },
509
+ 3: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'T3' },
510
+ };
511
+ // Status icons (F2 enhanced)
512
+ const statusIcons = {
513
+ idle: '🟢', // 대기
514
+ online: '🟢', // 온라인 (fallback)
515
+ busy: '🟡', // 작업
516
+ starting: '🔵', // 시작
517
+ dead: '🔴', // 비정상 종료
518
+ offline: '🔴', // 오프라인
519
+ disabled: '', // 비활성
520
+ };
521
+ // Status text labels
522
+ const statusLabels = {
523
+ idle: 'Ready',
524
+ online: 'Ready',
525
+ busy: 'Working...',
526
+ starting: 'Starting...',
527
+ dead: 'Error',
528
+ offline: 'Offline',
529
+ disabled: 'Disabled',
530
+ };
531
+ // Agent cards
532
+ const agentCards = agents
533
+ .map((agent) => {
534
+ const tier = tierColors[agent.tier] || tierColors[1];
535
+ const statusIcon = statusIcons[agent.status] || statusIcons.offline;
536
+ const statusLabel = statusLabels[agent.status] || 'Unknown';
537
+ const friendlyModel = formatModelName(agent.model) || agent.model || 'Default';
538
+ return `
585
539
  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-2.5 hover:shadow-md transition-shadow">
586
540
  <div class="flex items-center justify-between mb-1.5">
587
541
  <div class="flex items-center gap-2">
@@ -591,36 +545,30 @@ export class DashboardModule {
591
545
  </div>
592
546
  <h3 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">${escapeHtml(agent.name)}</h3>
593
547
  <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">${escapeHtml(friendlyModel)}</p>
594
- ${
595
- agent.lastActivity
548
+ ${agent.lastActivity
596
549
  ? `<p class="text-[10px] text-gray-400 mt-1">Last: ${this.formatRelativeTime(agent.lastActivity)}</p>`
597
- : ''
598
- }
550
+ : ''}
599
551
  </div>
600
552
  `;
601
- })
602
- .join('');
603
-
604
- // Recent delegations (F2 F4 API integration)
605
- const delegationsData = this.delegationsData || { delegations: [], count: 0 };
606
- const delegations = delegationsData.delegations || [];
607
-
608
- // Status badge colors
609
- const statusColors = {
610
- completed: 'bg-green-100 text-green-700',
611
- claimed: 'bg-yellow-100 text-yellow-700',
612
- failed: 'bg-red-100 text-red-700',
613
- pending: 'bg-gray-100 text-gray-700',
614
- };
615
-
616
- const delegationList =
617
- delegations.length > 0
618
- ? delegations
619
- .slice(0, 5)
620
- .map((del) => {
621
- const statusColor = statusColors[del.status] || statusColors.pending;
622
- const timestamp = del.completedAt || del.claimedAt;
623
- return `
553
+ })
554
+ .join('');
555
+ // Recent delegations (F2 F4 API integration)
556
+ const delegationsData = this.delegationsData || { delegations: [], count: 0 };
557
+ const delegations = delegationsData.delegations || [];
558
+ // Status badge colors
559
+ const statusColors = {
560
+ completed: 'bg-green-100 text-green-700',
561
+ claimed: 'bg-yellow-100 text-yellow-700',
562
+ failed: 'bg-red-100 text-red-700',
563
+ pending: 'bg-gray-100 text-gray-700',
564
+ };
565
+ const delegationList = delegations.length > 0
566
+ ? delegations
567
+ .slice(0, 5)
568
+ .map((del) => {
569
+ const statusColor = statusColors[del.status] || statusColors.pending;
570
+ const timestamp = del.completedAt || del.claimedAt;
571
+ return `
624
572
  <div class="text-xs text-gray-700 dark:text-gray-300 py-1 border-b border-gray-100 dark:border-gray-700 last:border-0">
625
573
  <span class="${statusColor} text-[10px] font-bold px-1 py-0.5 rounded">${escapeHtml(del.status)}</span>
626
574
  <span class="font-medium">${escapeHtml(del.claimedBy || 'unknown')}</span>:
@@ -630,24 +578,21 @@ export class DashboardModule {
630
578
  </div>
631
579
  `;
632
580
  })
633
- .join('')
634
- : '<p class="text-xs text-gray-400">No recent delegations</p>';
635
-
636
- // Active chains
637
- const activeChains = multiAgent.activeChains || 0;
638
- const chainBadge =
639
- activeChains > 0
640
- ? `<span class="text-xs bg-green-100 text-green-600 px-2 py-0.5 rounded-full">${activeChains} active</span>`
641
- : '<span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">0 active</span>';
642
-
643
- container.innerHTML = `
581
+ .join('')
582
+ : '<p class="text-xs text-gray-400">No recent delegations</p>';
583
+ // Active chains
584
+ const activeChains = multiAgent.activeChains || 0;
585
+ const chainBadge = activeChains > 0
586
+ ? `<span class="text-xs bg-green-100 text-green-600 px-2 py-0.5 rounded-full">${activeChains} active</span>`
587
+ : '<span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">0 active</span>';
588
+ container.innerHTML = `
644
589
  <div class="mb-3">
645
590
  <p class="text-xs text-gray-500 mb-2">Agent Team:</p>
646
591
  <div class="grid grid-cols-1 md:grid-cols-3 gap-2">
647
592
  ${agentCards}
648
593
  </div>
649
594
  </div>
650
- <div class="mb-2 pb-2 border-b border-gray-200 dark:border-gray-700">
595
+ <div class="mb-2 pb-2 border-b border-gray-200 dark:border-gray-700">
651
596
  <div class="flex items-center justify-between mb-2">
652
597
  <p class="text-xs text-gray-500">Delegation Chain:</p>
653
598
  ${chainBadge}
@@ -657,94 +602,85 @@ export class DashboardModule {
657
602
  </div>
658
603
  </div>
659
604
  `;
660
- }
661
-
662
- /**
663
- * Render top topics
664
- */
665
- renderTopTopics() {
666
- const container = document.getElementById('dashboard-topics');
667
- if (!container || !this.data.memory) {
668
- return;
669
605
  }
670
-
671
- const topics = this.data.memory.topTopics || [];
672
-
673
- if (topics.length === 0) {
674
- container.innerHTML = `
606
+ /**
607
+ * Render top topics
608
+ */
609
+ renderTopTopics() {
610
+ const container = getElementByIdOrNull('dashboard-topics');
611
+ if (!container || !this.data.memory) {
612
+ return;
613
+ }
614
+ const topics = this.data.memory.topTopics || [];
615
+ if (topics.length === 0) {
616
+ container.innerHTML = `
675
617
  <p class="text-gray-500 dark:text-gray-400 text-sm">No topics yet. Start making decisions!</p>
676
618
  `;
677
- return;
678
- }
679
-
680
- const maxCount = Math.max(...topics.map((t) => t.count));
681
-
682
- const html = topics
683
- .map(
684
- (topic) => `
619
+ return;
620
+ }
621
+ const counts = topics
622
+ .map((topic) => (Number.isFinite(topic.count) ? topic.count : 0))
623
+ .filter((count) => count >= 0);
624
+ const maxCount = Math.max(1, ...counts);
625
+ const html = topics
626
+ .map((topic) => {
627
+ const safeCount = Number.isFinite(topic.count) ? topic.count : 0;
628
+ return `
685
629
  <div class="flex items-center gap-3 mb-2">
686
630
  <div class="flex-1">
687
631
  <div class="flex justify-between items-center mb-1">
688
632
  <span class="text-sm font-medium text-gray-900 dark:text-gray-100">${escapeHtml(topic.topic)}</span>
689
- <span class="text-xs text-gray-500 dark:text-gray-400">${topic.count}</span>
633
+ <span class="text-xs text-gray-500 dark:text-gray-400">${safeCount}</span>
690
634
  </div>
691
635
  <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
692
- <div class="bg-indigo-500 h-2 rounded-full" style="width: ${(topic.count / maxCount) * 100}%"></div>
636
+ <div class="bg-indigo-500 h-2 rounded-full" style="width: ${(safeCount / maxCount) * 100}%"></div>
693
637
  </div>
694
638
  </div>
695
639
  </div>
696
- `
697
- )
698
- .join('');
699
-
700
- container.innerHTML = html;
701
- }
702
-
703
- /**
704
- * Format timeout in human readable format
705
- */
706
- formatTimeout(ms) {
707
- if (!ms) {
708
- return 'N/A';
640
+ `;
641
+ })
642
+ .join('');
643
+ container.innerHTML = html;
709
644
  }
710
- if (ms < 60000) {
711
- return `${Math.round(ms / 1000)}s`;
712
- }
713
- return `${Math.round(ms / 60000)}min`;
714
- }
715
-
716
- /**
717
- * Render cron jobs section
718
- */
719
- renderCronJobs() {
720
- const container = document.getElementById('dashboard-cron');
721
- if (!container) {
722
- return;
645
+ /**
646
+ * Format timeout in human readable format
647
+ */
648
+ formatTimeout(ms) {
649
+ if (!ms) {
650
+ return 'N/A';
651
+ }
652
+ if (ms < 60000) {
653
+ return `${Math.round(ms / 1000)}s`;
654
+ }
655
+ return `${Math.round(ms / 60000)}min`;
723
656
  }
724
-
725
- const jobs = this.cronData?.jobs || this.cronData || [];
726
-
727
- if (!Array.isArray(jobs) || jobs.length === 0) {
728
- container.innerHTML = `
657
+ /**
658
+ * Render cron jobs section
659
+ */
660
+ renderCronJobs() {
661
+ const container = getElementByIdOrNull('dashboard-cron');
662
+ if (!container) {
663
+ return;
664
+ }
665
+ const jobs = this.cronData?.jobs || this.cronData || [];
666
+ if (!Array.isArray(jobs) || jobs.length === 0) {
667
+ container.innerHTML = `
729
668
  <p class="text-gray-500 text-sm text-center py-4">
730
669
  No cron jobs configured. Ask the agent to schedule a task or use the Settings tab.
731
670
  </p>
732
671
  `;
733
- return;
734
- }
735
-
736
- const rows = jobs
737
- .map((job) => {
738
- const isEnabled = job.enabled !== false;
739
- const statusBadge = isEnabled
740
- ? '<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-600">Active</span>'
741
- : '<span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-500">Paused</span>';
742
-
743
- const nextRun = job.nextRun
744
- ? new Date(job.nextRun).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
745
- : '-';
746
-
747
- return `
672
+ return;
673
+ }
674
+ const rows = jobs
675
+ .map((job) => {
676
+ const isEnabled = job.enabled !== false;
677
+ const statusBadge = isEnabled
678
+ ? '<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-600">Active</span>'
679
+ : '<span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-500">Paused</span>';
680
+ const nextRun = job.nextRun
681
+ ? new Date(job.nextRun).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
682
+ : '-';
683
+ return `
748
684
  <div class="flex items-center justify-between py-2 border-b border-gray-100 last:border-0">
749
685
  <div class="flex-1 min-w-0">
750
686
  <div class="flex items-center gap-2">
@@ -758,131 +694,118 @@ export class DashboardModule {
758
694
  </div>
759
695
  <div class="flex items-center gap-1 ml-2 shrink-0">
760
696
  <button class="text-xs px-2 py-1 bg-mama-yellow hover:bg-mama-yellow-hover text-mama-black rounded transition-colors"
761
- data-cron-id="${escapeHtml(job.id)}" title="Run Now">
697
+ data-action="run-cron"
698
+ data-cron-id="${escapeAttr(job.id)}" title="Run Now">
762
699
  Run
763
700
  </button>
764
701
  </div>
765
702
  </div>
766
703
  `;
767
- })
768
- .join('');
769
-
770
- container.innerHTML = `
704
+ })
705
+ .join('');
706
+ container.innerHTML = `
771
707
  <div class="space-y-0">
772
708
  ${rows}
773
709
  </div>
774
710
  `;
775
- }
776
-
777
- /**
778
- * Run a cron job immediately
779
- */
780
- async runCronJob(id) {
781
- try {
782
- await API.runCronJob(id);
783
- const statusEl = document.getElementById('dashboard-status');
784
- if (statusEl) {
785
- statusEl.textContent = `Cron job "${id}" triggered`;
786
- }
787
- await this.loadStatus();
788
- } catch (e) {
789
- logger.error('[Dashboard] Failed to run cron job:', e);
790
- const statusEl = document.getElementById('dashboard-status');
791
- if (statusEl) {
792
- const message = e instanceof Error ? e.message : String(e);
793
- statusEl.textContent = `Cron job "${id}" failed: ${message}`;
794
- }
795
711
  }
796
- }
797
-
798
- /**
799
- * Render token usage summary section
800
- */
801
- renderTokenSummary() {
802
- const container = document.getElementById('dashboard-tokens');
803
- if (!container) {
804
- return;
712
+ /**
713
+ * Run a cron job immediately
714
+ */
715
+ async runCronJob(id) {
716
+ try {
717
+ await API.runCronJob(id);
718
+ const statusEl = getElementByIdOrNull('dashboard-status');
719
+ if (statusEl) {
720
+ statusEl.textContent = `Cron job "${id}" triggered`;
721
+ }
722
+ await this.loadStatus();
723
+ }
724
+ catch (e) {
725
+ logger.error('[Dashboard] Failed to run cron job:', e);
726
+ const statusEl = getElementByIdOrNull('dashboard-status');
727
+ if (statusEl) {
728
+ const message = getErrorMessage(e);
729
+ statusEl.textContent = `Cron job "${id}" failed: ${message}`;
730
+ }
731
+ }
805
732
  }
806
-
807
- if (!this.tokenData?.summary) {
808
- container.innerHTML = `
733
+ /**
734
+ * Render token usage summary section
735
+ */
736
+ renderTokenSummary() {
737
+ const container = getElementByIdOrNull('dashboard-tokens');
738
+ if (!container) {
739
+ return;
740
+ }
741
+ if (!this.tokenData?.summary) {
742
+ container.innerHTML = `
809
743
  <p class="text-gray-500 text-sm text-center py-4">
810
744
  Token tracking not yet available. Usage data will appear after conversations.
811
745
  </p>
812
746
  `;
813
- return;
814
- }
815
-
816
- const s = this.tokenData.summary;
817
- const agents = this.tokenData.byAgent?.agents || [];
818
-
819
- const formatTokens = (n) => {
820
- if (!n || n === 0) {
821
- return '0';
822
- }
823
- if (n >= 1000000) {
824
- return (n / 1000000).toFixed(1) + 'M';
825
- }
826
- if (n >= 1000) {
827
- return (n / 1000).toFixed(1) + 'K';
828
- }
829
- return n.toString();
830
- };
831
-
832
- const formatCost = (usd) => {
833
- if (!usd || usd === 0) {
834
- return '$0.00';
835
- }
836
- return '$' + usd.toFixed(2);
837
- };
838
-
839
- // Summary cards
840
- const periods = [
841
- {
842
- label: 'Today',
843
- tokens: (s.today?.input_tokens || 0) + (s.today?.output_tokens || 0),
844
- cost: s.today?.cost_usd,
845
- icon: '📊',
846
- },
847
- {
848
- label: 'This Week',
849
- tokens: (s.week?.input_tokens || 0) + (s.week?.output_tokens || 0),
850
- cost: s.week?.cost_usd,
851
- icon: '📅',
852
- },
853
- {
854
- label: 'This Month',
855
- tokens: (s.month?.input_tokens || 0) + (s.month?.output_tokens || 0),
856
- cost: s.month?.cost_usd,
857
- icon: '📆',
858
- },
859
- ];
860
-
861
- const cards = periods
862
- .map(
863
- (p) => `
747
+ return;
748
+ }
749
+ const s = this.tokenData.summary;
750
+ const agents = this.tokenData.byAgent?.agents || [];
751
+ const formatTokens = (n) => {
752
+ if (!n || n === 0) {
753
+ return '0';
754
+ }
755
+ if (n >= 1000000) {
756
+ return (n / 1000000).toFixed(1) + 'M';
757
+ }
758
+ if (n >= 1000) {
759
+ return (n / 1000).toFixed(1) + 'K';
760
+ }
761
+ return n.toString();
762
+ };
763
+ const formatCost = (usd) => {
764
+ if (!usd || usd === 0) {
765
+ return '$0.00';
766
+ }
767
+ return '$' + usd.toFixed(2);
768
+ };
769
+ // Summary cards
770
+ const periods = [
771
+ {
772
+ label: 'Today',
773
+ tokens: (s.today?.input_tokens || 0) + (s.today?.output_tokens || 0),
774
+ cost: s.today?.cost_usd,
775
+ icon: '📊',
776
+ },
777
+ {
778
+ label: 'This Week',
779
+ tokens: (s.week?.input_tokens || 0) + (s.week?.output_tokens || 0),
780
+ cost: s.week?.cost_usd,
781
+ icon: '📅',
782
+ },
783
+ {
784
+ label: 'This Month',
785
+ tokens: (s.month?.input_tokens || 0) + (s.month?.output_tokens || 0),
786
+ cost: s.month?.cost_usd,
787
+ icon: '📆',
788
+ },
789
+ ];
790
+ const cards = periods
791
+ .map((p) => `
864
792
  <div class="bg-white border border-gray-200 rounded-lg p-2.5 text-center">
865
793
  <span class="text-lg">${p.icon}</span>
866
794
  <p class="text-xl font-bold text-gray-900 mt-1">${formatTokens(p.tokens)}</p>
867
795
  <p class="text-[10px] text-gray-500">${p.label}</p>
868
796
  <p class="text-[10px] text-mama-yellow-hover font-medium">${formatCost(p.cost)}</p>
869
797
  </div>
870
- `
871
- )
872
- .join('');
873
-
874
- // Agent breakdown (mini bar chart)
875
- const maxTokens = Math.max(
876
- ...agents.map((a) => (a.input_tokens || 0) + (a.output_tokens || 0)),
877
- 1
878
- );
879
- const agentBars = agents
880
- .slice(0, 5)
881
- .map((a) => {
882
- const totalTokens = (a.input_tokens || 0) + (a.output_tokens || 0);
883
- const pct = Math.round((totalTokens / maxTokens) * 100);
884
- const agentLabel = a.agent_name || a.agent_id || 'unknown';
885
- return `
798
+ `)
799
+ .join('');
800
+ // Agent breakdown (mini bar chart)
801
+ const maxTokens = Math.max(...agents.map((a) => (a.input_tokens || 0) + (a.output_tokens || 0)), 1);
802
+ const agentBars = agents
803
+ .slice(0, 5)
804
+ .map((a) => {
805
+ const totalTokens = (a.input_tokens || 0) + (a.output_tokens || 0);
806
+ const pct = Math.round((totalTokens / maxTokens) * 100);
807
+ const agentLabel = a.agent_name || a.agent_id || 'unknown';
808
+ return `
886
809
  <div class="flex items-center gap-2 mb-1.5">
887
810
  <span class="text-xs text-gray-700 w-20 truncate" title="${escapeHtml(agentLabel)}">${escapeHtml(agentLabel)}</span>
888
811
  <div class="flex-1 bg-gray-200 rounded-full h-2">
@@ -891,48 +814,43 @@ export class DashboardModule {
891
814
  <span class="text-[10px] text-gray-500 w-12 text-right">${formatTokens(totalTokens)}</span>
892
815
  </div>
893
816
  `;
894
- })
895
- .join('');
896
-
897
- container.innerHTML = `
817
+ })
818
+ .join('');
819
+ container.innerHTML = `
898
820
  <div class="grid grid-cols-3 gap-2 mb-3">
899
821
  ${cards}
900
822
  </div>
901
- ${
902
- agents.length > 0
903
- ? `
823
+ ${agents.length > 0
824
+ ? `
904
825
  <div>
905
826
  <p class="text-xs text-gray-500 mb-2">By Agent:</p>
906
827
  ${agentBars}
907
828
  </div>
908
829
  `
909
- : ''
910
- }
830
+ : ''}
911
831
  `;
912
- }
913
-
914
- /**
915
- * Set status message
916
- */
917
- setStatus(message, type = '') {
918
- const statusEl = document.getElementById('dashboard-status');
919
- if (statusEl) {
920
- statusEl.textContent = message;
921
- statusEl.className = `text-sm text-center py-2 ${type === 'error' ? 'text-red-500' : 'text-gray-500 dark:text-gray-400'}`;
922
832
  }
923
- }
924
-
925
- /**
926
- * Cleanup interval on destroy
927
- */
928
- cleanup() {
929
- if (this.updateInterval) {
930
- clearInterval(this.updateInterval);
931
- this.updateInterval = null;
833
+ /**
834
+ * Set status message
835
+ */
836
+ setStatus(message, type = '') {
837
+ const statusEl = getElementByIdOrNull('dashboard-status');
838
+ if (statusEl) {
839
+ statusEl.textContent = message;
840
+ statusEl.className = `text-sm text-center py-2 ${type === 'error' ? 'text-red-500' : 'text-gray-500 dark:text-gray-400'}`;
841
+ }
932
842
  }
933
- if (this.onCronClick) {
934
- document.removeEventListener('click', this.onCronClick);
935
- this.onCronClick = null;
843
+ /**
844
+ * Cleanup interval on destroy
845
+ */
846
+ cleanup() {
847
+ if (this.updateInterval) {
848
+ clearInterval(this.updateInterval);
849
+ this.updateInterval = null;
850
+ }
851
+ if (this.onCronClick) {
852
+ document.removeEventListener('click', this.onCronClick);
853
+ this.onCronClick = null;
854
+ }
936
855
  }
937
- }
938
856
  }