@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.
- package/CHANGELOG.md +10 -0
- package/dist/agent/agent-loop.d.ts +1 -8
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +44 -159
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/claude-cli-wrapper.d.ts +6 -0
- package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
- package/dist/agent/claude-cli-wrapper.js +6 -0
- package/dist/agent/claude-cli-wrapper.js.map +1 -1
- package/dist/agent/codex-mcp-process.d.ts +85 -0
- package/dist/agent/codex-mcp-process.d.ts.map +1 -0
- package/dist/agent/codex-mcp-process.js +357 -0
- package/dist/agent/codex-mcp-process.js.map +1 -0
- package/dist/agent/session-pool.d.ts +17 -2
- package/dist/agent/session-pool.d.ts.map +1 -1
- package/dist/agent/session-pool.js +51 -26
- package/dist/agent/session-pool.js.map +1 -1
- package/dist/agent/types.d.ts +9 -24
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/graph-api.d.ts.map +1 -1
- package/dist/api/graph-api.js +133 -45
- package/dist/api/graph-api.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +14 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -10
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +143 -54
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +2 -7
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/config-manager.d.ts.map +1 -1
- package/dist/cli/config/config-manager.js +9 -17
- package/dist/cli/config/config-manager.js.map +1 -1
- package/dist/cli/config/types.d.ts +19 -25
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/gateways/context-injector.d.ts.map +1 -1
- package/dist/gateways/context-injector.js +6 -3
- package/dist/gateways/context-injector.js.map +1 -1
- package/dist/gateways/discord.d.ts +4 -0
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +39 -16
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/message-router.d.ts +6 -1
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +92 -7
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
- package/dist/multi-agent/agent-process-manager.js +36 -9
- package/dist/multi-agent/agent-process-manager.js.map +1 -1
- package/dist/multi-agent/runtime-process.d.ts +4 -4
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +9 -20
- package/dist/multi-agent/runtime-process.js.map +1 -1
- package/dist/multi-agent/types.d.ts +13 -8
- package/dist/multi-agent/types.d.ts.map +1 -1
- package/dist/multi-agent/types.js.map +1 -1
- package/dist/setup/setup-prompt.d.ts +1 -1
- package/dist/setup/setup-prompt.d.ts.map +1 -1
- package/dist/setup/setup-prompt.js +19 -0
- package/dist/setup/setup-prompt.js.map +1 -1
- package/dist/setup/setup-server.d.ts.map +1 -1
- package/dist/setup/setup-server.js +39 -16
- package/dist/setup/setup-server.js.map +1 -1
- package/dist/skills/skill-registry.d.ts.map +1 -1
- package/dist/skills/skill-registry.js +5 -2
- package/dist/skills/skill-registry.js.map +1 -1
- package/package.json +5 -3
- package/public/setup.html +12 -1
- package/public/viewer/js/modules/chat.js +1760 -1976
- package/public/viewer/js/modules/dashboard.js +613 -695
- package/public/viewer/js/modules/graph.js +857 -970
- package/public/viewer/js/modules/memory.js +357 -312
- package/public/viewer/js/modules/settings.js +1009 -1026
- package/public/viewer/js/modules/skills.js +336 -355
- package/public/viewer/js/utils/api.js +255 -255
- package/public/viewer/js/utils/debug-logger.js +20 -26
- package/public/viewer/js/utils/dom.js +73 -60
- package/public/viewer/js/utils/format.js +182 -228
- package/public/viewer/js/utils/markdown.js +40 -0
- package/public/viewer/src/modules/chat.ts +2258 -0
- package/public/viewer/src/modules/dashboard.ts +1052 -0
- package/public/viewer/src/modules/graph.ts +1080 -0
- package/public/viewer/src/modules/memory.ts +453 -0
- package/public/viewer/src/modules/settings.ts +1398 -0
- package/public/viewer/src/modules/skills.ts +457 -0
- package/public/viewer/src/types/global.d.ts +168 -0
- package/public/viewer/src/utils/api.ts +650 -0
- package/public/viewer/src/utils/debug-logger.ts +36 -0
- package/public/viewer/src/utils/dom.ts +138 -0
- package/public/viewer/src/utils/format.ts +331 -0
- package/public/viewer/src/utils/markdown.ts +46 -0
- package/public/viewer/tsconfig.viewer.json +18 -0
- package/public/viewer/viewer.html +214 -311
- package/dist/agent/codex-cli-wrapper.d.ts +0 -85
- package/dist/agent/codex-cli-wrapper.d.ts.map +0 -1
- package/dist/agent/codex-cli-wrapper.js +0 -295
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
ch.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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="
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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">${
|
|
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: ${(
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
808
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
}
|