feat(server): completely overhaul dashboard into interactive memory hub

Added interactive knowledge graph visualization, omni-search, kanban task board, and real-time live agent activity stream.
This commit is contained in:
Riz Ashraf committed 2026-09-12 09:17:59 +01:00
1 parent 02bafa5db1
commit 36674df176
3 files changed
+234 -112

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
import re
endpoints = """
.route("/api/graph", get({
let state_clone = app_state.handler.state.clone();
move || async move {
let graph = state_clone.get_full_graph();
axum::Json(graph)
}
}))
.route("/api/tasks", get({
let state_clone = app_state.handler.state.clone();
move || async move {
let tasks = state_clone.tasks.read();
axum::Json(tasks.clone())
}
}))
.route("/api/search", get({
let state_clone = app_state.handler.state.clone();
move |axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>| async move {
if let Some(q) = params.get("q") {
if let Ok(idx) = state_clone.search_index.read() {
let results = idx.search(q, 50);
return axum::Json(serde_json::json!({ "results": results }));
}
}
axum::Json(serde_json::json!({ "results": [] }))
}
}))
"""
filepath = "C:\\Users\\reazul.ashraf\\workspace\\rust\\mcp-memory\\server\\src\\main.rs"
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
content = content.replace('.route("/api/stats",', endpoints + ' .route("/api/stats",')
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
+177 -112
View File
@@ -1,139 +1,204 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Brain Monitor</title> <title>Antigravity Memory Hub</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style> <style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 40px; background-color: #f0f2f5; color: #333; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f0f2f5; color: #333; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; }
h1 { color: #2c3e50; text-align: center; margin-bottom: 30px; } .header-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; max-width: 1000px; margin: 0 auto; } h1 { margin: 0; color: #2c3e50; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); text-align: center; transition: transform 0.2s ease; }
.stat-card:hover { transform: translateY(-5px); }
.stat-value { font-size: 2.5em; font-weight: bold; color: #3498db; margin: 10px 0; }
.stat-label { font-size: 1.1em; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px; }
.status-dot { display: inline-block; width: 10px; height: 10px; background-color: #2ecc71; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 5px #2ecc71; } .status-dot { display: inline-block; width: 10px; height: 10px; background-color: #2ecc71; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 5px #2ecc71; }
.header-bar { max-width: 1000px; margin: 0 auto 20px; display: flex; justify-content: space-between; align-items: center; }
.main-container { display: flex; gap: 20px; flex: 1; min-height: 0; }
.panel { background: white; padding: 15px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; flex-direction: column; }
.panel h2 { margin-top: 0; margin-bottom: 15px; color: #34495e; font-size: 1.2em; border-bottom: 1px solid #ecf0f1; padding-bottom: 10px; }
/* Graph Panel */
.graph-panel { flex: 2; }
#network-container { flex: 1; border: 1px solid #bdc3c7; border-radius: 4px; background: #fafafa; }
/* Right Column */
.right-column { flex: 1; display: flex; flex-direction: column; gap: 20px; }
/* Search Panel */
.search-panel { flex: 1; }
input[type="text"] { width: 100%; padding: 10px; border: 1px solid #bdc3c7; border-radius: 4px; box-sizing: border-box; margin-bottom: 10px; }
#search-results { overflow-y: auto; flex: 1; font-size: 0.9em; }
.search-result-item { padding: 8px; border-bottom: 1px solid #eee; }
.search-result-item strong { color: #2980b9; display: block; }
/* Kanban Panel */
.kanban-panel { flex: 1.5; }
.kanban-board { display: flex; gap: 10px; height: 100%; }
.kanban-column { flex: 1; background: #f8f9fa; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; }
.kanban-column h3 { margin: 0 0 10px 0; font-size: 0.9em; color: #7f8c8d; text-align: center; }
.kanban-items { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.task-card { background: white; padding: 8px; border-radius: 4px; border-left: 4px solid #3498db; font-size: 0.85em; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.task-card.completed { border-left-color: #2ecc71; opacity: 0.7; }
/* Activity Feed */
.activity-panel { height: 200px; margin-top: 20px; }
#activity-feed { overflow-y: auto; flex: 1; font-family: monospace; font-size: 0.85em; background: #1e1e1e; color: #00ff00; padding: 10px; border-radius: 4px; }
.feed-entry { margin-bottom: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="header-bar"> <div class="header-bar">
<h1>🧠 Antigravity Brain Monitor</h1> <h1>🧠 Antigravity Memory Hub</h1>
<div><span class="status-dot"></span>System Online</div> <div><span class="status-dot"></span>System Online</div>
</div> </div>
<div class="grid"> <div class="main-container">
<div class="stat-card"> <div class="panel graph-panel">
<div class="stat-label">Entities</div> <h2>Knowledge Graph</h2>
<div class="stat-value" id="val-entities">-</div> <div id="network-container"></div>
</div> </div>
<div class="stat-card">
<div class="stat-label">Relations</div> <div class="right-column">
<div class="stat-value" id="val-relations">-</div> <div class="panel search-panel">
</div> <h2>Omni-Search</h2>
<div class="stat-card"> <input type="text" id="search-box" placeholder="Search memory (e.g., 'database')..." onkeyup="handleSearch(event)">
<div class="stat-label">Active Tasks</div> <div id="search-results"></div>
<div class="stat-value" id="val-tasks">-</div> </div>
</div>
<div class="stat-card"> <div class="panel kanban-panel">
<div class="stat-label">Snippets</div> <h2>Task Board</h2>
<div class="stat-value" id="val-snippets">-</div> <div class="kanban-board">
</div> <div class="kanban-column">
<div class="stat-card"> <h3>TODO / IN PROGRESS</h3>
<div class="stat-label">Tech Debt</div> <div class="kanban-items" id="tasks-active"></div>
<div class="stat-value" id="val-tech_debts">-</div> </div>
</div> <div class="kanban-column">
<div class="stat-card"> <h3>COMPLETED</h3>
<div class="stat-label">ADRs</div> <div class="kanban-items" id="tasks-done"></div>
<div class="stat-value" id="val-adrs">-</div> </div>
</div> </div>
<div class="stat-card"> </div>
<div class="stat-label">Audit Ledger</div>
<div class="stat-value" id="val-ledger">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Sticky Notes</div>
<div class="stat-value" id="val-sticky">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Error Fixes</div>
<div class="stat-value" id="val-error_fixes">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Pinned Files</div>
<div class="stat-value" id="val-pinned_files">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Session Summaries</div>
<div class="stat-value" id="val-session_summaries">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Handoff Memos</div>
<div class="stat-value" id="val-handoff_memos">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Env Fingerprints</div>
<div class="stat-value" id="val-env_fingerprints">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Env Requirements</div>
<div class="stat-value" id="val-env_requirements">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Milestones</div>
<div class="stat-value" id="val-milestones">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Environments</div>
<div class="stat-value" id="val-environments">-</div>
</div>
<div class="stat-card">
<div class="stat-label">PR Checklists</div>
<div class="stat-value" id="val-pr_checklists">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Safety Gates</div>
<div class="stat-value" id="val-gates">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Workspaces</div>
<div class="stat-value" id="val-context_workspaces">-</div>
</div> </div>
</div> </div>
<div class="panel activity-panel">
<h2>Live Activity Feed</h2>
<div id="activity-feed"></div>
</div>
<script> <script>
async function fetchStats() { // Graph Initialization
let network = null;
async function loadGraph() {
try { try {
const response = await fetch('/api/stats'); const res = await fetch('/api/graph');
const data = await response.json(); const data = await res.json();
document.getElementById('val-entities').innerText = data.entities; const nodes = [];
document.getElementById('val-relations').innerText = data.relations; const edges = [];
document.getElementById('val-tasks').innerText = data.tasks; const nodeIds = new Set();
document.getElementById('val-snippets').innerText = data.snippets;
document.getElementById('val-tech_debts').innerText = data.tech_debts;
document.getElementById('val-adrs').innerText = data.adrs;
document.getElementById('val-ledger').innerText = data.ledger; for (const [name, entity] of Object.entries(data.entities)) {
document.getElementById('val-sticky').innerText = data.sticky; nodes.push({ id: name, label: name, title: entity.observations.join('\n') });
document.getElementById('val-error_fixes').innerText = data.error_fixes; nodeIds.add(name);
document.getElementById('val-pinned_files').innerText = data.pinned_files; }
document.getElementById('val-session_summaries').innerText = data.session_summaries;
document.getElementById('val-handoff_memos').innerText = data.handoff_memos; data.relations.forEach(r => {
document.getElementById('val-env_fingerprints').innerText = data.env_fingerprints; if (nodeIds.has(r.from) && nodeIds.has(r.to)) {
document.getElementById('val-env_requirements').innerText = data.env_requirements; edges.push({ from: r.from, to: r.to, label: r.relation_type, arrows: 'to' });
document.getElementById('val-milestones').innerText = data.milestones; }
document.getElementById('val-environments').innerText = data.environments; });
document.getElementById('val-pr_checklists').innerText = data.pr_checklists;
document.getElementById('val-gates').innerText = data.gates; const container = document.getElementById('network-container');
document.getElementById('val-context_workspaces').innerText = data.context_workspaces; const graphData = { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) };
const options = {
nodes: { shape: 'dot', size: 16, font: { size: 12, color: '#333' }, color: { background: '#3498db', border: '#2980b9' } },
edges: { color: { color: '#bdc3c7' }, font: { size: 10, align: 'middle' } },
physics: { barnesHut: { gravitationalConstant: -2000, centralGravity: 0.3, springLength: 95 } }
};
if (network) {
network.setData(graphData);
} else {
network = new vis.Network(container, graphData, options);
}
} catch (err) { } catch (err) {
console.error("Failed to fetch stats:", err); console.error("Failed to load graph", err);
} }
} }
// Kanban Board Initialization
async function loadTasks() {
try {
const res = await fetch('/api/tasks');
const tasks = await res.json();
const activeContainer = document.getElementById('tasks-active');
const doneContainer = document.getElementById('tasks-done');
activeContainer.innerHTML = '';
doneContainer.innerHTML = '';
tasks.forEach(t => {
const card = document.createElement('div');
card.className = `task-card ${t.status === 'completed' ? 'completed' : ''}`;
card.innerHTML = `<strong>${t.name}</strong><br>${t.description}`;
if (t.status === 'completed') {
doneContainer.appendChild(card);
} else {
activeContainer.appendChild(card);
}
});
} catch (err) {
console.error("Failed to load tasks", err);
}
}
// Search
async function handleSearch(e) {
if (e.key === 'Enter') {
const q = e.target.value;
if (!q) {
document.getElementById('search-results').innerHTML = '';
return;
}
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
const data = await res.json();
const container = document.getElementById('search-results');
container.innerHTML = data.results.map(r => `
<div class="search-result-item">
<strong>${r.type_name}</strong>
<div>${r.content.substring(0, 100)}...</div>
<div style="font-size:0.8em;color:#95a5a6">Score: ${r.score.toFixed(2)}</div>
</div>
`).join('');
}
}
// SSE Activity Feed
function setupSSE() {
const es = new EventSource('/sse');
const feed = document.getElementById('activity-feed');
es.addEventListener('activity', function(event) {
// If it's the endpoint initialization message, ignore it visually
if (event.data.startsWith('/messages?')) return;
const div = document.createElement('div');
div.className = 'feed-entry';
const time = new Date().toLocaleTimeString();
div.innerText = `[${time}] ${event.data}`;
feed.appendChild(div);
feed.scrollTop = feed.scrollHeight;
};
}
// Start
loadGraph();
loadTasks();
setupSSE();
// Initial fetch setInterval(() => {
fetchStats(); loadGraph();
// Poll every 2 seconds loadTasks();
setInterval(fetchStats, 2000); }, 5000);
</script> </script>
</body> </body>
</html> </html>
+18
View File
@@ -426,6 +426,24 @@ async fn message_handler(
let session_id = query.session_id.clone(); let session_id = query.session_id.clone();
let clients = Arc::clone(&state); let clients = Arc::clone(&state);
let activity_msg = if let Some(method) = payload.get("method").and_then(|m| m.as_str()) {
if method == "tools/call" {
let name = payload.get("params").and_then(|p| p.get("name")).and_then(|n| n.as_str()).unwrap_or("unknown_tool");
Some(format!("Agent executed tool: {}", name))
} else {
None
}
} else {
None
};
if let Some(msg) = activity_msg {
let clients_map = clients.clients.read().unwrap().clone();
for tx in clients_map.values() {
let _ = tx.send(Ok(Event::default().event("activity").data(msg.clone())));
}
}
tokio::spawn(async move { tokio::spawn(async move {
if let Some(response) = handler.handle_request(payload).await { if let Some(response) = handler.handle_request(payload).await {
let tx_opt = clients.clients.read().unwrap().get(&session_id).cloned(); let tx_opt = clients.clients.read().unwrap().get(&session_id).cloned();