feat: swarm E2E, MCP tools, timmy-serve L402, tests, notifications
Major Features: - Auto-spawn persona agents (Echo, Forge, Seer) on app startup - WebSocket broadcasts for real-time swarm UI updates - MCP tool integration: web search, file I/O, shell, Python execution - New /tools dashboard page showing agent capabilities - Real timmy-serve start with L402 payment gating middleware - Browser push notifications for briefings and task events Tests: - test_docker_agent.py: 9 tests for Docker agent runner - test_swarm_integration_full.py: 18 E2E lifecycle tests - Fixed all pytest warnings (436 tests, 0 warnings) Improvements: - Fixed coroutine warnings in coordinator broadcasts - Fixed ResourceWarning for unclosed process pipes - Added pytest-asyncio config to pyproject.toml - Test isolation with proper event loop cleanup
This commit is contained in:
227
static/notifications.js
Normal file
227
static/notifications.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Browser Push Notifications for Timmy Time Dashboard
|
||||
*
|
||||
* Handles browser Notification API integration for:
|
||||
* - Briefing ready notifications
|
||||
* - Task completion notifications
|
||||
* - Swarm event notifications
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Notification state
|
||||
let notificationsEnabled = false;
|
||||
let wsConnection = null;
|
||||
|
||||
/**
|
||||
* Request permission for browser notifications
|
||||
*/
|
||||
async function requestNotificationPermission() {
|
||||
if (!('Notification' in window)) {
|
||||
console.log('Browser notifications not supported');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
notificationsEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'denied') {
|
||||
console.log('Notification permission denied');
|
||||
return false;
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
notificationsEnabled = permission === 'granted';
|
||||
return notificationsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a browser notification
|
||||
*/
|
||||
function showNotification(title, options = {}) {
|
||||
if (!notificationsEnabled || Notification.permission !== 'granted') {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
icon: '/static/favicon.ico',
|
||||
badge: '/static/favicon.ico',
|
||||
tag: 'timmy-notification',
|
||||
requireInteraction: false,
|
||||
};
|
||||
|
||||
const notification = new Notification(title, { ...defaultOptions, ...options });
|
||||
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
};
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show briefing ready notification
|
||||
*/
|
||||
function notifyBriefingReady(briefingInfo = {}) {
|
||||
const approvalCount = briefingInfo.approval_count || 0;
|
||||
const body = approvalCount > 0
|
||||
? `Your morning briefing is ready. ${approvalCount} item(s) await your approval.`
|
||||
: 'Your morning briefing is ready.';
|
||||
|
||||
showNotification('Morning Briefing Ready', {
|
||||
body,
|
||||
tag: 'briefing-ready',
|
||||
requireInteraction: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show task completed notification
|
||||
*/
|
||||
function notifyTaskCompleted(taskInfo = {}) {
|
||||
const { task_id, agent_name, result } = taskInfo;
|
||||
const body = result
|
||||
? `Task completed by ${agent_name || 'agent'}: ${result.substring(0, 100)}${result.length > 100 ? '...' : ''}`
|
||||
: `Task ${task_id?.substring(0, 8)} completed by ${agent_name || 'agent'}`;
|
||||
|
||||
showNotification('Task Completed', {
|
||||
body,
|
||||
tag: `task-${task_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show agent joined notification
|
||||
*/
|
||||
function notifyAgentJoined(agentInfo = {}) {
|
||||
const { name, agent_id } = agentInfo;
|
||||
showNotification('Agent Joined Swarm', {
|
||||
body: `${name || 'New agent'} (${agent_id?.substring(0, 8)}) has joined the swarm.`,
|
||||
tag: `agent-joined-${agent_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show task assigned notification
|
||||
*/
|
||||
function notifyTaskAssigned(taskInfo = {}) {
|
||||
const { task_id, agent_name } = taskInfo;
|
||||
showNotification('Task Assigned', {
|
||||
body: `Task assigned to ${agent_name || 'agent'}`,
|
||||
tag: `task-assigned-${task_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to WebSocket for real-time notifications
|
||||
*/
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/swarm/live`;
|
||||
|
||||
wsConnection = new WebSocket(wsUrl);
|
||||
|
||||
wsConnection.onopen = () => {
|
||||
console.log('WebSocket connected for notifications');
|
||||
};
|
||||
|
||||
wsConnection.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
handleWebSocketEvent(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse WebSocket message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
wsConnection.onclose = () => {
|
||||
console.log('WebSocket disconnected, retrying in 5s...');
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
|
||||
wsConnection.onerror = (err) => {
|
||||
console.error('WebSocket error:', err);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebSocket events and trigger notifications
|
||||
*/
|
||||
function handleWebSocketEvent(event) {
|
||||
if (!notificationsEnabled) return;
|
||||
|
||||
switch (event.event) {
|
||||
case 'briefing_ready':
|
||||
notifyBriefingReady(event.data);
|
||||
break;
|
||||
case 'task_completed':
|
||||
notifyTaskCompleted(event.data);
|
||||
break;
|
||||
case 'agent_joined':
|
||||
notifyAgentJoined(event.data);
|
||||
break;
|
||||
case 'task_assigned':
|
||||
notifyTaskAssigned(event.data);
|
||||
break;
|
||||
default:
|
||||
// Unknown event type, ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize notifications system
|
||||
*/
|
||||
async function init() {
|
||||
// Request permission on user interaction
|
||||
const enableBtn = document.getElementById('enable-notifications');
|
||||
if (enableBtn) {
|
||||
enableBtn.addEventListener('click', async () => {
|
||||
const granted = await requestNotificationPermission();
|
||||
if (granted) {
|
||||
enableBtn.textContent = 'Notifications Enabled';
|
||||
enableBtn.disabled = true;
|
||||
connectWebSocket();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-request if permission was previously granted
|
||||
if (Notification.permission === 'granted') {
|
||||
notificationsEnabled = true;
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
// Listen for briefing ready events via custom event
|
||||
document.addEventListener('briefing-ready', (e) => {
|
||||
notifyBriefingReady(e.detail);
|
||||
});
|
||||
|
||||
// Listen for task completion events
|
||||
document.addEventListener('task-completed', (e) => {
|
||||
notifyTaskCompleted(e.detail);
|
||||
});
|
||||
}
|
||||
|
||||
// Expose public API
|
||||
window.TimmyNotifications = {
|
||||
requestPermission: requestNotificationPermission,
|
||||
show: showNotification,
|
||||
notifyBriefingReady,
|
||||
notifyTaskCompleted,
|
||||
notifyAgentJoined,
|
||||
notifyTaskAssigned,
|
||||
isEnabled: () => notificationsEnabled,
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user