Files
Claude-Code-Workflow/ccw/src/templates/dashboard-js/components/notifications.js
catlog22 43c962b48b feat: Add Notifications Component with WebSocket and Auto Refresh
- Implemented a Notifications component for real-time updates using WebSocket.
- Added silent refresh functionality to update data without notification bubbles.
- Introduced auto-refresh mechanism to periodically check for changes in workflow data.
- Enhanced data handling with session and task updates, ensuring UI reflects the latest state.

feat: Create Hook Manager View for Managing Hooks

- Developed a Hook Manager view to manage project and global hooks.
- Added functionality to create, edit, and delete hooks with a user-friendly interface.
- Implemented quick install templates for common hooks to streamline user experience.
- Included environment variables reference for hooks to assist users in configuration.

feat: Implement MCP Manager View for Server Management

- Created an MCP Manager view for managing MCP servers within projects.
- Enabled adding and removing servers from projects with a clear UI.
- Displayed available servers from other projects for easy access and management.
- Provided an overview of all projects and their associated MCP servers.

feat: Add Version Fetcher Utility for GitHub Releases

- Implemented a version fetcher utility to retrieve release information from GitHub.
- Added functions to fetch the latest release, recent releases, and latest commit details.
- Included functionality to download and extract repository zip files.
- Ensured cleanup of temporary directories after downloads to maintain system hygiene.
2025-12-07 15:48:39 +08:00

195 lines
5.3 KiB
JavaScript

// ==========================================
// NOTIFICATIONS COMPONENT
// ==========================================
// Real-time silent refresh (no notification bubbles)
let wsConnection = null;
let autoRefreshInterval = null;
let lastDataHash = null;
const AUTO_REFRESH_INTERVAL_MS = 30000; // 30 seconds
// ========== WebSocket Connection ==========
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
try {
wsConnection = new WebSocket(wsUrl);
wsConnection.onopen = () => {
console.log('[WS] Connected');
};
wsConnection.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleNotification(data);
} catch (e) {
console.error('[WS] Failed to parse message:', e);
}
};
wsConnection.onclose = () => {
console.log('[WS] Disconnected, reconnecting in 5s...');
setTimeout(initWebSocket, 5000);
};
wsConnection.onerror = (error) => {
console.error('[WS] Error:', error);
};
} catch (e) {
console.log('[WS] WebSocket not available, using polling');
}
}
// ========== Notification Handler ==========
function handleNotification(data) {
const { type, payload } = data;
// Silent refresh - no notification bubbles
switch (type) {
case 'session_updated':
case 'summary_written':
case 'task_completed':
case 'new_session':
// Just refresh data silently
refreshIfNeeded();
// Optionally highlight in carousel if it's the current session
if (payload.sessionId && typeof carouselGoTo === 'function') {
carouselGoTo(payload.sessionId);
}
break;
default:
console.log('[WS] Unknown notification type:', type);
}
}
// ========== Auto Refresh ==========
function initAutoRefresh() {
// Calculate initial hash
lastDataHash = calculateDataHash();
// Start polling interval
autoRefreshInterval = setInterval(checkForChanges, AUTO_REFRESH_INTERVAL_MS);
}
function calculateDataHash() {
if (!workflowData) return null;
// Simple hash based on key data points
const hashData = {
activeSessions: (workflowData.activeSessions || []).length,
archivedSessions: (workflowData.archivedSessions || []).length,
totalTasks: workflowData.statistics?.totalTasks || 0,
completedTasks: workflowData.statistics?.completedTasks || 0,
generatedAt: workflowData.generatedAt
};
return JSON.stringify(hashData);
}
async function checkForChanges() {
if (!window.SERVER_MODE) return;
try {
const response = await fetch(`/api/data?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) return;
const newData = await response.json();
const newHash = JSON.stringify({
activeSessions: (newData.activeSessions || []).length,
archivedSessions: (newData.archivedSessions || []).length,
totalTasks: newData.statistics?.totalTasks || 0,
completedTasks: newData.statistics?.completedTasks || 0,
generatedAt: newData.generatedAt
});
if (newHash !== lastDataHash) {
lastDataHash = newHash;
// Silent refresh - no notification
await refreshWorkspaceData(newData);
}
} catch (e) {
console.error('[AutoRefresh] Check failed:', e);
}
}
async function refreshIfNeeded() {
if (!window.SERVER_MODE) return;
try {
const response = await fetch(`/api/data?path=${encodeURIComponent(projectPath)}`);
if (!response.ok) return;
const newData = await response.json();
await refreshWorkspaceData(newData);
} catch (e) {
console.error('[Refresh] Failed:', e);
}
}
async function refreshWorkspaceData(newData) {
// Update global data
window.workflowData = newData;
// Clear and repopulate stores
Object.keys(sessionDataStore).forEach(k => delete sessionDataStore[k]);
Object.keys(liteTaskDataStore).forEach(k => delete liteTaskDataStore[k]);
[...(newData.activeSessions || []), ...(newData.archivedSessions || [])].forEach(s => {
const key = `session-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[key] = s;
});
[...(newData.liteTasks?.litePlan || []), ...(newData.liteTasks?.liteFix || [])].forEach(s => {
const key = `lite-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[key] = s;
});
// Update UI silently
updateStats();
updateBadges();
updateCarousel();
// Re-render current view if needed
if (currentView === 'sessions') {
renderSessions();
} else if (currentView === 'liteTasks') {
renderLiteTasks();
}
lastDataHash = calculateDataHash();
}
// ========== Cleanup ==========
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
}
function closeWebSocket() {
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
}
// ========== Navigation Helper ==========
function goToSession(sessionId) {
// Find session in carousel and navigate
const sessionKey = `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-');
// Jump to session in carousel if visible
if (typeof carouselGoTo === 'function') {
carouselGoTo(sessionId);
}
// Navigate to session detail
if (sessionDataStore[sessionKey]) {
showSessionDetailPage(sessionKey);
}
}