Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
442
admin-ui/js/components/tools/ds-activity-log.js
Normal file
442
admin-ui/js/components/tools/ds-activity-log.js
Normal file
@@ -0,0 +1,442 @@
|
||||
/**
|
||||
* 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 = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.activity-log-container {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.log-controls {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.auto-refresh-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
/* Badge styles */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: rgba(75, 181, 211, 0.2);
|
||||
color: #4bb5d3;
|
||||
}
|
||||
|
||||
.badge-running {
|
||||
background: rgba(75, 181, 211, 0.2);
|
||||
color: #4bb5d3;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(137, 209, 133, 0.2);
|
||||
color: #89d185;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: rgba(244, 135, 113, 0.2);
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: 'Courier New', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.icon-cell {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.error-box {
|
||||
background-color: rgba(244, 135, 113, 0.1);
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
color: #f48771;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="activity-log-container">
|
||||
<!-- Log Controls -->
|
||||
<div class="log-controls">
|
||||
<label class="auto-refresh-label">
|
||||
<input type="checkbox" id="activity-auto-refresh" />
|
||||
Live updates
|
||||
</label>
|
||||
<button
|
||||
id="activity-clear-btn"
|
||||
data-action="clear"
|
||||
class="clear-btn"
|
||||
type="button"
|
||||
aria-label="Clear activity log">
|
||||
🗑️ Clear Log
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-wrapper" id="activity-content">
|
||||
<div class="loading">Loading activities...</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<div class="table-empty"><div class="table-empty-icon">📋</div><div class="table-empty-text">No recent activity</div></div>';
|
||||
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 + '<div class="hint">💡 Click any row to view full activity details</div>';
|
||||
|
||||
// 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 `<span class="icon-cell">${icon}</span>`;
|
||||
|
||||
case 'status':
|
||||
return `<span class="badge badge-${row.status}">${this.escapeHtml(row.status)}</span>`;
|
||||
|
||||
case 'toolName':
|
||||
return `<span class="tool-name">${this.escapeHtml(toolName)}</span>`;
|
||||
|
||||
case 'duration':
|
||||
return `<span style="color: var(--vscode-descriptionForeground);">${duration}</span>`;
|
||||
|
||||
case 'timestamp':
|
||||
return `<span style="color: var(--vscode-descriptionForeground);">${timestamp}</span>`;
|
||||
|
||||
default:
|
||||
return this.escapeHtml(String(row[col.key] || '-'));
|
||||
}
|
||||
}
|
||||
|
||||
renderDetails(row) {
|
||||
const toolName = row.toolName || 'Unknown';
|
||||
const duration = row.duration ? ComponentHelpers.formatDuration(row.duration) : '-';
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 12px;">
|
||||
<span class="detail-label">Tool:</span>
|
||||
<span class="detail-value code">${this.escapeHtml(toolName)}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="badge badge-${row.status}">${this.escapeHtml(row.status)}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<span class="detail-label">Timestamp:</span>
|
||||
<span>${ComponentHelpers.formatTimestamp(row.timestamp)}</span>
|
||||
</div>
|
||||
${row.duration ? `
|
||||
<div style="margin-bottom: 12px;">
|
||||
<span class="detail-label">Duration:</span>
|
||||
<span>${duration}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${row.params && Object.keys(row.params).length > 0 ? `
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div class="detail-label" style="display: block; margin-bottom: 4px;">Parameters:</div>
|
||||
<pre class="detail-code">${this.escapeHtml(JSON.stringify(row.params, null, 2))}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
${row.error ? `
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div class="detail-label" style="display: block; margin-bottom: 4px;">Error:</div>
|
||||
<div class="error-box">
|
||||
${this.escapeHtml(row.error)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-activity-log', DSActivityLog);
|
||||
|
||||
export default DSActivityLog;
|
||||
Reference in New Issue
Block a user