fix(dashboard): enhance live activity formatting, add empty state placeholder, and add API unit tests
This commit is contained in:
1 parent
19f9c7ace7
commit
da3700d1d5
3 files changed
+164
-21
No files matched your search
@@ -713,6 +713,42 @@ mod tests {
|
|||||||
assert!(js_str.contains("refreshActiveTab"));
|
assert!(js_str.contains("refreshActiveTab"));
|
||||||
assert!(js_str.contains("parseActivityPayload"));
|
assert!(js_str.contains("parseActivityPayload"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_activity_feed_and_empty_state() {
|
||||||
|
let (_app, app_state, _dir) = setup_app().await;
|
||||||
|
|
||||||
|
// 1. Initial GET /api/activity should return empty array []
|
||||||
|
let app_act = create_router(app_state.clone());
|
||||||
|
let req_act = Request::builder().uri("/api/activity").body(Body::empty()).unwrap();
|
||||||
|
let resp_act = app_act.oneshot(req_act).await.unwrap();
|
||||||
|
assert_eq!(resp_act.status(), StatusCode::OK);
|
||||||
|
let body_bytes = axum::body::to_bytes(resp_act.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let json_str = String::from_utf8(body_bytes.to_vec()).unwrap();
|
||||||
|
assert_eq!(json_str.trim(), "[]");
|
||||||
|
|
||||||
|
// 2. Record an activity
|
||||||
|
app_state.handler.state.record_activity("code_change", "Refactored live activity UI", Some("Added empty state placeholder"));
|
||||||
|
|
||||||
|
// 3. GET /api/activity should now return recorded item
|
||||||
|
let app_act2 = create_router(app_state.clone());
|
||||||
|
let req_act2 = Request::builder().uri("/api/activity").body(Body::empty()).unwrap();
|
||||||
|
let resp_act2 = app_act2.oneshot(req_act2).await.unwrap();
|
||||||
|
assert_eq!(resp_act2.status(), StatusCode::OK);
|
||||||
|
let body_bytes2 = axum::body::to_bytes(resp_act2.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let json_str2 = String::from_utf8(body_bytes2.to_vec()).unwrap();
|
||||||
|
assert!(json_str2.contains("CODE_CHANGE"));
|
||||||
|
assert!(json_str2.contains("Refactored live activity UI"));
|
||||||
|
|
||||||
|
// 4. Verify /dashboard.js contains empty state text
|
||||||
|
let app_js = create_router(app_state.clone());
|
||||||
|
let req_js = Request::builder().uri("/dashboard.js").body(Body::empty()).unwrap();
|
||||||
|
let resp_js = app_js.oneshot(req_js).await.unwrap();
|
||||||
|
assert_eq!(resp_js.status(), StatusCode::OK);
|
||||||
|
let js_bytes = axum::body::to_bytes(resp_js.into_body(), usize::MAX).await.unwrap();
|
||||||
|
let js_str = String::from_utf8(js_bytes.to_vec()).unwrap();
|
||||||
|
assert!(js_str.contains("No recent activity recorded yet."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+64
-10
@@ -344,8 +344,40 @@ function parseActivityPayload(item) {
|
|||||||
return parseActivityPayload(item.params);
|
return parseActivityPayload(item.params);
|
||||||
}
|
}
|
||||||
category = (item.category || item.type || "TOOL").toUpperCase();
|
category = (item.category || item.type || "TOOL").toUpperCase();
|
||||||
summary = item.summary || item.message || item.description || item.data || "";
|
let rawSum = item.summary || item.message || item.description || item.data;
|
||||||
details = item.details || "";
|
if (rawSum !== undefined && rawSum !== null) {
|
||||||
|
if (typeof rawSum === "object") {
|
||||||
|
summary = JSON.stringify(rawSum);
|
||||||
|
} else {
|
||||||
|
summary = String(rawSum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!summary) {
|
||||||
|
if (item.title)
|
||||||
|
summary = String(item.title);
|
||||||
|
else if (item.name)
|
||||||
|
summary = String(item.name);
|
||||||
|
else if (item.action)
|
||||||
|
summary = String(item.action);
|
||||||
|
else if (item.content)
|
||||||
|
summary = String(item.content);
|
||||||
|
else {
|
||||||
|
const { timestamp: _t, time: _tm, updated_at: _u, category: _c, type: _tp, method: _m, params: _p, jsonrpc: _j, ...rest } = item;
|
||||||
|
if (Object.keys(rest).length > 0) {
|
||||||
|
summary = JSON.stringify(rest);
|
||||||
|
} else {
|
||||||
|
summary = JSON.stringify(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rawDet = item.details;
|
||||||
|
if (rawDet !== undefined && rawDet !== null) {
|
||||||
|
if (typeof rawDet === "object") {
|
||||||
|
details = JSON.stringify(rawDet);
|
||||||
|
} else {
|
||||||
|
details = String(rawDet);
|
||||||
|
}
|
||||||
|
}
|
||||||
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
|
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
|
||||||
if (typeof timestamp === "number" && timestamp < 10000000000) {
|
if (typeof timestamp === "number" && timestamp < 10000000000) {
|
||||||
timestamp = timestamp * 1000;
|
timestamp = timestamp * 1000;
|
||||||
@@ -356,15 +388,23 @@ function parseActivityPayload(item) {
|
|||||||
GRAPH: "#2ecc71",
|
GRAPH: "#2ecc71",
|
||||||
DECISION: "#f39c12",
|
DECISION: "#f39c12",
|
||||||
CODE: "#3498db",
|
CODE: "#3498db",
|
||||||
|
CODE_CHANGE: "#3498db",
|
||||||
TASK: "#1abc9c",
|
TASK: "#1abc9c",
|
||||||
|
TASK_CREATE: "#1abc9c",
|
||||||
|
TASK_UPDATE: "#1abc9c",
|
||||||
CLIPBOARD: "#9b59b6",
|
CLIPBOARD: "#9b59b6",
|
||||||
STICKY_NOTE: "#e67e22",
|
STICKY_NOTE: "#e67e22",
|
||||||
CHECKPOINT: "#e74c3c",
|
CHECKPOINT: "#e74c3c",
|
||||||
ERROR_FIX: "#e74c3c",
|
ERROR_FIX: "#e74c3c",
|
||||||
TECH_DEBT: "#d35400",
|
TECH_DEBT: "#d35400",
|
||||||
SUBAGENT: "#8e44ad",
|
SUBAGENT: "#8e44ad",
|
||||||
|
AGENT_SIGNAL: "#8e44ad",
|
||||||
SNIPPET: "#16a085",
|
SNIPPET: "#16a085",
|
||||||
TOOL: "#3498db",
|
TOOL: "#3498db",
|
||||||
|
EDITOR: "#9b59b6",
|
||||||
|
TERMINAL: "#27ae60",
|
||||||
|
SNAPSHOT: "#f1c40f",
|
||||||
|
NAMESPACE: "#e67e22",
|
||||||
SYSTEM: "#95a5a6"
|
SYSTEM: "#95a5a6"
|
||||||
};
|
};
|
||||||
const badgeColor = colorMap[catUpper] || "#3498db";
|
const badgeColor = colorMap[catUpper] || "#3498db";
|
||||||
@@ -391,6 +431,10 @@ async function loadActivityHistory() {
|
|||||||
if (!feed)
|
if (!feed)
|
||||||
return;
|
return;
|
||||||
feed.innerHTML = "";
|
feed.innerHTML = "";
|
||||||
|
if (!history || history.length === 0) {
|
||||||
|
feed.innerHTML = '<div style="padding:40px 20px; color:#a4b0be; text-align:center; font-family:sans-serif;">No recent activity recorded yet.<br/><span style="font-size:0.85em; opacity:0.8;">Live activity will stream here in real-time as MCP tools and system events execute.</span></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const getMillis = (item) => {
|
const getMillis = (item) => {
|
||||||
if (!item)
|
if (!item)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -427,18 +471,23 @@ async function loadActivityHistory() {
|
|||||||
function setupWS() {
|
function setupWS() {
|
||||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`);
|
const ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`);
|
||||||
const feed = document.getElementById("activity-feed");
|
|
||||||
ws.onmessage = function(event) {
|
ws.onmessage = function(event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (data.type === "activity" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") {
|
if (data.type === "activity" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") {
|
||||||
refreshActiveTab();
|
if (currentTabId !== "activity-tab") {
|
||||||
const isScrolledToTop = feed ? feed.scrollTop <= 20 : true;
|
refreshActiveTab();
|
||||||
const div = document.createElement("div");
|
}
|
||||||
div.className = "feed-entry";
|
const feed = document.getElementById("activity-feed");
|
||||||
const payload = data.params || data.data || data;
|
|
||||||
div.innerHTML = parseActivityPayload(payload);
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
if (feed.querySelector(".feed-entry") === null) {
|
||||||
|
feed.innerHTML = "";
|
||||||
|
}
|
||||||
|
const isScrolledToTop = feed.scrollTop <= 20;
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "feed-entry";
|
||||||
|
const payload = data.params || data.data || data;
|
||||||
|
div.innerHTML = parseActivityPayload(payload);
|
||||||
feed.prepend(div);
|
feed.prepend(div);
|
||||||
while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) {
|
while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) {
|
||||||
feed.removeChild(feed.lastChild);
|
feed.removeChild(feed.lastChild);
|
||||||
@@ -619,9 +668,14 @@ function setupSSE() {
|
|||||||
sse.onmessage = function(event) {
|
sse.onmessage = function(event) {
|
||||||
if (event.data) {
|
if (event.data) {
|
||||||
try {
|
try {
|
||||||
refreshActiveTab();
|
if (currentTabId !== "activity-tab") {
|
||||||
|
refreshActiveTab();
|
||||||
|
}
|
||||||
const feed = document.getElementById("activity-feed");
|
const feed = document.getElementById("activity-feed");
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
if (feed.querySelector(".feed-entry") === null) {
|
||||||
|
feed.innerHTML = "";
|
||||||
|
}
|
||||||
const isScrolledToTop = feed.scrollTop <= 20;
|
const isScrolledToTop = feed.scrollTop <= 20;
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "feed-entry";
|
div.className = "feed-entry";
|
||||||
|
|||||||
+64
-11
@@ -538,8 +538,39 @@ function parseActivityPayload(item: any): string {
|
|||||||
return parseActivityPayload(item.params);
|
return parseActivityPayload(item.params);
|
||||||
}
|
}
|
||||||
category = (item.category || item.type || 'TOOL').toUpperCase();
|
category = (item.category || item.type || 'TOOL').toUpperCase();
|
||||||
summary = item.summary || item.message || item.description || item.data || '';
|
|
||||||
details = item.details || '';
|
let rawSum = item.summary || item.message || item.description || item.data;
|
||||||
|
if (rawSum !== undefined && rawSum !== null) {
|
||||||
|
if (typeof rawSum === 'object') {
|
||||||
|
summary = JSON.stringify(rawSum);
|
||||||
|
} else {
|
||||||
|
summary = String(rawSum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!summary) {
|
||||||
|
if (item.title) summary = String(item.title);
|
||||||
|
else if (item.name) summary = String(item.name);
|
||||||
|
else if (item.action) summary = String(item.action);
|
||||||
|
else if (item.content) summary = String(item.content);
|
||||||
|
else {
|
||||||
|
const { timestamp: _t, time: _tm, updated_at: _u, category: _c, type: _tp, method: _m, params: _p, jsonrpc: _j, ...rest } = item;
|
||||||
|
if (Object.keys(rest).length > 0) {
|
||||||
|
summary = JSON.stringify(rest);
|
||||||
|
} else {
|
||||||
|
summary = JSON.stringify(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawDet = item.details;
|
||||||
|
if (rawDet !== undefined && rawDet !== null) {
|
||||||
|
if (typeof rawDet === 'object') {
|
||||||
|
details = JSON.stringify(rawDet);
|
||||||
|
} else {
|
||||||
|
details = String(rawDet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
|
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
|
||||||
|
|
||||||
if (typeof timestamp === 'number' && timestamp < 10000000000) {
|
if (typeof timestamp === 'number' && timestamp < 10000000000) {
|
||||||
@@ -552,15 +583,23 @@ function parseActivityPayload(item: any): string {
|
|||||||
'GRAPH': '#2ecc71',
|
'GRAPH': '#2ecc71',
|
||||||
'DECISION': '#f39c12',
|
'DECISION': '#f39c12',
|
||||||
'CODE': '#3498db',
|
'CODE': '#3498db',
|
||||||
|
'CODE_CHANGE': '#3498db',
|
||||||
'TASK': '#1abc9c',
|
'TASK': '#1abc9c',
|
||||||
|
'TASK_CREATE': '#1abc9c',
|
||||||
|
'TASK_UPDATE': '#1abc9c',
|
||||||
'CLIPBOARD': '#9b59b6',
|
'CLIPBOARD': '#9b59b6',
|
||||||
'STICKY_NOTE': '#e67e22',
|
'STICKY_NOTE': '#e67e22',
|
||||||
'CHECKPOINT': '#e74c3c',
|
'CHECKPOINT': '#e74c3c',
|
||||||
'ERROR_FIX': '#e74c3c',
|
'ERROR_FIX': '#e74c3c',
|
||||||
'TECH_DEBT': '#d35400',
|
'TECH_DEBT': '#d35400',
|
||||||
'SUBAGENT': '#8e44ad',
|
'SUBAGENT': '#8e44ad',
|
||||||
|
'AGENT_SIGNAL': '#8e44ad',
|
||||||
'SNIPPET': '#16a085',
|
'SNIPPET': '#16a085',
|
||||||
'TOOL': '#3498db',
|
'TOOL': '#3498db',
|
||||||
|
'EDITOR': '#9b59b6',
|
||||||
|
'TERMINAL': '#27ae60',
|
||||||
|
'SNAPSHOT': '#f1c40f',
|
||||||
|
'NAMESPACE': '#e67e22',
|
||||||
'SYSTEM': '#95a5a6'
|
'SYSTEM': '#95a5a6'
|
||||||
};
|
};
|
||||||
const badgeColor = colorMap[catUpper] || '#3498db';
|
const badgeColor = colorMap[catUpper] || '#3498db';
|
||||||
@@ -586,6 +625,11 @@ async function loadActivityHistory(): Promise<void> {
|
|||||||
if (!feed) return;
|
if (!feed) return;
|
||||||
feed.innerHTML = '';
|
feed.innerHTML = '';
|
||||||
|
|
||||||
|
if (!history || history.length === 0) {
|
||||||
|
feed.innerHTML = '<div style="padding:40px 20px; color:#a4b0be; text-align:center; font-family:sans-serif;">No recent activity recorded yet.<br/><span style="font-size:0.85em; opacity:0.8;">Live activity will stream here in real-time as MCP tools and system events execute.</span></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const getMillis = (item: any): number => {
|
const getMillis = (item: any): number => {
|
||||||
if (!item) return 0;
|
if (!item) return 0;
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
@@ -620,20 +664,24 @@ async function loadActivityHistory(): Promise<void> {
|
|||||||
function setupWS(): void {
|
function setupWS(): void {
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`);
|
const ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`);
|
||||||
const feed = document.getElementById('activity-feed');
|
|
||||||
|
|
||||||
ws.onmessage = function(event: MessageEvent) {
|
ws.onmessage = function(event: MessageEvent) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (data.type === 'activity' || data.method === 'notifications/activity' || data.method === 'notifications/task/completed' || data.method === 'notifications/resources/updated') {
|
if (data.type === 'activity' || data.method === 'notifications/activity' || data.method === 'notifications/task/completed' || data.method === 'notifications/resources/updated') {
|
||||||
refreshActiveTab();
|
if (currentTabId !== 'activity-tab') {
|
||||||
const isScrolledToTop = feed ? feed.scrollTop <= 20 : true;
|
refreshActiveTab();
|
||||||
|
}
|
||||||
const div = document.createElement('div');
|
const feed = document.getElementById('activity-feed');
|
||||||
div.className = 'feed-entry';
|
|
||||||
const payload = data.params || data.data || data;
|
|
||||||
div.innerHTML = parseActivityPayload(payload);
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
if (feed.querySelector('.feed-entry') === null) {
|
||||||
|
feed.innerHTML = '';
|
||||||
|
}
|
||||||
|
const isScrolledToTop = feed.scrollTop <= 20;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'feed-entry';
|
||||||
|
const payload = data.params || data.data || data;
|
||||||
|
div.innerHTML = parseActivityPayload(payload);
|
||||||
feed.prepend(div);
|
feed.prepend(div);
|
||||||
while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) {
|
while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) {
|
||||||
feed.removeChild(feed.lastChild);
|
feed.removeChild(feed.lastChild);
|
||||||
@@ -871,9 +919,14 @@ function setupSSE(): void {
|
|||||||
sse.onmessage = function(event: MessageEvent) {
|
sse.onmessage = function(event: MessageEvent) {
|
||||||
if (event.data) {
|
if (event.data) {
|
||||||
try {
|
try {
|
||||||
refreshActiveTab();
|
if (currentTabId !== 'activity-tab') {
|
||||||
|
refreshActiveTab();
|
||||||
|
}
|
||||||
const feed = document.getElementById('activity-feed');
|
const feed = document.getElementById('activity-feed');
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
if (feed.querySelector('.feed-entry') === null) {
|
||||||
|
feed.innerHTML = '';
|
||||||
|
}
|
||||||
const isScrolledToTop = feed.scrollTop <= 20;
|
const isScrolledToTop = feed.scrollTop <= 20;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'feed-entry';
|
div.className = 'feed-entry';
|
||||||
|
|||||||
Reference in new issue
Block a user