Skip to content

Commit 0095808

Browse files
authored
Merge pull request #21 from basicmachines-co/claw/memory-dashboard
feat: Memory Dashboard — kanban board, activity feed, stats
2 parents 178372c + 8eab185 commit 0095808

File tree

6 files changed

+574
-0
lines changed

6 files changed

+574
-0
lines changed

config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export type CloudConfig = {
66
api_key: string
77
}
88

9+
export type DashboardConfig = {
10+
enabled: boolean
11+
port: number
12+
}
13+
914
export type BasicMemoryConfig = {
1015
project: string
1116
bmPath: string
@@ -18,6 +23,7 @@ export type BasicMemoryConfig = {
1823
recallPrompt: string
1924
debug: boolean
2025
cloud?: CloudConfig
26+
dashboard: DashboardConfig
2127
}
2228

2329
const ALLOWED_KEYS = [
@@ -37,6 +43,7 @@ const ALLOWED_KEYS = [
3743
"recall_prompt",
3844
"debug",
3945
"cloud",
46+
"dashboard",
4047
]
4148

4249
function assertAllowedKeys(
@@ -106,6 +113,22 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
106113
}
107114
}
108115

116+
let dashboard: DashboardConfig = { enabled: false, port: 3838 }
117+
if (
118+
cfg.dashboard &&
119+
typeof cfg.dashboard === "object" &&
120+
!Array.isArray(cfg.dashboard)
121+
) {
122+
const d = cfg.dashboard as Record<string, unknown>
123+
dashboard = {
124+
enabled: typeof d.enabled === "boolean" ? d.enabled : false,
125+
port:
126+
typeof d.port === "number" && d.port > 0 && d.port < 65536
127+
? d.port
128+
: 3838,
129+
}
130+
}
131+
109132
return {
110133
project:
111134
typeof cfg.project === "string" && cfg.project.length > 0
@@ -144,6 +167,7 @@ export function parseConfig(raw: unknown): BasicMemoryConfig {
144167
: "Check for active tasks and recent activity. Summarize anything relevant to the current session.",
145168
debug: typeof cfg.debug === "boolean" ? cfg.debug : false,
146169
cloud,
170+
dashboard,
147171
}
148172
}
149173

dashboard/index.html

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Memory Dashboard</title>
7+
<style>
8+
* { margin: 0; padding: 0; box-sizing: border-box; }
9+
body { background: #0a0a0a; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
10+
code, .mono { font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; }
11+
12+
/* Stats Bar */
13+
.stats-bar {
14+
display: flex; gap: 24px; padding: 16px 24px;
15+
background: #111; border-bottom: 1px solid #222;
16+
}
17+
.stat { text-align: center; }
18+
.stat-value { font-size: 28px; font-weight: 700; }
19+
.stat-label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
20+
.stat-active .stat-value { color: #3b82f6; }
21+
.stat-done .stat-value { color: #22c55e; }
22+
.stat-explore .stat-value { color: #a855f7; }
23+
.stat-total .stat-value { color: #e0e0e0; }
24+
25+
/* Layout */
26+
.main { display: flex; height: calc(100vh - 70px); }
27+
.kanban-area { flex: 1; overflow-x: auto; padding: 16px; }
28+
.sidebar { width: 320px; border-left: 1px solid #222; padding: 16px; overflow-y: auto; flex-shrink: 0; }
29+
30+
/* Kanban */
31+
.kanban { display: flex; gap: 12px; height: 100%; }
32+
.column { flex: 1; min-width: 220px; background: #111; border-radius: 8px; display: flex; flex-direction: column; }
33+
.column-header { padding: 12px; font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid; }
34+
.col-active .column-header { border-color: #3b82f6; color: #3b82f6; }
35+
.col-blocked .column-header { border-color: #ef4444; color: #ef4444; }
36+
.col-done .column-header { border-color: #22c55e; color: #22c55e; }
37+
.col-abandoned .column-header { border-color: #6b7280; color: #6b7280; }
38+
.column-body { padding: 8px; overflow-y: auto; flex: 1; }
39+
40+
/* Cards */
41+
.card {
42+
background: #1a1a1a; border: 1px solid #333; border-radius: 6px;
43+
padding: 10px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.15s;
44+
}
45+
.card:hover { border-color: #555; }
46+
.card-title { font-size: 13px; font-weight: 600; margin-bottom: 6px; word-break: break-word; }
47+
.card-meta { font-size: 11px; color: #888; font-family: 'SF Mono', monospace; }
48+
.card-meta span { margin-right: 10px; }
49+
.card-detail { display: none; margin-top: 8px; font-size: 12px; color: #aaa; border-top: 1px solid #333; padding-top: 8px; }
50+
.card.expanded .card-detail { display: block; }
51+
52+
/* Sidebar */
53+
.sidebar h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 12px; }
54+
.activity-item {
55+
padding: 8px 0; border-bottom: 1px solid #1a1a1a; font-size: 13px;
56+
}
57+
.activity-title { font-weight: 500; }
58+
.activity-time { font-size: 11px; color: #666; font-family: 'SF Mono', monospace; }
59+
60+
/* Refresh indicator */
61+
.refresh { position: fixed; top: 8px; right: 8px; font-size: 11px; color: #444; }
62+
63+
@media (max-width: 900px) {
64+
.main { flex-direction: column; }
65+
.sidebar { width: 100%; border-left: none; border-top: 1px solid #222; max-height: 300px; }
66+
.kanban { flex-wrap: wrap; }
67+
.column { min-width: 180px; }
68+
}
69+
</style>
70+
</head>
71+
<body>
72+
73+
<div class="stats-bar" id="stats-bar">
74+
<div class="stat stat-total"><div class="stat-value" id="stat-total">-</div><div class="stat-label">Total Notes</div></div>
75+
<div class="stat stat-active"><div class="stat-value" id="stat-active">-</div><div class="stat-label">Active Tasks</div></div>
76+
<div class="stat stat-done"><div class="stat-value" id="stat-done">-</div><div class="stat-label">Completed</div></div>
77+
<div class="stat stat-explore"><div class="stat-value" id="stat-explore">-</div><div class="stat-label">Explorations</div></div>
78+
</div>
79+
80+
<div class="main">
81+
<div class="kanban-area">
82+
<div class="kanban">
83+
<div class="column col-active"><div class="column-header">Active <span class="col-count"></span></div><div class="column-body" id="col-active"></div></div>
84+
<div class="column col-blocked"><div class="column-header">Blocked <span class="col-count"></span></div><div class="column-body" id="col-blocked"></div></div>
85+
<div class="column col-done"><div class="column-header">Done <span class="col-count"></span></div><div class="column-body" id="col-done"></div></div>
86+
<div class="column col-abandoned"><div class="column-header">Abandoned <span class="col-count"></span></div><div class="column-body" id="col-abandoned"></div></div>
87+
</div>
88+
</div>
89+
<div class="sidebar">
90+
<h2>Activity Feed</h2>
91+
<div id="activity-feed"></div>
92+
</div>
93+
</div>
94+
95+
<div class="refresh" id="refresh"></div>
96+
97+
<script>
98+
const API = '';
99+
100+
function makeCard(task) {
101+
const fm = task.frontmatter || {};
102+
const status = (fm.status || 'active').toLowerCase();
103+
const step = fm.current_step || '?';
104+
const total = fm.total_steps || '?';
105+
const assigned = fm.assigned_to || '';
106+
const div = document.createElement('div');
107+
div.className = 'card';
108+
div.innerHTML = `
109+
<div class="card-title">${esc(task.title)}</div>
110+
<div class="card-meta">
111+
${assigned ? `<span>👤 ${esc(assigned)}</span>` : ''}
112+
<span>📊 ${esc(String(step))}/${esc(String(total))}</span>
113+
</div>
114+
<div class="card-detail mono">${esc(task.content || '').slice(0, 300)}</div>
115+
`;
116+
div.onclick = () => div.classList.toggle('expanded');
117+
return { el: div, status };
118+
}
119+
120+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
121+
122+
async function fetchJson(url) {
123+
try { const r = await fetch(url); return r.ok ? r.json() : null; } catch { return null; }
124+
}
125+
126+
async function refresh() {
127+
document.getElementById('refresh').textContent = '🔄';
128+
129+
const [tasks, activity, stats] = await Promise.all([
130+
fetchJson(`${API}/api/tasks`),
131+
fetchJson(`${API}/api/activity`),
132+
fetchJson(`${API}/api/stats`),
133+
]);
134+
135+
// Stats
136+
if (stats) {
137+
document.getElementById('stat-total').textContent = stats.totalNotes;
138+
document.getElementById('stat-active').textContent = stats.activeTasks;
139+
document.getElementById('stat-done').textContent = stats.completedTasks;
140+
document.getElementById('stat-explore').textContent = stats.explorations;
141+
}
142+
143+
// Kanban
144+
const cols = { active: [], blocked: [], done: [], abandoned: [] };
145+
if (tasks) {
146+
for (const t of tasks) {
147+
const { el, status } = makeCard(t);
148+
const bucket = cols[status] ? status : 'active';
149+
cols[bucket].push(el);
150+
}
151+
}
152+
for (const [key, items] of Object.entries(cols)) {
153+
const container = document.getElementById(`col-${key}`);
154+
container.innerHTML = '';
155+
for (const el of items) container.appendChild(el);
156+
container.parentElement.querySelector('.col-count').textContent = `(${items.length})`;
157+
}
158+
159+
// Activity
160+
const feed = document.getElementById('activity-feed');
161+
feed.innerHTML = '';
162+
if (activity?.length) {
163+
for (const a of activity.slice(0, 30)) {
164+
const div = document.createElement('div');
165+
div.className = 'activity-item';
166+
const time = a.created_at ? new Date(a.created_at).toLocaleTimeString() : '';
167+
div.innerHTML = `<div class="activity-title">${esc(a.title)}</div><div class="activity-time">${time}</div>`;
168+
feed.appendChild(div);
169+
}
170+
} else {
171+
feed.innerHTML = '<div style="color:#555">No recent activity</div>';
172+
}
173+
174+
document.getElementById('refresh').textContent = '✓';
175+
setTimeout(() => { document.getElementById('refresh').textContent = ''; }, 2000);
176+
}
177+
178+
refresh();
179+
setInterval(refresh, 30000);
180+
</script>
181+
</body>
182+
</html>

0 commit comments

Comments
 (0)