/** * ds-activity-log.js * Activity log showing recent MCP tool executions and user actions * * REFACTORED: DSS-compliant version using DSBaseTool + table-template.js * - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle * - Uses table-template.js for DSS-compliant table rendering (NO inline events/styles) * - Event delegation pattern for all interactions * - Logger utility instead of console.* * * Reference: .knowledge/dss-coding-standards.json */ import DSBaseTool from '../base/ds-base-tool.js'; import toolBridge from '../../services/tool-bridge.js'; import { ComponentHelpers } from '../../utils/component-helpers.js'; import { logger } from '../../utils/logger.js'; import { createTableView, setupTableEvents, createStatsCard } from '../../templates/table-template.js'; class DSActivityLog extends DSBaseTool { constructor() { super(); this.activities = []; this.maxActivities = 100; this.autoRefresh = false; this.refreshInterval = null; // Listen for tool executions this.originalExecuteTool = toolBridge.executeTool.bind(toolBridge); this.setupToolInterceptor(); } connectedCallback() { super.connectedCallback(); this.loadActivities(); } disconnectedCallback() { if (this.refreshInterval) { clearInterval(this.refreshInterval); } super.disconnectedCallback(); } setupToolInterceptor() { // Intercept tool executions to log them toolBridge.executeTool = async (toolName, params) => { const startTime = Date.now(); const activity = { id: Date.now() + Math.random(), type: 'tool_execution', toolName, params, timestamp: new Date(), status: 'running' }; this.addActivity(activity); try { const result = await this.originalExecuteTool(toolName, params); const duration = Date.now() - startTime; activity.status = 'success'; activity.duration = duration; activity.result = result; this.updateActivity(activity); return result; } catch (error) { const duration = Date.now() - startTime; activity.status = 'error'; activity.duration = duration; activity.error = error.message; this.updateActivity(activity); throw error; } }; } addActivity(activity) { this.activities.unshift(activity); if (this.activities.length > this.maxActivities) { this.activities.pop(); } this.saveActivities(); this.renderActivities(); } updateActivity(activity) { const index = this.activities.findIndex(a => a.id === activity.id); if (index !== -1) { this.activities[index] = activity; this.saveActivities(); this.renderActivities(); } } saveActivities() { try { localStorage.setItem('ds-activity-log', JSON.stringify(this.activities.slice(0, 50))); } catch (e) { logger.warn('[DSActivityLog] Failed to save activities to localStorage', e); } } loadActivities() { try { const stored = localStorage.getItem('ds-activity-log'); if (stored) { this.activities = JSON.parse(stored).map(a => ({ ...a, timestamp: new Date(a.timestamp) })); this.renderActivities(); logger.debug('[DSActivityLog] Loaded activities from localStorage', { count: this.activities.length }); } } catch (e) { logger.warn('[DSActivityLog] Failed to load activities from localStorage', e); } } clearActivities() { this.activities = []; this.saveActivities(); this.renderActivities(); logger.info('[DSActivityLog] Activities cleared'); } /** * Render the component (required by DSBaseTool) */ render() { this.shadowRoot.innerHTML = `
Loading activities...
`; } /** * Setup event listeners (required by DSBaseTool) */ setupEventListeners() { // EVENT-002: Event delegation this.delegateEvents('.activity-log-container', 'click', (action, e) => { if (action === 'clear') { this.clearActivities(); } }); // Auto-refresh toggle const autoRefreshToggle = this.$('#activity-auto-refresh'); if (autoRefreshToggle) { this.bindEvent(autoRefreshToggle, 'change', (e) => { this.autoRefresh = e.target.checked; if (this.autoRefresh) { this.refreshInterval = setInterval(() => this.renderActivities(), 1000); logger.debug('[DSActivityLog] Auto-refresh enabled'); } else { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; logger.debug('[DSActivityLog] Auto-refresh disabled'); } } }); } } getActivityIcon(activity) { if (activity.status === 'running') return '🔄'; if (activity.status === 'success') return '✅'; if (activity.status === 'error') return '❌'; return '⚪'; } renderActivities() { const content = this.$('#activity-content'); if (!content) return; if (this.activities.length === 0) { content.innerHTML = '
📋
No recent activity
'; return; } // Calculate stats const stats = { Total: this.activities.length, Success: this.activities.filter(a => a.status === 'success').length, Failed: this.activities.filter(a => a.status === 'error').length }; const running = this.activities.filter(a => a.status === 'running').length; if (running > 0) { stats.Running = running; } // Render stats card const statsHtml = createStatsCard(stats); // Use table-template.js for DSS-compliant rendering const { html: tableHtml, styles: tableStyles } = createTableView({ columns: [ { header: '', key: 'icon', width: '40px', align: 'center' }, { header: 'Status', key: 'status', width: '80px', align: 'left' }, { header: 'Tool', key: 'toolName', align: 'left' }, { header: 'Duration', key: 'duration', width: '100px', align: 'left' }, { header: 'Time', key: 'timestamp', width: '120px', align: 'left' } ], rows: this.activities, renderCell: (col, row) => this.renderCell(col, row), renderDetails: (row) => this.renderDetails(row), emptyMessage: 'No recent activity', emptyIcon: '📋' }); // Adopt table styles this.adoptStyles(tableStyles); // Render table content.innerHTML = statsHtml + tableHtml + '
💡 Click any row to view full activity details
'; // Setup table event handlers setupTableEvents(this.shadowRoot); logger.debug('[DSActivityLog] Rendered activities', { count: this.activities.length }); } renderCell(col, row) { const icon = this.getActivityIcon(row); const toolName = row.toolName || 'Unknown'; const duration = row.duration ? ComponentHelpers.formatDuration(row.duration) : '-'; const timestamp = ComponentHelpers.formatRelativeTime(row.timestamp); switch (col.key) { case 'icon': return `${icon}`; case 'status': return `${this.escapeHtml(row.status)}`; case 'toolName': return `${this.escapeHtml(toolName)}`; case 'duration': return `${duration}`; case 'timestamp': return `${timestamp}`; default: return this.escapeHtml(String(row[col.key] || '-')); } } renderDetails(row) { const toolName = row.toolName || 'Unknown'; const duration = row.duration ? ComponentHelpers.formatDuration(row.duration) : '-'; return `
Tool: ${this.escapeHtml(toolName)}
Status: ${this.escapeHtml(row.status)}
Timestamp: ${ComponentHelpers.formatTimestamp(row.timestamp)}
${row.duration ? `
Duration: ${duration}
` : ''} ${row.params && Object.keys(row.params).length > 0 ? `
Parameters:
${this.escapeHtml(JSON.stringify(row.params, null, 2))}
` : ''} ${row.error ? `
Error:
${this.escapeHtml(row.error)}
` : ''} `; } } customElements.define('ds-activity-log', DSActivityLog); export default DSActivityLog;