ocerebro 0.3.2 → 0.4.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/cerebro/index/entities.db +0 -0
- package/package.json +1 -1
- package/pyproject.toml +3 -1
- package/src/dashboard/__init__.py +1 -0
- package/src/dashboard/api.py +404 -0
- package/src/dashboard/server.py +179 -0
- package/src/dashboard/static/index.html +519 -0
- package/src/dashboard/static/style.css +579 -0
- package/src/index/entities_db.py +5 -1
- package/src/mcp/server.py +95 -32
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>🧠 OCerebro Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/style.css">
|
|
8
|
+
<!-- Cytoscape.js -->
|
|
9
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
|
|
10
|
+
<!-- Chart.js -->
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
12
|
+
<!-- marked.js -->
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
14
|
+
<!-- Tailwind CSS (utility classes) -->
|
|
15
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<header>
|
|
19
|
+
<div class="logo">
|
|
20
|
+
🧠 OCerebro <span class="text-indigo-400">Dashboard</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="flex items-center gap-2">
|
|
23
|
+
<span class="text-sm text-slate-400">MCP Status:</span>
|
|
24
|
+
<span class="status-dot" id="status-dot" title="Online"></span>
|
|
25
|
+
</div>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<nav class="tabs">
|
|
29
|
+
<button class="tab-btn active" data-tab="overview">Overview</button>
|
|
30
|
+
<button class="tab-btn" data-tab="graph">Grafo</button>
|
|
31
|
+
<button class="tab-btn" data-tab="memories">Memórias</button>
|
|
32
|
+
<button class="tab-btn" data-tab="timeline">Timeline</button>
|
|
33
|
+
</nav>
|
|
34
|
+
|
|
35
|
+
<main>
|
|
36
|
+
<!-- Tab 1: Overview -->
|
|
37
|
+
<div id="overview" class="tab-content active">
|
|
38
|
+
<div class="status-grid">
|
|
39
|
+
<div class="status-card">
|
|
40
|
+
<h3>Total Memórias</h3>
|
|
41
|
+
<div class="value" id="stat-memories">-</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="status-card">
|
|
44
|
+
<h3>Entidades</h3>
|
|
45
|
+
<div class="value" id="stat-entities">-</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="status-card">
|
|
48
|
+
<h3>Relacionamentos</h3>
|
|
49
|
+
<div class="value" id="stat-relationships">-</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="status-card">
|
|
52
|
+
<h3>Projetos Ativos</h3>
|
|
53
|
+
<div class="value" id="stat-projects">-</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<h2 class="text-xl font-bold mb-4 text-slate-300">Projetos</h2>
|
|
58
|
+
<div class="projects-grid" id="projects-grid">
|
|
59
|
+
<div class="loading">Carregando</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Tab 2: Grafo -->
|
|
64
|
+
<div id="graph" class="tab-content">
|
|
65
|
+
<div class="graph-container">
|
|
66
|
+
<div id="cy"></div>
|
|
67
|
+
<div class="graph-panel">
|
|
68
|
+
<div class="graph-controls">
|
|
69
|
+
<label>
|
|
70
|
+
Projeto:
|
|
71
|
+
<select id="graph-project" class="filter-select w-full mt-1">
|
|
72
|
+
<option value="">Todos</option>
|
|
73
|
+
</select>
|
|
74
|
+
</label>
|
|
75
|
+
<div class="flex flex-wrap gap-2">
|
|
76
|
+
<label class="flex items-center gap-1">
|
|
77
|
+
<input type="checkbox" value="PROJECT" checked class="graph-type">
|
|
78
|
+
<span class="w-3 h-3 rounded-full" style="background:#3B82F6"></span>
|
|
79
|
+
</label>
|
|
80
|
+
<label class="flex items-center gap-1">
|
|
81
|
+
<input type="checkbox" value="TECH" checked class="graph-type">
|
|
82
|
+
<span class="w-3 h-3 rounded-full" style="background:#10B981"></span>
|
|
83
|
+
</label>
|
|
84
|
+
<label class="flex items-center gap-1">
|
|
85
|
+
<input type="checkbox" value="PERSON" checked class="graph-type">
|
|
86
|
+
<span class="w-3 h-3 rounded-full" style="background:#F59E0B"></span>
|
|
87
|
+
</label>
|
|
88
|
+
<label class="flex items-center gap-1">
|
|
89
|
+
<input type="checkbox" value="ORG" checked class="graph-type">
|
|
90
|
+
<span class="w-3 h-3 rounded-full" style="background:#EF4444"></span>
|
|
91
|
+
</label>
|
|
92
|
+
<label class="flex items-center gap-1">
|
|
93
|
+
<input type="checkbox" value="TAG" class="graph-type">
|
|
94
|
+
<span class="w-3 h-3 rounded-full" style="background:#6B7280"></span>
|
|
95
|
+
</label>
|
|
96
|
+
<label class="flex items-center gap-1">
|
|
97
|
+
<input type="checkbox" value="META" class="graph-type">
|
|
98
|
+
<span class="w-3 h-3 rounded-full" style="background:#8B5CF6"></span>
|
|
99
|
+
</label>
|
|
100
|
+
</div>
|
|
101
|
+
<button class="btn btn-secondary" onclick="resetGraphZoom()">Reset Zoom</button>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="node-details" id="node-details">
|
|
104
|
+
<p class="text-slate-400 text-sm">Clique em um nó para ver detalhes</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Tab 3: Memórias -->
|
|
111
|
+
<div id="memories" class="tab-content">
|
|
112
|
+
<div class="memories-controls">
|
|
113
|
+
<input type="text" class="search-input" id="memory-search" placeholder="Buscar memórias...">
|
|
114
|
+
<select class="filter-select" id="memory-project">
|
|
115
|
+
<option value="">Todos projetos</option>
|
|
116
|
+
</select>
|
|
117
|
+
<select class="filter-select" id="memory-type">
|
|
118
|
+
<option value="">Todos tipos</option>
|
|
119
|
+
<option value="decision">Decision</option>
|
|
120
|
+
<option value="error">Error</option>
|
|
121
|
+
<option value="reference">Reference</option>
|
|
122
|
+
<option value="feedback">Feedback</option>
|
|
123
|
+
</select>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="memories-list" id="memories-list">
|
|
126
|
+
<div class="loading">Carregando</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Tab 4: Timeline -->
|
|
131
|
+
<div id="timeline" class="tab-content">
|
|
132
|
+
<div class="timeline-controls">
|
|
133
|
+
<label class="text-slate-300">Projeto:</label>
|
|
134
|
+
<select class="filter-select" id="timeline-project">
|
|
135
|
+
<option value="">Todos</option>
|
|
136
|
+
</select>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="timeline-container">
|
|
139
|
+
<canvas id="timeline-chart"></canvas>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</main>
|
|
143
|
+
|
|
144
|
+
<!-- Modal de memória -->
|
|
145
|
+
<div class="modal-overlay" id="memory-modal">
|
|
146
|
+
<div class="modal">
|
|
147
|
+
<div class="modal-header">
|
|
148
|
+
<h2 id="modal-title">Memória</h2>
|
|
149
|
+
<button class="modal-close" onclick="closeModal()">×</button>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="modal-body">
|
|
152
|
+
<div class="markdown-content" id="modal-content"></div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<script>
|
|
158
|
+
// Estado global
|
|
159
|
+
let cy = null;
|
|
160
|
+
let timelineChart = null;
|
|
161
|
+
let currentProject = '';
|
|
162
|
+
|
|
163
|
+
// Cores dos tipos de nó
|
|
164
|
+
const nodeColors = {
|
|
165
|
+
PROJECT: '#3B82F6',
|
|
166
|
+
TECH: '#10B981',
|
|
167
|
+
PERSON: '#F59E0B',
|
|
168
|
+
TAG: '#6B7280',
|
|
169
|
+
META: '#8B5CF6',
|
|
170
|
+
ORG: '#EF4444'
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Inicialização
|
|
174
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
175
|
+
initTabs();
|
|
176
|
+
loadOverview();
|
|
177
|
+
initGraph();
|
|
178
|
+
loadMemories();
|
|
179
|
+
loadTimeline();
|
|
180
|
+
populateFilters();
|
|
181
|
+
|
|
182
|
+
// Event listeners
|
|
183
|
+
document.getElementById('memory-search').addEventListener('input', debounce(loadMemories, 300));
|
|
184
|
+
document.getElementById('memory-project').addEventListener('change', loadMemories);
|
|
185
|
+
document.getElementById('memory-type').addEventListener('change', loadMemories);
|
|
186
|
+
document.getElementById('graph-project').addEventListener('change', loadGraph);
|
|
187
|
+
document.querySelectorAll('.graph-type').forEach(cb => {
|
|
188
|
+
cb.addEventListener('change', loadGraph);
|
|
189
|
+
});
|
|
190
|
+
document.getElementById('timeline-project').addEventListener('change', loadTimeline);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Tabs
|
|
194
|
+
function initTabs() {
|
|
195
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
196
|
+
btn.addEventListener('click', () => {
|
|
197
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
198
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
199
|
+
btn.classList.add('active');
|
|
200
|
+
document.getElementById(btn.dataset.tab).classList.add('active');
|
|
201
|
+
|
|
202
|
+
// Resize graph when tab becomes visible
|
|
203
|
+
if (btn.dataset.tab === 'graph' && cy) {
|
|
204
|
+
setTimeout(() => cy.resize(), 100);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Overview
|
|
211
|
+
async function loadOverview() {
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch('/api/status');
|
|
214
|
+
const data = await res.json();
|
|
215
|
+
|
|
216
|
+
document.getElementById('stat-memories').textContent = data.total_memories || 0;
|
|
217
|
+
document.getElementById('stat-entities').textContent = data.total_entities || 0;
|
|
218
|
+
document.getElementById('stat-relationships').textContent = data.total_relationships || 0;
|
|
219
|
+
document.getElementById('stat-projects').textContent = data.projects || 0;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error('Erro ao carregar status:', e);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Carrega projetos
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch('/api/projects');
|
|
227
|
+
const projects = await res.json();
|
|
228
|
+
|
|
229
|
+
const grid = document.getElementById('projects-grid');
|
|
230
|
+
if (projects.length === 0) {
|
|
231
|
+
grid.innerHTML = '<p class="text-slate-400">Nenhum projeto encontrado</p>';
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
grid.innerHTML = projects.map(p => `
|
|
236
|
+
<div class="project-card" onclick="filterByProject('${p.name}')">
|
|
237
|
+
<h3>${p.name}</h3>
|
|
238
|
+
<div class="text-2xl font-bold text-indigo-400 mb-2">${p.memory_count} memórias</div>
|
|
239
|
+
<div class="project-stats">
|
|
240
|
+
${Object.entries(p.types).map(([type, count]) => `
|
|
241
|
+
<span class="badge badge-${type}">${type}: ${count}</span>
|
|
242
|
+
`).join('')}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
`).join('');
|
|
246
|
+
} catch (e) {
|
|
247
|
+
console.error('Erro ao carregar projetos:', e);
|
|
248
|
+
document.getElementById('projects-grid').innerHTML = '<p class="text-red-400">Erro ao carregar projetos</p>';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function filterByProject(project) {
|
|
253
|
+
currentProject = project;
|
|
254
|
+
document.getElementById('memory-project').value = project;
|
|
255
|
+
document.getElementById('graph-project').value = project;
|
|
256
|
+
document.getElementById('timeline-project').value = project;
|
|
257
|
+
|
|
258
|
+
// Switch to memories tab
|
|
259
|
+
document.querySelector('[data-tab="memories"]').click();
|
|
260
|
+
loadMemories();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Graph
|
|
264
|
+
function initGraph() {
|
|
265
|
+
cy = cytoscape({
|
|
266
|
+
container: document.getElementById('cy'),
|
|
267
|
+
style: [
|
|
268
|
+
{
|
|
269
|
+
selector: 'node',
|
|
270
|
+
style: {
|
|
271
|
+
'background-color': (el) => nodeColors[el.data('type')] || '#6366F1',
|
|
272
|
+
'label': 'data(label)',
|
|
273
|
+
'color': '#F1F5F9',
|
|
274
|
+
'font-size': '12px',
|
|
275
|
+
'text-valign': 'bottom',
|
|
276
|
+
'text-halign': 'center',
|
|
277
|
+
'width': 40,
|
|
278
|
+
'height': 40
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
selector: 'edge',
|
|
283
|
+
style: {
|
|
284
|
+
'width': 2,
|
|
285
|
+
'line-color': '#475569',
|
|
286
|
+
'target-arrow-color': '#475569',
|
|
287
|
+
'target-arrow-shape': 'triangle',
|
|
288
|
+
'curve-style': 'bezier',
|
|
289
|
+
'label': 'data(label)',
|
|
290
|
+
'font-size': '10px',
|
|
291
|
+
'color': '#94A3B8',
|
|
292
|
+
'text-rotation': 'autorotate'
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
selector: 'node:selected',
|
|
297
|
+
style: {
|
|
298
|
+
'border-width': 3,
|
|
299
|
+
'border-color': '#6366F1'
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
layout: {
|
|
304
|
+
name: 'cose',
|
|
305
|
+
animate: false,
|
|
306
|
+
nodeRepulsion: 4500,
|
|
307
|
+
edgeElasticity: 100,
|
|
308
|
+
packingFactor: 0.5
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
cy.on('tap', 'node', (evt) => {
|
|
313
|
+
showNodeDetails(evt.target);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
loadGraph();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function loadGraph() {
|
|
320
|
+
if (!cy) return;
|
|
321
|
+
|
|
322
|
+
const project = document.getElementById('graph-project').value;
|
|
323
|
+
const types = Array.from(document.querySelectorAll('.graph-type:checked')).map(cb => cb.value).join(',');
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch(`/api/graph?project=${encodeURIComponent(project)}&types=${encodeURIComponent(types)}`);
|
|
327
|
+
const data = await res.json();
|
|
328
|
+
|
|
329
|
+
cy.remove(cy.elements());
|
|
330
|
+
cy.add([...data.nodes, ...data.edges]);
|
|
331
|
+
cy.layout({ name: 'cose', animate: true }).run();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.error('Erro ao carregar grafo:', e);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function resetGraphZoom() {
|
|
338
|
+
if (cy) {
|
|
339
|
+
cy.fit(null, 50);
|
|
340
|
+
cy.zoom({ level: 1, renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 } });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function showNodeDetails(node) {
|
|
345
|
+
const data = node.data();
|
|
346
|
+
const details = document.getElementById('node-details');
|
|
347
|
+
|
|
348
|
+
details.innerHTML = `
|
|
349
|
+
<h3>${data.label}</h3>
|
|
350
|
+
<p class="type">Tipo: <span class="badge badge-${data.type.toLowerCase() || 'default'}">${data.type}</span></p>
|
|
351
|
+
${data.memory_id ? `
|
|
352
|
+
<div class="related-memories">
|
|
353
|
+
<h4>Memória relacionada</h4>
|
|
354
|
+
<a href="#" class="memory-link" onclick="openMemory('${data.memory_id}'); return false;">
|
|
355
|
+
📄 ${data.memory_id}
|
|
356
|
+
</a>
|
|
357
|
+
</div>
|
|
358
|
+
` : ''}
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Memórias
|
|
363
|
+
async function loadMemories() {
|
|
364
|
+
const q = document.getElementById('memory-search').value;
|
|
365
|
+
const project = document.getElementById('memory-project').value;
|
|
366
|
+
const type = document.getElementById('memory-type').value;
|
|
367
|
+
|
|
368
|
+
let url = '/api/memories?limit=100';
|
|
369
|
+
if (q) url += `&q=${encodeURIComponent(q)}`;
|
|
370
|
+
if (project) url += `&project=${encodeURIComponent(project)}`;
|
|
371
|
+
if (type) url += `&type=${encodeURIComponent(type)}`;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch(url);
|
|
375
|
+
const memories = await res.json();
|
|
376
|
+
|
|
377
|
+
const list = document.getElementById('memories-list');
|
|
378
|
+
if (memories.length === 0) {
|
|
379
|
+
list.innerHTML = '<p class="text-slate-400 text-center py-8">Nenhuma memória encontrada</p>';
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
list.innerHTML = memories.map(m => `
|
|
384
|
+
<div class="memory-card ${m.gc_risk > 0.7 ? 'high-gc-risk' : ''}" onclick="openMemory('${m.id}')">
|
|
385
|
+
<div class="memory-header">
|
|
386
|
+
<h3>${m.title}</h3>
|
|
387
|
+
<span class="badge badge-${m.type}">${m.type}</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="memory-meta">
|
|
390
|
+
<span>📁 ${m.project}</span>
|
|
391
|
+
<span>📅 ${new Date(m.created_at).toLocaleDateString('pt-BR')}</span>
|
|
392
|
+
${m.gc_risk > 0.7 ? '<span class="text-red-400">⚠️ GC Risk: ' + m.gc_risk + '</span>' : ''}
|
|
393
|
+
</div>
|
|
394
|
+
${m.tags.length > 0 ? `
|
|
395
|
+
<div class="memory-tags">
|
|
396
|
+
${m.tags.map(t => `<span class="tag">${t}</span>`).join('')}
|
|
397
|
+
</div>
|
|
398
|
+
` : ''}
|
|
399
|
+
</div>
|
|
400
|
+
`).join('');
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.error('Erro ao carregar memórias:', e);
|
|
403
|
+
document.getElementById('memories-list').innerHTML = '<p class="text-red-400">Erro ao carregar memórias</p>';
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function openMemory(memoryId) {
|
|
408
|
+
try {
|
|
409
|
+
const res = await fetch(`/api/memory/${memoryId}`);
|
|
410
|
+
const data = await res.json();
|
|
411
|
+
|
|
412
|
+
document.getElementById('modal-title').textContent = data.metadata?.title || memoryId;
|
|
413
|
+
document.getElementById('modal-content').innerHTML = marked.parse(data.content || 'Sem conteúdo');
|
|
414
|
+
|
|
415
|
+
document.getElementById('memory-modal').classList.add('active');
|
|
416
|
+
} catch (e) {
|
|
417
|
+
console.error('Erro ao abrir memória:', e);
|
|
418
|
+
alert('Erro ao carregar memória: ' + e.message);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function closeModal() {
|
|
423
|
+
document.getElementById('memory-modal').classList.remove('active');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Timeline
|
|
427
|
+
async function loadTimeline() {
|
|
428
|
+
const project = document.getElementById('timeline-project').value;
|
|
429
|
+
const ctx = document.getElementById('timeline-chart').getContext('2d');
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const res = await fetch(`/api/timeline?project=${encodeURIComponent(project)}&days=30`);
|
|
433
|
+
const data = await res.json();
|
|
434
|
+
|
|
435
|
+
if (timelineChart) {
|
|
436
|
+
timelineChart.destroy();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
timelineChart = new Chart(ctx, {
|
|
440
|
+
type: 'line',
|
|
441
|
+
data: {
|
|
442
|
+
labels: data.labels,
|
|
443
|
+
datasets: data.datasets
|
|
444
|
+
},
|
|
445
|
+
options: {
|
|
446
|
+
responsive: true,
|
|
447
|
+
maintainAspectRatio: false,
|
|
448
|
+
plugins: {
|
|
449
|
+
legend: {
|
|
450
|
+
labels: { color: '#F1F5F9' }
|
|
451
|
+
},
|
|
452
|
+
tooltip: {
|
|
453
|
+
backgroundColor: '#1E293B',
|
|
454
|
+
titleColor: '#F1F5F9',
|
|
455
|
+
bodyColor: '#94A3B8',
|
|
456
|
+
borderColor: '#334155',
|
|
457
|
+
borderWidth: 1
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
scales: {
|
|
461
|
+
x: {
|
|
462
|
+
ticks: { color: '#94A3B8' },
|
|
463
|
+
grid: { color: '#334155' }
|
|
464
|
+
},
|
|
465
|
+
y: {
|
|
466
|
+
ticks: { color: '#94A3B8' },
|
|
467
|
+
grid: { color: '#334155' },
|
|
468
|
+
beginAtZero: true
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
} catch (e) {
|
|
474
|
+
console.error('Erro ao carregar timeline:', e);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Popula filtros
|
|
479
|
+
async function populateFilters() {
|
|
480
|
+
try {
|
|
481
|
+
const res = await fetch('/api/projects');
|
|
482
|
+
const projects = await res.json();
|
|
483
|
+
|
|
484
|
+
const selects = ['graph-project', 'memory-project', 'timeline-project'];
|
|
485
|
+
selects.forEach(id => {
|
|
486
|
+
const select = document.getElementById(id);
|
|
487
|
+
select.innerHTML = '<option value="">Todos</option>' +
|
|
488
|
+
projects.map(p => `<option value="${p.name}">${p.name}</option>`).join('');
|
|
489
|
+
});
|
|
490
|
+
} catch (e) {
|
|
491
|
+
console.error('Erro ao popular filtros:', e);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Utilities
|
|
496
|
+
function debounce(fn, delay) {
|
|
497
|
+
let timer;
|
|
498
|
+
return (...args) => {
|
|
499
|
+
clearTimeout(timer);
|
|
500
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Verifica status MCP periodicamente
|
|
505
|
+
setInterval(async () => {
|
|
506
|
+
try {
|
|
507
|
+
const res = await fetch('/api/status');
|
|
508
|
+
if (res.ok) {
|
|
509
|
+
document.getElementById('status-dot').classList.remove('offline');
|
|
510
|
+
} else {
|
|
511
|
+
document.getElementById('status-dot').classList.add('offline');
|
|
512
|
+
}
|
|
513
|
+
} catch (e) {
|
|
514
|
+
document.getElementById('status-dot').classList.add('offline');
|
|
515
|
+
}
|
|
516
|
+
}, 5000);
|
|
517
|
+
</script>
|
|
518
|
+
</body>
|
|
519
|
+
</html>
|