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:
268
admin-ui/js/services/api-client.js
Normal file
268
admin-ui/js/services/api-client.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* api-client.js
|
||||
* Unified API client for backend communication
|
||||
* Handles authentication, request/response formatting, error handling
|
||||
* Supports incognito mode with sessionStorage fallback (lazy-initialized)
|
||||
*/
|
||||
|
||||
class ApiClient {
|
||||
constructor(baseURL = null) {
|
||||
// Use relative path /api which works for both production (proxied through nginx)
|
||||
// and development (proxied through Vite to backend on localhost:8002)
|
||||
this.baseURL = baseURL || '/api';
|
||||
this.accessToken = null;
|
||||
this.refreshToken = null;
|
||||
this.loadTokensFromStorage();
|
||||
this.setupAuthChangeListener();
|
||||
}
|
||||
|
||||
setupAuthChangeListener() {
|
||||
// Listen for auth changes (e.g., from demo-auth-init.js or other components)
|
||||
// and reload tokens from storage
|
||||
document.addEventListener('auth-change', () => {
|
||||
console.log('[ApiClient] Auth change detected, reloading tokens...');
|
||||
this.loadTokensFromStorage();
|
||||
});
|
||||
window.addEventListener('auth-change', () => {
|
||||
console.log('[ApiClient] Auth change detected, reloading tokens...');
|
||||
this.loadTokensFromStorage();
|
||||
});
|
||||
}
|
||||
|
||||
loadTokensFromStorage() {
|
||||
// Always use localStorage directly for auth tokens
|
||||
// This ensures compatibility with demo-auth-init.js which also uses localStorage
|
||||
const stored = localStorage.getItem('auth_tokens');
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
console.log('[ApiClient] Parsed tokens object:', { hasAccessToken: !!parsed.accessToken, hasRefreshToken: !!parsed.refreshToken });
|
||||
this.accessToken = parsed.accessToken;
|
||||
this.refreshToken = parsed.refreshToken;
|
||||
console.log('[ApiClient] Tokens assigned - accessToken:', this.accessToken ? `${this.accessToken.substring(0, 20)}...` : 'null');
|
||||
} catch (e) {
|
||||
console.error('[ApiClient] Failed to parse stored tokens:', e, 'stored value:', stored);
|
||||
}
|
||||
} else {
|
||||
console.warn('[ApiClient] No auth_tokens found in localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
saveTokensToStorage() {
|
||||
try {
|
||||
// Always use localStorage directly for auth tokens
|
||||
localStorage.setItem('auth_tokens', JSON.stringify({
|
||||
accessToken: this.accessToken,
|
||||
refreshToken: this.refreshToken
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('[ApiClient] Failed to save tokens to storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
clearTokens() {
|
||||
this.accessToken = null;
|
||||
this.refreshToken = null;
|
||||
localStorage.removeItem('auth_tokens');
|
||||
}
|
||||
|
||||
async request(method, endpoint, data = null) {
|
||||
// Always reload tokens from storage before making a request
|
||||
// This handles the case where tokens were set after API client initialization (e.g., auto-login)
|
||||
this.loadTokensFromStorage();
|
||||
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (this.accessToken) {
|
||||
options.headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
console.log(`[ApiClient] ${method} ${endpoint}: Token present (${this.accessToken.substring(0, 20)}...)`);
|
||||
} else {
|
||||
console.warn(`[ApiClient] ${method} ${endpoint}: No token provided`);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
// Handle token refresh
|
||||
if (response.status === 403 && result.code === 'FORBIDDEN' && this.refreshToken) {
|
||||
await this.refreshAccessToken();
|
||||
return this.request(method, endpoint, data);
|
||||
}
|
||||
|
||||
throw new Error(result.message || 'API request failed');
|
||||
}
|
||||
|
||||
// Handle both response formats:
|
||||
// 1. Standard wrapped response: { data: {...} }
|
||||
// 2. Direct array response: [...]
|
||||
// 3. Direct object response: {...}
|
||||
if (Array.isArray(result)) {
|
||||
return result;
|
||||
}
|
||||
return result.data || result;
|
||||
} catch (error) {
|
||||
console.error(`[ApiClient] ${method} ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshAccessToken() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken: this.refreshToken })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.status === 'success') {
|
||||
this.accessToken = result.data.tokens.accessToken;
|
||||
this.refreshToken = result.data.tokens.refreshToken;
|
||||
this.saveTokensToStorage();
|
||||
this.dispatchAuthChangeEvent('token-refresh');
|
||||
return true;
|
||||
}
|
||||
|
||||
this.clearTokens();
|
||||
this.dispatchAuthChangeEvent('token-expired');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('[ApiClient] Token refresh failed:', error);
|
||||
this.clearTokens();
|
||||
this.dispatchAuthChangeEvent('token-refresh-error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch auth-change event to notify all listeners of authentication state changes
|
||||
*/
|
||||
dispatchAuthChangeEvent(reason = 'auth-change') {
|
||||
const event = new CustomEvent('auth-change', {
|
||||
detail: { reason, hasToken: !!this.accessToken },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Auth endpoints
|
||||
async register(email, password, name) {
|
||||
const result = await this.request('POST', '/auth/register', {
|
||||
email, password, name
|
||||
});
|
||||
this.accessToken = result.tokens.accessToken;
|
||||
this.refreshToken = result.tokens.refreshToken;
|
||||
this.saveTokensToStorage();
|
||||
this.dispatchAuthChangeEvent('user-registered');
|
||||
return result.user;
|
||||
}
|
||||
|
||||
async login(email, password) {
|
||||
const result = await this.request('POST', '/auth/login', {
|
||||
email, password
|
||||
});
|
||||
this.accessToken = result.tokens.accessToken;
|
||||
this.refreshToken = result.tokens.refreshToken;
|
||||
this.saveTokensToStorage();
|
||||
this.dispatchAuthChangeEvent('user-logged-in');
|
||||
return result.user;
|
||||
}
|
||||
|
||||
async getMe() {
|
||||
const result = await this.request('GET', '/auth/me');
|
||||
return result.user;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
this.clearTokens();
|
||||
this.dispatchAuthChangeEvent('user-logged-out');
|
||||
}
|
||||
|
||||
// Project endpoints
|
||||
async getProjects() {
|
||||
const result = await this.request('GET', '/projects');
|
||||
// Handle both response formats:
|
||||
// 1. Wrapped response: { data: { projects: [...] } }
|
||||
// 2. Direct array response: [...]
|
||||
if (Array.isArray(result)) {
|
||||
return result;
|
||||
}
|
||||
return result.projects || result.data?.projects || [];
|
||||
}
|
||||
|
||||
async getProject(id) {
|
||||
const result = await this.request('GET', `/projects/${id}`);
|
||||
return result.project;
|
||||
}
|
||||
|
||||
async createProject(data) {
|
||||
const result = await this.request('POST', '/projects', data);
|
||||
return result.project;
|
||||
}
|
||||
|
||||
async updateProject(id, data) {
|
||||
const result = await this.request('PUT', `/projects/${id}`, data);
|
||||
return result.project;
|
||||
}
|
||||
|
||||
async deleteProject(id) {
|
||||
await this.request('DELETE', `/projects/${id}`);
|
||||
}
|
||||
|
||||
// Token endpoints
|
||||
async getTokens(projectId, category = null) {
|
||||
const url = `/tokens/project/${projectId}${category ? `?category=${category}` : ''}`;
|
||||
const result = await this.request('GET', url);
|
||||
return result.tokens;
|
||||
}
|
||||
|
||||
async createToken(data) {
|
||||
const result = await this.request('POST', '/tokens', data);
|
||||
return result.token;
|
||||
}
|
||||
|
||||
async updateToken(id, data) {
|
||||
const result = await this.request('PUT', `/tokens/${id}`, data);
|
||||
return result.token;
|
||||
}
|
||||
|
||||
async deleteToken(id) {
|
||||
await this.request('DELETE', `/tokens/${id}`);
|
||||
}
|
||||
|
||||
// Component endpoints
|
||||
async getComponents(projectId) {
|
||||
const result = await this.request('GET', `/components/project/${projectId}`);
|
||||
return result.components;
|
||||
}
|
||||
|
||||
async createComponent(data) {
|
||||
const result = await this.request('POST', '/components', data);
|
||||
return result.component;
|
||||
}
|
||||
|
||||
async updateComponent(id, data) {
|
||||
const result = await this.request('PUT', `/components/${id}`, data);
|
||||
return result.component;
|
||||
}
|
||||
|
||||
async deleteComponent(id) {
|
||||
await this.request('DELETE', `/components/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ApiClient();
|
||||
193
admin-ui/js/services/audit-service.js
Normal file
193
admin-ui/js/services/audit-service.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Audit Log Service - User action history tracking
|
||||
*/
|
||||
|
||||
import api from '../core/api.js';
|
||||
import logger from '../core/logger.js';
|
||||
|
||||
class AuditService {
|
||||
constructor() {
|
||||
this.cache = {
|
||||
categories: null,
|
||||
actions: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit log with filters
|
||||
*/
|
||||
async getAuditLog(filters = {}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.project_id) params.append('project_id', filters.project_id);
|
||||
if (filters.user_id) params.append('user_id', filters.user_id);
|
||||
if (filters.action) params.append('action', filters.action);
|
||||
if (filters.category) params.append('category', filters.category);
|
||||
if (filters.entity_type) params.append('entity_type', filters.entity_type);
|
||||
if (filters.severity) params.append('severity', filters.severity);
|
||||
if (filters.start_date) params.append('start_date', filters.start_date);
|
||||
if (filters.end_date) params.append('end_date', filters.end_date);
|
||||
if (filters.limit) params.append('limit', filters.limit);
|
||||
if (filters.offset) params.append('offset', filters.offset);
|
||||
|
||||
try {
|
||||
const response = await api.get(`/audit?${params.toString()}`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error('AuditService', 'Failed to fetch audit log', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit statistics
|
||||
*/
|
||||
async getStats() {
|
||||
try {
|
||||
return await api.get('/audit/stats');
|
||||
} catch (error) {
|
||||
logger.error('AuditService', 'Failed to fetch audit stats', error);
|
||||
return {
|
||||
by_category: {},
|
||||
by_user: {},
|
||||
total_count: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*/
|
||||
async getCategories() {
|
||||
if (this.cache.categories) {
|
||||
return this.cache.categories;
|
||||
}
|
||||
|
||||
try {
|
||||
this.cache.categories = await api.get('/audit/categories');
|
||||
return this.cache.categories;
|
||||
} catch (error) {
|
||||
logger.error('AuditService', 'Failed to fetch categories', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all actions
|
||||
*/
|
||||
async getActions() {
|
||||
if (this.cache.actions) {
|
||||
return this.cache.actions;
|
||||
}
|
||||
|
||||
try {
|
||||
this.cache.actions = await api.get('/audit/actions');
|
||||
return this.cache.actions;
|
||||
} catch (error) {
|
||||
logger.error('AuditService', 'Failed to fetch actions', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create audit entry
|
||||
*/
|
||||
async logAction(entry) {
|
||||
try {
|
||||
await api.post('/audit', entry);
|
||||
logger.info('AuditService', 'Action logged', { action: entry.action });
|
||||
} catch (error) {
|
||||
logger.error('AuditService', 'Failed to log action', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit log
|
||||
*/
|
||||
async export(filters = {}, format = 'json') {
|
||||
const params = new URLSearchParams({ format });
|
||||
|
||||
if (filters.project_id) params.append('project_id', filters.project_id);
|
||||
if (filters.category) params.append('category', filters.category);
|
||||
if (filters.start_date) params.append('start_date', filters.start_date);
|
||||
if (filters.end_date) params.append('end_date', filters.end_date);
|
||||
|
||||
try {
|
||||
const url = `${api.baseUrl}/audit/export?${params.toString()}`;
|
||||
window.open(url, '_blank');
|
||||
logger.info('AuditService', 'Export initiated', { format });
|
||||
} catch (error) {
|
||||
logger.error('AuditService', 'Failed to export', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get severity badge class
|
||||
*/
|
||||
getSeverityClass(severity) {
|
||||
const classes = {
|
||||
'info': 'badge--primary',
|
||||
'warning': 'badge--warning',
|
||||
'critical': 'badge--destructive'
|
||||
};
|
||||
return classes[severity] || 'badge--secondary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category icon
|
||||
*/
|
||||
getCategoryIcon(category) {
|
||||
const icons = {
|
||||
'design_system': '🎨',
|
||||
'code': '⚛️',
|
||||
'configuration': '⚙️',
|
||||
'project': '📁',
|
||||
'team': '👥',
|
||||
'storybook': '📚',
|
||||
'other': '📌'
|
||||
};
|
||||
return icons[category] || '📌';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp
|
||||
*/
|
||||
formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
// Less than 1 minute
|
||||
if (diff < 60000) {
|
||||
return 'Just now';
|
||||
}
|
||||
|
||||
// Less than 1 hour
|
||||
if (diff < 3600000) {
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 1 day
|
||||
if (diff < 86400000) {
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 7 days
|
||||
if (diff < 604800000) {
|
||||
const days = Math.floor(diff / 86400000);
|
||||
return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Show full date
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const auditService = new AuditService();
|
||||
|
||||
export default auditService;
|
||||
export { AuditService };
|
||||
378
admin-ui/js/services/claude-service.js
Normal file
378
admin-ui/js/services/claude-service.js
Normal file
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* Claude Service - Chat interface with Claude AI and MCP Integration
|
||||
*
|
||||
* Provides:
|
||||
* - Direct Claude chat
|
||||
* - MCP tool execution
|
||||
* - Project context integration
|
||||
* - Integration management (Figma, Jira, Confluence)
|
||||
*/
|
||||
|
||||
import api from '../core/api.js';
|
||||
import logger from '../core/logger.js';
|
||||
|
||||
class ClaudeService {
|
||||
constructor() {
|
||||
this.conversationHistory = [];
|
||||
this.maxHistory = 50;
|
||||
this.currentProjectId = null;
|
||||
this.userId = 1; // Default user ID (TODO: implement proper auth)
|
||||
this.mcpTools = null;
|
||||
this.mcpStatus = null;
|
||||
|
||||
// Load persisted chat history
|
||||
this.loadHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage key for current user
|
||||
* @private
|
||||
*/
|
||||
_getStorageKey() {
|
||||
return `ds-chat-history-user-${this.userId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load chat history from localStorage
|
||||
* @private
|
||||
*/
|
||||
loadHistory() {
|
||||
const key = this._getStorageKey();
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored) {
|
||||
this.conversationHistory = JSON.parse(stored);
|
||||
logger.info('Claude', `Loaded ${this.conversationHistory.length} messages from history`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Claude', 'Failed to load chat history, resetting', error);
|
||||
localStorage.removeItem(key);
|
||||
this.conversationHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save chat history to localStorage with quota management
|
||||
* @private
|
||||
*/
|
||||
saveHistory() {
|
||||
const key = this._getStorageKey();
|
||||
try {
|
||||
// Keep only last 50 messages to respect localStorage limits
|
||||
const historyToSave = this.conversationHistory.slice(-50);
|
||||
localStorage.setItem(key, JSON.stringify(historyToSave));
|
||||
} catch (error) {
|
||||
logger.warn('Claude', 'Failed to save chat history', error);
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
// Emergency trim: cut in half and retry
|
||||
this.conversationHistory = this.conversationHistory.slice(-25);
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(this.conversationHistory));
|
||||
logger.info('Claude', 'History trimmed to 25 messages due to quota');
|
||||
} catch (retryError) {
|
||||
logger.error('Claude', 'Failed to save history even after trim', retryError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current project context
|
||||
*/
|
||||
setProject(projectId) {
|
||||
this.currentProjectId = projectId;
|
||||
logger.info('Claude', 'Project context set', { projectId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Claude with MCP context
|
||||
*/
|
||||
async chat(message, context = {}) {
|
||||
logger.info('Claude', 'Sending message', { message: message.substring(0, 50) + '...' });
|
||||
|
||||
// Add project context if available
|
||||
if (this.currentProjectId && !context.projectId) {
|
||||
context.projectId = this.currentProjectId;
|
||||
}
|
||||
|
||||
// Add to history
|
||||
this.conversationHistory.push({
|
||||
role: 'user',
|
||||
content: message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
// Call backend Claude endpoint with MCP context
|
||||
const response = await api.post('/claude/chat', {
|
||||
message,
|
||||
context: {
|
||||
...context,
|
||||
mcp_enabled: true,
|
||||
available_tools: this.mcpTools ? Object.keys(this.mcpTools).flatMap(k => this.mcpTools[k].map(t => t.name)) : []
|
||||
},
|
||||
history: this.conversationHistory.slice(-10) // Send last 10 messages
|
||||
});
|
||||
|
||||
// Add response to history
|
||||
this.conversationHistory.push({
|
||||
role: 'assistant',
|
||||
content: response.response,
|
||||
timestamp: new Date().toISOString(),
|
||||
tools_used: response.tools_used || []
|
||||
});
|
||||
|
||||
// Keep history manageable
|
||||
if (this.conversationHistory.length > this.maxHistory) {
|
||||
this.conversationHistory = this.conversationHistory.slice(-this.maxHistory);
|
||||
}
|
||||
|
||||
// Persist updated history
|
||||
this.saveHistory();
|
||||
|
||||
logger.info('Claude', 'Received response', { length: response.response.length });
|
||||
|
||||
return response.response;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Chat failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*/
|
||||
getHistory() {
|
||||
return this.conversationHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear history
|
||||
*/
|
||||
clearHistory() {
|
||||
this.conversationHistory = [];
|
||||
this.saveHistory(); // Persist empty state
|
||||
logger.info('Claude', 'Conversation history cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Export conversation
|
||||
*/
|
||||
exportConversation() {
|
||||
const data = JSON.stringify(this.conversationHistory, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(data);
|
||||
const exportFileDefaultName = `claude-conversation-${Date.now()}.json`;
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
linkElement.click();
|
||||
}
|
||||
|
||||
// === MCP Integration Methods ===
|
||||
|
||||
/**
|
||||
* Get MCP server status
|
||||
*/
|
||||
async getMcpStatus() {
|
||||
try {
|
||||
const status = await api.get('/mcp/status');
|
||||
this.mcpStatus = status;
|
||||
logger.info('Claude', 'MCP status fetched', status);
|
||||
return status;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to fetch MCP status', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available MCP tools
|
||||
*/
|
||||
async getMcpTools() {
|
||||
try {
|
||||
const tools = await api.get('/mcp/tools');
|
||||
this.mcpTools = tools.tools;
|
||||
logger.info('Claude', 'MCP tools fetched', { count: tools.total_count });
|
||||
return tools;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to fetch MCP tools', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an MCP tool directly
|
||||
*/
|
||||
async executeMcpTool(toolName, args = {}) {
|
||||
if (this.currentProjectId && !args.project_id) {
|
||||
args.project_id = this.currentProjectId;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Claude', 'Executing MCP tool', { toolName, args });
|
||||
const result = await api.post(`/mcp/tools/${toolName}/execute?user_id=${this.userId}`, args);
|
||||
logger.info('Claude', 'MCP tool executed', { toolName, success: !result.error });
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'MCP tool execution failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// === Integration Management ===
|
||||
|
||||
/**
|
||||
* Get all integration types and their health status
|
||||
*/
|
||||
async getIntegrations() {
|
||||
try {
|
||||
const integrations = await api.get('/mcp/integrations');
|
||||
logger.info('Claude', 'Integrations fetched', { count: integrations.integrations.length });
|
||||
return integrations.integrations;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to fetch integrations', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project-specific integrations
|
||||
*/
|
||||
async getProjectIntegrations(projectId = null) {
|
||||
const pid = projectId || this.currentProjectId;
|
||||
if (!pid) {
|
||||
throw new Error('No project ID specified');
|
||||
}
|
||||
|
||||
try {
|
||||
const integrations = await api.get(`/projects/${pid}/integrations?user_id=${this.userId}`);
|
||||
return integrations.integrations;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to fetch project integrations', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an integration for a project
|
||||
*/
|
||||
async configureIntegration(integrationType, config, projectId = null) {
|
||||
const pid = projectId || this.currentProjectId;
|
||||
if (!pid) {
|
||||
throw new Error('No project ID specified');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.post(`/projects/${pid}/integrations?user_id=${this.userId}`, {
|
||||
integration_type: integrationType,
|
||||
config,
|
||||
enabled: true
|
||||
});
|
||||
logger.info('Claude', 'Integration configured', { integrationType, projectId: pid });
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to configure integration', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an integration
|
||||
*/
|
||||
async updateIntegration(integrationType, updates, projectId = null) {
|
||||
const pid = projectId || this.currentProjectId;
|
||||
if (!pid) {
|
||||
throw new Error('No project ID specified');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.put(
|
||||
`/projects/${pid}/integrations/${integrationType}?user_id=${this.userId}`,
|
||||
updates
|
||||
);
|
||||
logger.info('Claude', 'Integration updated', { integrationType, projectId: pid });
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to update integration', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an integration
|
||||
*/
|
||||
async deleteIntegration(integrationType, projectId = null) {
|
||||
const pid = projectId || this.currentProjectId;
|
||||
if (!pid) {
|
||||
throw new Error('No project ID specified');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.delete(
|
||||
`/projects/${pid}/integrations/${integrationType}?user_id=${this.userId}`
|
||||
);
|
||||
logger.info('Claude', 'Integration deleted', { integrationType, projectId: pid });
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Claude', 'Failed to delete integration', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle integration enabled/disabled
|
||||
*/
|
||||
async toggleIntegration(integrationType, enabled, projectId = null) {
|
||||
return this.updateIntegration(integrationType, { enabled }, projectId);
|
||||
}
|
||||
|
||||
// === Project Context Tools ===
|
||||
|
||||
/**
|
||||
* Get project summary via MCP
|
||||
*/
|
||||
async getProjectSummary(projectId = null) {
|
||||
return this.executeMcpTool('dss_get_project_summary', {
|
||||
project_id: projectId || this.currentProjectId,
|
||||
include_components: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project health via MCP
|
||||
*/
|
||||
async getProjectHealth(projectId = null) {
|
||||
return this.executeMcpTool('dss_get_project_health', {
|
||||
project_id: projectId || this.currentProjectId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List project components via MCP
|
||||
*/
|
||||
async listComponents(projectId = null, filter = null) {
|
||||
const args = { project_id: projectId || this.currentProjectId };
|
||||
if (filter) {
|
||||
args.filter_name = filter;
|
||||
}
|
||||
return this.executeMcpTool('dss_list_components', args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get design tokens via MCP
|
||||
*/
|
||||
async getDesignTokens(projectId = null, category = null) {
|
||||
const args = { project_id: projectId || this.currentProjectId };
|
||||
if (category) {
|
||||
args.token_category = category;
|
||||
}
|
||||
return this.executeMcpTool('dss_get_design_tokens', args);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const claudeService = new ClaudeService();
|
||||
|
||||
export default claudeService;
|
||||
export { ClaudeService };
|
||||
256
admin-ui/js/services/dashboard-service.js
Normal file
256
admin-ui/js/services/dashboard-service.js
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Dashboard Service
|
||||
*
|
||||
* Handles team dashboard data fetching and management
|
||||
* - UX Dashboard: Figma files and component tokens
|
||||
* - UI Dashboard: Token drift detection and code metrics
|
||||
* - QA Dashboard: ESRE definitions and test results
|
||||
*/
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
export class DashboardService {
|
||||
/**
|
||||
* Get aggregated dashboard summary for all teams
|
||||
*/
|
||||
static async getDashboardSummary(projectId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/dashboard/summary`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch dashboard summary: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard summary:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UX DASHBOARD - Figma Files
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* List all Figma files for a project
|
||||
*/
|
||||
static async listFigmaFiles(projectId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/figma-files`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Figma files: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching Figma files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new Figma file to a project
|
||||
*/
|
||||
static async addFigmaFile(projectId, figmaData) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/figma-files`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(figmaData)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to add Figma file');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error adding Figma file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Figma file sync status
|
||||
*/
|
||||
static async updateFigmaFileSync(projectId, fileId, syncStatus) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/figma-files/${fileId}/sync`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sync_status: syncStatus,
|
||||
last_synced: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update sync status');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error updating Figma sync status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Figma file
|
||||
*/
|
||||
static async deleteFigmaFile(projectId, fileId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/figma-files/${fileId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete Figma file');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error deleting Figma file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UI DASHBOARD - Token Drift
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* List token drift issues for a project
|
||||
*/
|
||||
static async listTokenDrift(projectId, severity = null) {
|
||||
try {
|
||||
const url = severity
|
||||
? `${API_BASE}/projects/${projectId}/token-drift?severity=${severity}`
|
||||
: `${API_BASE}/projects/${projectId}/token-drift`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch token drift: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching token drift:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a new token drift issue
|
||||
*/
|
||||
static async recordTokenDrift(projectId, driftData) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/token-drift`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(driftData)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to record token drift');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error recording token drift:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update token drift status (pending/fixed/ignored)
|
||||
*/
|
||||
static async updateTokenDriftStatus(projectId, driftId, status) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/token-drift/${driftId}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update token drift status');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error updating token drift status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// QA DASHBOARD - ESRE Definitions
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* List all ESRE definitions for a project
|
||||
*/
|
||||
static async listESREDefinitions(projectId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/esre`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ESRE definitions: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching ESRE definitions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ESRE definition
|
||||
*/
|
||||
static async createESREDefinition(projectId, esreData) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/esre`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(esreData)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Failed to create ESRE definition');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error creating ESRE definition:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an ESRE definition
|
||||
*/
|
||||
static async updateESREDefinition(projectId, esreId, updateData) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/esre/${esreId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update ESRE definition');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error updating ESRE definition:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an ESRE definition
|
||||
*/
|
||||
static async deleteESREDefinition(projectId, esreId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${projectId}/esre/${esreId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete ESRE definition');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error deleting ESRE definition:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardService;
|
||||
90
admin-ui/js/services/discovery-service.js
Normal file
90
admin-ui/js/services/discovery-service.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Design System Server (DSS) - Discovery Service
|
||||
*
|
||||
* Project analysis interface for the admin dashboard.
|
||||
* No mocks - requires backend connection.
|
||||
*/
|
||||
|
||||
class DiscoveryService {
|
||||
constructor() {
|
||||
this.apiBase = '/api/discovery';
|
||||
this.connected = null;
|
||||
this.cache = new Map();
|
||||
this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
|
||||
}
|
||||
|
||||
async checkConnection() {
|
||||
try {
|
||||
const response = await fetch('/health', { method: 'GET' });
|
||||
this.connected = response.ok;
|
||||
} catch {
|
||||
this.connected = false;
|
||||
}
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
_requireConnection() {
|
||||
if (this.connected === false) {
|
||||
throw new Error('API unavailable. Start DSS server first.');
|
||||
}
|
||||
}
|
||||
|
||||
async discover(projectPath = '.', fullScan = false) {
|
||||
const cacheKey = `${projectPath}:${fullScan}`;
|
||||
|
||||
// Check cache
|
||||
if (this.cache.has(cacheKey)) {
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (Date.now() - cached.timestamp < this.cacheExpiry) {
|
||||
return cached.data;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/scan`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: projectPath, full_scan: fullScan })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Discovery scan failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.cache.set(cacheKey, { data: result, timestamp: Date.now() });
|
||||
return result;
|
||||
}
|
||||
|
||||
async getHealth() {
|
||||
const response = await fetch('/health');
|
||||
if (!response.ok) {
|
||||
throw new Error('Health check failed');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getRecentActivity() {
|
||||
const response = await fetch(`${this.apiBase}/activity`);
|
||||
if (!response.ok) {
|
||||
return { items: [] };
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getProjectStats() {
|
||||
const response = await fetch(`${this.apiBase}/stats`);
|
||||
if (!response.ok) {
|
||||
return { projects: {}, tokens: {}, components: {}, syncs: {} };
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
invalidateCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const discoveryService = new DiscoveryService();
|
||||
export { DiscoveryService };
|
||||
export default discoveryService;
|
||||
229
admin-ui/js/services/figma-service.js
Normal file
229
admin-ui/js/services/figma-service.js
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Design System Server (DSS) - Figma Service
|
||||
*
|
||||
* Client-side interface to Figma API tools.
|
||||
* No mocks - requires backend connection.
|
||||
*/
|
||||
import notificationService from './notification-service.js';
|
||||
|
||||
class FigmaService {
|
||||
constructor() {
|
||||
this.apiBase = '/api/figma';
|
||||
this.connected = null; // null = unknown, true/false = checked
|
||||
this.listeners = new Set();
|
||||
}
|
||||
|
||||
// === Connection ===
|
||||
|
||||
async checkConnection() {
|
||||
try {
|
||||
const response = await fetch('/health', { method: 'GET' });
|
||||
this.connected = response.ok;
|
||||
} catch {
|
||||
this.connected = false;
|
||||
}
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
_requireConnection() {
|
||||
if (this.connected === false) {
|
||||
throw new Error('API unavailable. Start DSS server first.');
|
||||
}
|
||||
}
|
||||
|
||||
// === Event System ===
|
||||
|
||||
on(event, callback) {
|
||||
this.listeners.add({ event, callback });
|
||||
return () => this.listeners.delete({ event, callback });
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
this.listeners.forEach(l => {
|
||||
if (l.event === event) l.callback(data);
|
||||
});
|
||||
}
|
||||
|
||||
// === API Methods ===
|
||||
|
||||
async extractVariables(fileKey, format = 'css') {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'extractVariables' });
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/extract-variables`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey, format })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Failed to extract variables');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.emit('complete', { operation: 'extractVariables', result });
|
||||
|
||||
notificationService.create({
|
||||
title: 'Variables Extracted',
|
||||
message: `Successfully extracted ${result.variable_count || 'variables'} from Figma.`,
|
||||
type: 'success',
|
||||
source: 'Figma',
|
||||
actions: [
|
||||
{ label: 'View Tokens', event: 'navigate', payload: { page: 'tokens' } }
|
||||
]
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
notificationService.create({
|
||||
title: 'Extraction Failed',
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
source: 'Figma'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async extractComponents(fileKey) {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'extractComponents' });
|
||||
|
||||
const response = await fetch(`${this.apiBase}/extract-components`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Failed to extract components');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.emit('complete', { operation: 'extractComponents', result });
|
||||
return result;
|
||||
}
|
||||
|
||||
async extractStyles(fileKey) {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'extractStyles' });
|
||||
|
||||
const response = await fetch(`${this.apiBase}/extract-styles`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Failed to extract styles');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async syncTokens(fileKey, targetPath, format = 'css') {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'syncTokens' });
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/sync-tokens`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey, target_path: targetPath, format })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Sync failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.emit('complete', { operation: 'syncTokens', result });
|
||||
|
||||
notificationService.create({
|
||||
title: 'Tokens Synced',
|
||||
message: `Tokens successfully written to ${result.path || targetPath}.`,
|
||||
type: 'success',
|
||||
source: 'Figma',
|
||||
actions: [
|
||||
{ label: 'View Tokens', event: 'navigate', payload: { page: 'tokens' } }
|
||||
]
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
notificationService.create({
|
||||
title: 'Token Sync Failed',
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
source: 'Figma',
|
||||
actions: [
|
||||
{ label: 'Retry', event: 'figma:sync', payload: { fileKey } }
|
||||
]
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async visualDiff(fileKey, baselineVersion = 'latest') {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'visualDiff' });
|
||||
|
||||
const response = await fetch(`${this.apiBase}/visual-diff`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey, baseline_version: baselineVersion })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Visual diff failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async validateComponents(fileKey) {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'validateComponents' });
|
||||
|
||||
const response = await fetch(`${this.apiBase}/validate-components`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Validation failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async generateCode(fileKey, componentName, framework = 'webcomponent') {
|
||||
this._requireConnection();
|
||||
this.emit('loading', { operation: 'generateCode' });
|
||||
|
||||
const response = await fetch(`${this.apiBase}/generate-code`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_key: fileKey, component_name: componentName, framework })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Code generation failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
const figmaService = new FigmaService();
|
||||
export { FigmaService };
|
||||
export default figmaService;
|
||||
298
admin-ui/js/services/notification-service.js
Normal file
298
admin-ui/js/services/notification-service.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* @fileoverview Manages application-wide notifications.
|
||||
* Handles persistence via IndexedDB, real-time updates via SSE, and state management.
|
||||
*/
|
||||
|
||||
import dssDB from '../db/indexed-db.js';
|
||||
|
||||
const NOTIFICATION_STORE = 'notifications';
|
||||
|
||||
class NotificationService extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this.notifications = [];
|
||||
this.unreadCount = 0;
|
||||
this._eventSource = null;
|
||||
this._initialized = false;
|
||||
this._broadcastChannel = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
|
||||
await this._loadFromStorage();
|
||||
this._connectToEvents();
|
||||
this._setupCrossTabSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup cross-tab synchronization via BroadcastChannel API
|
||||
* When notifications are modified in another tab, reload them
|
||||
*/
|
||||
_setupCrossTabSync() {
|
||||
try {
|
||||
this._broadcastChannel = new BroadcastChannel('dss-notifications');
|
||||
|
||||
this._broadcastChannel.onmessage = (event) => {
|
||||
if (event.data?.type === 'notifications-updated') {
|
||||
console.log('[NotificationService] Notifications updated in another tab, reloading...');
|
||||
this._loadFromStorage();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('[NotificationService] BroadcastChannel not available:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify other tabs about notification changes
|
||||
* @private
|
||||
*/
|
||||
_notifyOtherTabs() {
|
||||
if (this._broadcastChannel) {
|
||||
try {
|
||||
this._broadcastChannel.postMessage({ type: 'notifications-updated' });
|
||||
} catch (error) {
|
||||
console.warn('[NotificationService] Failed to broadcast update:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _loadFromStorage() {
|
||||
try {
|
||||
this.notifications = await dssDB.getAll(NOTIFICATION_STORE) || [];
|
||||
this._sortNotifications();
|
||||
this._updateUnreadCount();
|
||||
this.dispatchEvent(new CustomEvent('notifications-updated', {
|
||||
detail: { notifications: this.notifications }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to load notifications from storage:', error);
|
||||
this.notifications = [];
|
||||
}
|
||||
}
|
||||
|
||||
_connectToEvents() {
|
||||
// Only connect if SSE endpoint exists and we have an auth token
|
||||
try {
|
||||
// Get access token from localStorage
|
||||
const authTokens = localStorage.getItem('auth_tokens');
|
||||
if (!authTokens) {
|
||||
console.log('[NotificationService] No auth token available, skipping SSE connection');
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken } = JSON.parse(authTokens);
|
||||
if (!accessToken) {
|
||||
console.log('[NotificationService] No access token found, skipping SSE connection');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct SSE URL with token parameter (EventSource can't send custom headers)
|
||||
const sseUrl = `/api/notifications/events?token=${encodeURIComponent(accessToken)}`;
|
||||
console.log('[NotificationService] Connecting to SSE with authentication...');
|
||||
|
||||
this._eventSource = new EventSource(sseUrl);
|
||||
|
||||
this._eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const notificationData = JSON.parse(event.data);
|
||||
console.log('[NotificationService] SSE notification received:', notificationData.type);
|
||||
// From SSE, persist to local storage
|
||||
this.create(notificationData, true);
|
||||
} catch (error) {
|
||||
console.error('[NotificationService] Error parsing SSE notification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this._eventSource.onerror = (err) => {
|
||||
console.warn('[NotificationService] SSE connection error, using local-only mode');
|
||||
this._eventSource.close();
|
||||
this._eventSource = null;
|
||||
};
|
||||
|
||||
this._eventSource.onopen = () => {
|
||||
console.log('[NotificationService] ✅ SSE connection established successfully');
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('[NotificationService] SSE setup error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
_sortNotifications() {
|
||||
this.notifications.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
_updateUnreadCount() {
|
||||
const newCount = this.notifications.filter(n => !n.read).length;
|
||||
if (newCount !== this.unreadCount) {
|
||||
this.unreadCount = newCount;
|
||||
this.dispatchEvent(new CustomEvent('unread-count-changed', {
|
||||
detail: { count: this.unreadCount }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
_dispatchUpdate() {
|
||||
this._sortNotifications();
|
||||
this._updateUnreadCount();
|
||||
this.dispatchEvent(new CustomEvent('notifications-updated', {
|
||||
detail: { notifications: this.notifications }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new notification.
|
||||
* @param {object} notificationData - The notification data.
|
||||
* @param {string} notificationData.title - Notification title
|
||||
* @param {string} [notificationData.message] - Notification message
|
||||
* @param {string} [notificationData.type='info'] - 'info', 'success', 'warning', 'error'
|
||||
* @param {string} [notificationData.source] - Source service name
|
||||
* @param {Array} [notificationData.actions] - Action buttons
|
||||
* @param {boolean} [persist=true] - Whether to save to IndexedDB.
|
||||
* @returns {Promise<object>} The created notification.
|
||||
*/
|
||||
async create(notificationData, persist = true) {
|
||||
const newNotification = {
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
read: false,
|
||||
type: 'info',
|
||||
...notificationData,
|
||||
};
|
||||
|
||||
this.notifications.unshift(newNotification);
|
||||
|
||||
if (persist) {
|
||||
try {
|
||||
await dssDB.put(NOTIFICATION_STORE, newNotification);
|
||||
} catch (error) {
|
||||
console.error('Failed to persist notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this._dispatchUpdate();
|
||||
this._notifyOtherTabs();
|
||||
|
||||
// Also show as toast for immediate visibility
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast({
|
||||
message: `<strong>${newNotification.title}</strong>${newNotification.message ? `<br>${newNotification.message}` : ''}`,
|
||||
type: newNotification.type,
|
||||
duration: 5000,
|
||||
dismissible: true
|
||||
});
|
||||
}
|
||||
|
||||
return newNotification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all notifications.
|
||||
* @returns {object[]}
|
||||
*/
|
||||
getAll() {
|
||||
return [...this.notifications];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread count
|
||||
* @returns {number}
|
||||
*/
|
||||
getUnreadCount() {
|
||||
return this.unreadCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a specific notification as read.
|
||||
* @param {string} id - The ID of the notification to update.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async markAsRead(id) {
|
||||
const notification = this.notifications.find(n => n.id === id);
|
||||
if (notification && !notification.read) {
|
||||
notification.read = true;
|
||||
try {
|
||||
await dssDB.put(NOTIFICATION_STORE, notification);
|
||||
} catch (error) {
|
||||
console.error('Failed to update notification:', error);
|
||||
}
|
||||
this._dispatchUpdate();
|
||||
this._notifyOtherTabs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all unread notifications as read.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async markAllAsRead() {
|
||||
const updates = [];
|
||||
this.notifications.forEach(n => {
|
||||
if (!n.read) {
|
||||
n.read = true;
|
||||
updates.push(dssDB.put(NOTIFICATION_STORE, n));
|
||||
}
|
||||
});
|
||||
|
||||
if (updates.length > 0) {
|
||||
try {
|
||||
await Promise.all(updates);
|
||||
} catch (error) {
|
||||
console.error('Failed to mark all as read:', error);
|
||||
}
|
||||
this._dispatchUpdate();
|
||||
this._notifyOtherTabs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a notification by its ID.
|
||||
* @param {string} id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async delete(id) {
|
||||
this.notifications = this.notifications.filter(n => n.id !== id);
|
||||
try {
|
||||
await dssDB.delete(NOTIFICATION_STORE, id);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete notification:', error);
|
||||
}
|
||||
this._dispatchUpdate();
|
||||
this._notifyOtherTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all notifications.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clearAll() {
|
||||
this.notifications = [];
|
||||
try {
|
||||
await dssDB.clear(NOTIFICATION_STORE);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear notifications:', error);
|
||||
}
|
||||
this._dispatchUpdate();
|
||||
this._notifyOtherTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup on app shutdown
|
||||
*/
|
||||
destroy() {
|
||||
if (this._eventSource) {
|
||||
this._eventSource.close();
|
||||
this._eventSource = null;
|
||||
}
|
||||
if (this._broadcastChannel) {
|
||||
this._broadcastChannel.close();
|
||||
this._broadcastChannel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
const notificationService = new NotificationService();
|
||||
export default notificationService;
|
||||
335
admin-ui/js/services/plugin-service.js
Normal file
335
admin-ui/js/services/plugin-service.js
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Plugin Service - Manages frontend plugins for DSS Admin UI
|
||||
*
|
||||
* Supports loading, enabling/disabling, and configuring plugins.
|
||||
* Plugins can extend UI, add commands, and integrate with services.
|
||||
*/
|
||||
|
||||
import logger from '../core/logger.js';
|
||||
|
||||
class PluginService {
|
||||
constructor() {
|
||||
this.plugins = new Map();
|
||||
this.hooks = new Map();
|
||||
this.loadedPlugins = [];
|
||||
this.storageKey = 'dss_plugins_config';
|
||||
|
||||
// Load saved plugin states
|
||||
this.config = this._loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin configuration from localStorage
|
||||
*/
|
||||
_loadConfig() {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.storageKey);
|
||||
return saved ? JSON.parse(saved) : { enabled: {}, settings: {} };
|
||||
} catch (e) {
|
||||
logger.warn('PluginService', 'Failed to load plugin config', e);
|
||||
return { enabled: {}, settings: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save plugin configuration to localStorage
|
||||
*/
|
||||
_saveConfig() {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(this.config));
|
||||
} catch (e) {
|
||||
logger.warn('PluginService', 'Failed to save plugin config', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a plugin
|
||||
* @param {Object} plugin - Plugin definition
|
||||
*/
|
||||
register(plugin) {
|
||||
if (!plugin.id || !plugin.name) {
|
||||
throw new Error('Plugin must have id and name');
|
||||
}
|
||||
|
||||
if (this.plugins.has(plugin.id)) {
|
||||
logger.warn('PluginService', `Plugin ${plugin.id} already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginDef = {
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
version: plugin.version || '1.0.0',
|
||||
description: plugin.description || '',
|
||||
author: plugin.author || 'Unknown',
|
||||
icon: plugin.icon || '🔌',
|
||||
category: plugin.category || 'general',
|
||||
|
||||
// Lifecycle hooks
|
||||
onInit: plugin.onInit || (() => {}),
|
||||
onEnable: plugin.onEnable || (() => {}),
|
||||
onDisable: plugin.onDisable || (() => {}),
|
||||
onDestroy: plugin.onDestroy || (() => {}),
|
||||
|
||||
// Extension points
|
||||
commands: plugin.commands || [],
|
||||
panels: plugin.panels || [],
|
||||
settings: plugin.settings || [],
|
||||
|
||||
// State
|
||||
enabled: this.config.enabled[plugin.id] ?? plugin.enabledByDefault ?? false,
|
||||
initialized: false
|
||||
};
|
||||
|
||||
this.plugins.set(plugin.id, pluginDef);
|
||||
logger.info('PluginService', `Registered plugin: ${plugin.name} v${pluginDef.version}`);
|
||||
|
||||
// Auto-init if enabled
|
||||
if (pluginDef.enabled) {
|
||||
this._initPlugin(pluginDef);
|
||||
}
|
||||
|
||||
return pluginDef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a plugin
|
||||
*/
|
||||
async _initPlugin(plugin) {
|
||||
if (plugin.initialized) return;
|
||||
|
||||
try {
|
||||
await plugin.onInit(this._createPluginContext(plugin));
|
||||
plugin.initialized = true;
|
||||
this.loadedPlugins.push(plugin.id);
|
||||
logger.info('PluginService', `Initialized plugin: ${plugin.name}`);
|
||||
} catch (e) {
|
||||
logger.error('PluginService', `Failed to init plugin ${plugin.name}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create context object passed to plugin hooks
|
||||
*/
|
||||
_createPluginContext(plugin) {
|
||||
return {
|
||||
pluginId: plugin.id,
|
||||
settings: this.config.settings[plugin.id] || {},
|
||||
|
||||
// API for plugins
|
||||
registerHook: (hookName, callback) => this.registerHook(plugin.id, hookName, callback),
|
||||
unregisterHook: (hookName) => this.unregisterHook(plugin.id, hookName),
|
||||
getSettings: () => this.config.settings[plugin.id] || {},
|
||||
setSetting: (key, value) => this.setPluginSetting(plugin.id, key, value),
|
||||
|
||||
// Access to app APIs
|
||||
emit: (event, data) => this._emitEvent(plugin.id, event, data),
|
||||
log: (msg, data) => logger.info(`Plugin:${plugin.name}`, msg, data)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a plugin
|
||||
*/
|
||||
async enable(pluginId) {
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin ${pluginId} not found`);
|
||||
}
|
||||
|
||||
if (plugin.enabled) return;
|
||||
|
||||
plugin.enabled = true;
|
||||
this.config.enabled[pluginId] = true;
|
||||
this._saveConfig();
|
||||
|
||||
if (!plugin.initialized) {
|
||||
await this._initPlugin(plugin);
|
||||
}
|
||||
|
||||
try {
|
||||
await plugin.onEnable(this._createPluginContext(plugin));
|
||||
logger.info('PluginService', `Enabled plugin: ${plugin.name}`);
|
||||
this._emitEvent('system', 'plugin:enabled', { pluginId });
|
||||
} catch (e) {
|
||||
logger.error('PluginService', `Failed to enable plugin ${plugin.name}`, e);
|
||||
plugin.enabled = false;
|
||||
this.config.enabled[pluginId] = false;
|
||||
this._saveConfig();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a plugin
|
||||
*/
|
||||
async disable(pluginId) {
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin ${pluginId} not found`);
|
||||
}
|
||||
|
||||
if (!plugin.enabled) return;
|
||||
|
||||
try {
|
||||
await plugin.onDisable(this._createPluginContext(plugin));
|
||||
plugin.enabled = false;
|
||||
this.config.enabled[pluginId] = false;
|
||||
this._saveConfig();
|
||||
logger.info('PluginService', `Disabled plugin: ${plugin.name}`);
|
||||
this._emitEvent('system', 'plugin:disabled', { pluginId });
|
||||
} catch (e) {
|
||||
logger.error('PluginService', `Failed to disable plugin ${plugin.name}`, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle plugin enabled state
|
||||
*/
|
||||
async toggle(pluginId) {
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin ${pluginId} not found`);
|
||||
}
|
||||
|
||||
if (plugin.enabled) {
|
||||
await this.disable(pluginId);
|
||||
} else {
|
||||
await this.enable(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a hook callback
|
||||
*/
|
||||
registerHook(pluginId, hookName, callback) {
|
||||
const key = `${pluginId}:${hookName}`;
|
||||
if (!this.hooks.has(hookName)) {
|
||||
this.hooks.set(hookName, new Map());
|
||||
}
|
||||
this.hooks.get(hookName).set(pluginId, callback);
|
||||
logger.debug('PluginService', `Registered hook ${hookName} for ${pluginId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a hook callback
|
||||
*/
|
||||
unregisterHook(pluginId, hookName) {
|
||||
const hookMap = this.hooks.get(hookName);
|
||||
if (hookMap) {
|
||||
hookMap.delete(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all callbacks for a hook
|
||||
*/
|
||||
async executeHook(hookName, data = {}) {
|
||||
const hookMap = this.hooks.get(hookName);
|
||||
if (!hookMap) return [];
|
||||
|
||||
const results = [];
|
||||
for (const [pluginId, callback] of hookMap) {
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (plugin?.enabled) {
|
||||
try {
|
||||
const result = await callback(data);
|
||||
results.push({ pluginId, result });
|
||||
} catch (e) {
|
||||
logger.error('PluginService', `Hook ${hookName} failed for ${pluginId}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a plugin setting
|
||||
*/
|
||||
setPluginSetting(pluginId, key, value) {
|
||||
if (!this.config.settings[pluginId]) {
|
||||
this.config.settings[pluginId] = {};
|
||||
}
|
||||
this.config.settings[pluginId][key] = value;
|
||||
this._saveConfig();
|
||||
this._emitEvent(pluginId, 'settings:changed', { key, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin settings
|
||||
*/
|
||||
getPluginSettings(pluginId) {
|
||||
return this.config.settings[pluginId] || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event to listeners
|
||||
*/
|
||||
_emitEvent(source, event, data) {
|
||||
window.dispatchEvent(new CustomEvent('dss:plugin:event', {
|
||||
detail: { source, event, data }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered plugins
|
||||
*/
|
||||
getAll() {
|
||||
return Array.from(this.plugins.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled plugins
|
||||
*/
|
||||
getEnabled() {
|
||||
return this.getAll().filter(p => p.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin by ID
|
||||
*/
|
||||
get(pluginId) {
|
||||
return this.plugins.get(pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all commands from enabled plugins
|
||||
*/
|
||||
getAllCommands() {
|
||||
const commands = [];
|
||||
for (const plugin of this.getEnabled()) {
|
||||
for (const cmd of plugin.commands) {
|
||||
commands.push({
|
||||
...cmd,
|
||||
pluginId: plugin.id,
|
||||
pluginName: plugin.name
|
||||
});
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all panels from enabled plugins
|
||||
*/
|
||||
getAllPanels() {
|
||||
const panels = [];
|
||||
for (const plugin of this.getEnabled()) {
|
||||
for (const panel of plugin.panels) {
|
||||
panels.push({
|
||||
...panel,
|
||||
pluginId: plugin.id,
|
||||
pluginName: plugin.name
|
||||
});
|
||||
}
|
||||
}
|
||||
return panels;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const pluginService = new PluginService();
|
||||
|
||||
export default pluginService;
|
||||
export { PluginService };
|
||||
241
admin-ui/js/services/team-service.js
Normal file
241
admin-ui/js/services/team-service.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Design System Server (DSS) - Team Service
|
||||
*
|
||||
* Multi-team management with RBAC handling team operations,
|
||||
* role management, and permissions.
|
||||
*/
|
||||
|
||||
// Role definitions with permissions
|
||||
const ROLES = {
|
||||
SUPER_ADMIN: {
|
||||
name: 'Super Admin',
|
||||
description: 'Full system access',
|
||||
permissions: ['*'],
|
||||
level: 100
|
||||
},
|
||||
TEAM_LEAD: {
|
||||
name: 'Team Lead',
|
||||
description: 'Full team access, can manage members',
|
||||
permissions: [
|
||||
'read', 'write', 'delete',
|
||||
'sync_tokens', 'sync_components',
|
||||
'manage_team_members', 'invite_users',
|
||||
'configure_figma', 'run_visual_diff',
|
||||
'generate_code', 'export_tokens'
|
||||
],
|
||||
level: 80
|
||||
},
|
||||
DEVELOPER: {
|
||||
name: 'Developer',
|
||||
description: 'Read/write access, can sync and generate',
|
||||
permissions: [
|
||||
'read', 'write',
|
||||
'sync_tokens', 'sync_components',
|
||||
'generate_code', 'export_tokens',
|
||||
'run_visual_diff'
|
||||
],
|
||||
level: 50
|
||||
},
|
||||
VIEWER: {
|
||||
name: 'Viewer',
|
||||
description: 'Read-only access',
|
||||
permissions: ['read', 'export_tokens'],
|
||||
level: 10
|
||||
}
|
||||
};
|
||||
|
||||
class TeamService {
|
||||
constructor() {
|
||||
this.apiBase = '/api/teams';
|
||||
this.useMock = false;
|
||||
this.currentUser = null;
|
||||
this.currentTeam = null;
|
||||
}
|
||||
|
||||
// === Role Management ===
|
||||
|
||||
getRoles() {
|
||||
return ROLES;
|
||||
}
|
||||
|
||||
getRole(roleKey) {
|
||||
return ROLES[roleKey];
|
||||
}
|
||||
|
||||
hasPermission(userRole, permission) {
|
||||
const role = ROLES[userRole];
|
||||
if (!role) return false;
|
||||
if (role.permissions.includes('*')) return true;
|
||||
return role.permissions.includes(permission);
|
||||
}
|
||||
|
||||
canManage(managerRole, targetRole) {
|
||||
const manager = ROLES[managerRole];
|
||||
const target = ROLES[targetRole];
|
||||
if (!manager || !target) return false;
|
||||
return manager.level > target.level;
|
||||
}
|
||||
|
||||
// === Team Operations ===
|
||||
|
||||
async getTeams() {
|
||||
if (this.useMock) {
|
||||
await this._delay(300);
|
||||
return this._getMockTeams();
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getTeam(teamId) {
|
||||
if (this.useMock) {
|
||||
await this._delay(200);
|
||||
const teams = this._getMockTeams();
|
||||
return teams.find(t => t.id === teamId);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createTeam(data) {
|
||||
if (this.useMock) {
|
||||
await this._delay(400);
|
||||
return {
|
||||
id: `team-${Date.now()}`,
|
||||
...data,
|
||||
createdAt: new Date().toISOString(),
|
||||
members: []
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async updateTeam(teamId, data) {
|
||||
if (this.useMock) {
|
||||
await this._delay(300);
|
||||
return { id: teamId, ...data, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// === Member Management ===
|
||||
|
||||
async getTeamMembers(teamId) {
|
||||
if (this.useMock) {
|
||||
await this._delay(200);
|
||||
return this._getMockMembers(teamId);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/members`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async addTeamMember(teamId, email, role) {
|
||||
if (this.useMock) {
|
||||
await this._delay(400);
|
||||
return {
|
||||
id: `member-${Date.now()}`,
|
||||
email,
|
||||
role,
|
||||
teamId,
|
||||
status: 'pending',
|
||||
invitedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/members`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, role })
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async updateMemberRole(teamId, memberId, role) {
|
||||
if (this.useMock) {
|
||||
await this._delay(300);
|
||||
return { id: memberId, role, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/members/${memberId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role })
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async removeMember(teamId, memberId) {
|
||||
if (this.useMock) {
|
||||
await this._delay(300);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/members/${memberId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// === Team Settings ===
|
||||
|
||||
async getTeamSettings(teamId) {
|
||||
if (this.useMock) {
|
||||
await this._delay(200);
|
||||
return this._getMockTeamSettings(teamId);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/settings`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async updateTeamSettings(teamId, settings) {
|
||||
if (this.useMock) {
|
||||
await this._delay(300);
|
||||
return { ...settings, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/settings`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// === Dashboard Data ===
|
||||
|
||||
async getTeamDashboard(teamId) {
|
||||
if (this.useMock) {
|
||||
await this._delay(400);
|
||||
return this._getMockDashboard(teamId);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/${teamId}/dashboard`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getAdminDashboard() {
|
||||
if (this.useMock) {
|
||||
await this._delay(500);
|
||||
return this._getMockAdminDashboard();
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiBase}/admin/dashboard`);
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
311
admin-ui/js/services/tool-bridge.js
Normal file
311
admin-ui/js/services/tool-bridge.js
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* tool-bridge.js
|
||||
* Service to execute MCP tools from the UI
|
||||
* MVP1: Auto-injects context from ContextStore, removes hardcoded defaults
|
||||
*/
|
||||
|
||||
import contextStore from '../stores/context-store.js';
|
||||
|
||||
class ToolBridge {
|
||||
constructor() {
|
||||
this.apiBase = '/api';
|
||||
this.toolCache = new Map();
|
||||
this.context = null; // Deprecated - use contextStore instead
|
||||
}
|
||||
|
||||
/**
|
||||
* MVP1: Get execution context from ContextStore
|
||||
* NO DEFAULTS - throws if project not selected
|
||||
* @returns {object} Context with projectId, teamId, userId
|
||||
*/
|
||||
getContext() {
|
||||
const mcpContext = contextStore.getMCPContext();
|
||||
|
||||
// Validate required context
|
||||
if (!mcpContext.project_id) {
|
||||
throw new Error('No project selected. Please select a project from the header.');
|
||||
}
|
||||
|
||||
return {
|
||||
projectId: mcpContext.project_id,
|
||||
teamId: mcpContext.team_id,
|
||||
userId: mcpContext.user_id,
|
||||
capabilities: mcpContext.capabilities
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set execution context (DEPRECATED - use contextStore.setProject/setTeam instead)
|
||||
* @param {string} projectId - Project ID
|
||||
* @param {string} userId - User ID
|
||||
*/
|
||||
setContext(projectId, userId) {
|
||||
console.warn('[ToolBridge] setContext is deprecated. Use contextStore.setProject() instead');
|
||||
contextStore.setProject(projectId);
|
||||
this.context = { projectId, userId };
|
||||
}
|
||||
|
||||
/**
|
||||
* MVP1: Execute an MCP tool with auto-context injection
|
||||
* Implements interceptor pattern with standardized error handling
|
||||
* @param {string} toolName - MCP tool name (e.g., 'dss_extract_tokens')
|
||||
* @param {object} params - Tool parameters
|
||||
* @returns {Promise<any>} Tool execution result
|
||||
*/
|
||||
async executeTool(toolName, params = {}) {
|
||||
console.log(`[ToolBridge] Executing tool: ${toolName}`, params);
|
||||
|
||||
try {
|
||||
// Auto-inject context from ContextStore
|
||||
const context = this.getContext();
|
||||
|
||||
// Wrap parameters with auto-injected context
|
||||
const payload = {
|
||||
project_id: context.projectId,
|
||||
team_id: context.teamId,
|
||||
user_id: context.userId,
|
||||
arguments: params,
|
||||
_client_timestamp: Date.now() // For debugging
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.apiBase}/mcp/tools/${toolName}/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
// Standardized error handling
|
||||
if (!response.ok) {
|
||||
// Handle specific HTTP status codes
|
||||
if (response.status === 502) {
|
||||
throw new Error('MCP Server Offline - Please check backend connection');
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new Error(`Tool not found: ${toolName}`);
|
||||
}
|
||||
|
||||
const errorText = await response.text();
|
||||
let errorMessage;
|
||||
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText);
|
||||
errorMessage = errorJson.message || errorJson.detail || `Tool execution failed: ${response.statusText}`;
|
||||
} catch (e) {
|
||||
errorMessage = `Tool execution failed: ${response.status} - ${errorText}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log(`[ToolBridge] Tool ${toolName} completed:`, result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`[ToolBridge] Tool ${toolName} failed:`, error);
|
||||
|
||||
// Re-throw with enhanced error message if it's a context error
|
||||
if (error.message.includes('No project selected')) {
|
||||
throw new Error(`${error.message}\n\nTool: ${toolName}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available MCP tools
|
||||
* @returns {Promise<Array>} Array of tool definitions
|
||||
*/
|
||||
async getAvailableTools() {
|
||||
if (this.toolCache.has('all')) {
|
||||
return this.toolCache.get('all');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/mcp/tools`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch tools: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const tools = await response.json();
|
||||
this.toolCache.set('all', tools);
|
||||
return tools;
|
||||
} catch (error) {
|
||||
console.error('[ToolBridge] Failed to fetch available tools:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool definition by name
|
||||
* @param {string} toolName - MCP tool name
|
||||
* @returns {Promise<object|null>} Tool definition
|
||||
*/
|
||||
async getToolDefinition(toolName) {
|
||||
const tools = await this.getAvailableTools();
|
||||
return tools.find(tool => tool.name === toolName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_extract_tokens tool
|
||||
*/
|
||||
async extractTokens(path, sources = ['css', 'scss', 'tailwind', 'json']) {
|
||||
return this.executeTool('dss_extract_tokens', { path, sources });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_generate_theme tool
|
||||
*/
|
||||
async generateTheme(format, tokens = null, themeName = 'default') {
|
||||
return this.executeTool('dss_generate_theme', {
|
||||
format,
|
||||
tokens,
|
||||
theme_name: themeName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_sync_figma tool
|
||||
*/
|
||||
async syncFigma(fileKey) {
|
||||
return this.executeTool('dss_sync_figma', { file_key: fileKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_resolve_token tool
|
||||
*/
|
||||
async resolveToken(manifestPath, tokenPath, forceRefresh = false) {
|
||||
return this.executeTool('dss_resolve_token', {
|
||||
manifest_path: manifestPath,
|
||||
token_path: tokenPath,
|
||||
force_refresh: forceRefresh
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_get_resolved_context tool
|
||||
*/
|
||||
async getResolvedContext(manifestPath, debug = false, forceRefresh = false) {
|
||||
return this.executeTool('dss_get_resolved_context', {
|
||||
manifest_path: manifestPath,
|
||||
debug,
|
||||
force_refresh: forceRefresh
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute browser_get_logs tool
|
||||
*/
|
||||
async getBrowserLogs(level = 'all', limit = 100) {
|
||||
return this.executeTool('browser_get_logs', { level, limit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute browser_get_errors tool
|
||||
*/
|
||||
async getBrowserErrors(limit = 50) {
|
||||
return this.executeTool('browser_get_errors', { limit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute browser_accessibility_audit tool
|
||||
*/
|
||||
async runAccessibilityAudit(selector = null) {
|
||||
return this.executeTool('browser_accessibility_audit', { selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute browser_performance tool
|
||||
*/
|
||||
async getPerformanceMetrics() {
|
||||
return this.executeTool('browser_performance');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute devtools_screenshot tool
|
||||
*/
|
||||
async takeScreenshot(fullPage = false, selector = null) {
|
||||
return this.executeTool('devtools_screenshot', { full_page: fullPage, selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute devtools_query_dom tool
|
||||
*/
|
||||
async queryDOM(selector) {
|
||||
return this.executeTool('devtools_query_dom', { selector });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute devtools_network_requests tool
|
||||
*/
|
||||
async getNetworkRequests(filterUrl = null, limit = 50) {
|
||||
return this.executeTool('devtools_network_requests', {
|
||||
filter_url: filterUrl,
|
||||
limit
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_audit_components tool
|
||||
*/
|
||||
async auditComponents(path) {
|
||||
return this.executeTool('dss_audit_components', { path });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_find_quick_wins tool
|
||||
*/
|
||||
async findQuickWins(path) {
|
||||
return this.executeTool('dss_find_quick_wins', { path });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_get_status tool
|
||||
*/
|
||||
async getDSSStatus(format = 'json') {
|
||||
return this.executeTool('dss_get_status', { format });
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute dss_list_themes tool
|
||||
*/
|
||||
async listThemes() {
|
||||
return this.executeTool('dss_list_themes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute browser_dom_snapshot tool
|
||||
*/
|
||||
async getDOMSnapshot() {
|
||||
return this.executeTool('browser_dom_snapshot');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute devtools_console_logs tool
|
||||
*/
|
||||
async getConsoleLogs(level = 'all', limit = 100, clear = false) {
|
||||
return this.executeTool('devtools_console_logs', { level, limit, clear });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token information from Context Compiler
|
||||
*/
|
||||
async getTokens(manifestPath) {
|
||||
return this.getResolvedContext(manifestPath, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear browser console logs
|
||||
*/
|
||||
async clearConsoleLogs() {
|
||||
return this.executeTool('devtools_console_logs', { clear: true, limit: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const toolBridge = new ToolBridge();
|
||||
|
||||
export default toolBridge;
|
||||
449
admin-ui/js/services/tools-service.js
Normal file
449
admin-ui/js/services/tools-service.js
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* Tools Service - Manages MCP tools catalog and execution
|
||||
*/
|
||||
|
||||
import api from '../core/api.js';
|
||||
import logger from '../core/logger.js';
|
||||
|
||||
class ToolsService {
|
||||
constructor() {
|
||||
this.tools = [];
|
||||
this.toolsMetadata = this._getToolsMetadata();
|
||||
this.executionHistory = [];
|
||||
this.toolStatus = {};
|
||||
this.teamToolMappings = this._getTeamToolMappings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define which tools are relevant for each team
|
||||
*/
|
||||
_getTeamToolMappings() {
|
||||
return {
|
||||
'ui': [
|
||||
'extract_tokens', 'extract_components', 'generate_component_code',
|
||||
'sync_tokens_to_file', 'ingest_css_tokens', 'ingest_scss_tokens',
|
||||
'ingest_tailwind_tokens', 'merge_tokens', 'export_tokens',
|
||||
'scan_storybook', 'generate_story', 'generate_stories_batch',
|
||||
'generate_storybook_theme'
|
||||
],
|
||||
'ux': [
|
||||
'analyze_style_values', 'check_naming_consistency', 'find_style_patterns',
|
||||
'validate_tokens', 'extract_tokens', 'export_tokens',
|
||||
'discover_project', 'get_sync_history'
|
||||
],
|
||||
'qa': [
|
||||
'get_quick_wins', 'get_quick_wins_report', 'validate_tokens',
|
||||
'find_unused_styles', 'find_inline_styles', 'analyze_react_components',
|
||||
'build_source_graph', 'get_story_coverage'
|
||||
],
|
||||
'all': [] // Empty means all tools
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for all 32 MCP tools
|
||||
*/
|
||||
_getToolsMetadata() {
|
||||
return {
|
||||
// Projects
|
||||
list_projects: {
|
||||
name: 'list_projects',
|
||||
category: 'Projects',
|
||||
description: 'List all design system projects',
|
||||
parameters: [],
|
||||
icon: '📁'
|
||||
},
|
||||
create_project: {
|
||||
name: 'create_project',
|
||||
category: 'Projects',
|
||||
description: 'Create a new design system project',
|
||||
parameters: ['name', 'description', 'figma_file_key'],
|
||||
icon: '➕'
|
||||
},
|
||||
get_project: {
|
||||
name: 'get_project',
|
||||
category: 'Projects',
|
||||
description: 'Get details of a specific project',
|
||||
parameters: ['project_id'],
|
||||
icon: '🔍'
|
||||
},
|
||||
|
||||
// Figma
|
||||
extract_tokens: {
|
||||
name: 'extract_tokens',
|
||||
category: 'Figma',
|
||||
description: 'Extract design tokens from Figma file',
|
||||
parameters: ['file_key', 'format'],
|
||||
icon: '🎨',
|
||||
requiresConfig: ['figma_token']
|
||||
},
|
||||
extract_components: {
|
||||
name: 'extract_components',
|
||||
category: 'Figma',
|
||||
description: 'Extract components from Figma file',
|
||||
parameters: ['file_key'],
|
||||
icon: '🧩',
|
||||
requiresConfig: ['figma_token']
|
||||
},
|
||||
generate_component_code: {
|
||||
name: 'generate_component_code',
|
||||
category: 'Figma',
|
||||
description: 'Generate React code from Figma component',
|
||||
parameters: ['file_key', 'node_id', 'framework'],
|
||||
icon: '⚛️',
|
||||
requiresConfig: ['figma_token']
|
||||
},
|
||||
sync_tokens_to_file: {
|
||||
name: 'sync_tokens_to_file',
|
||||
category: 'Figma',
|
||||
description: 'Sync tokens to output file',
|
||||
parameters: ['file_key', 'output_path', 'format'],
|
||||
icon: '🔄',
|
||||
requiresConfig: ['figma_token']
|
||||
},
|
||||
|
||||
// Ingestion
|
||||
ingest_css_tokens: {
|
||||
name: 'ingest_css_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Ingest tokens from CSS variables',
|
||||
parameters: ['source'],
|
||||
icon: '📥'
|
||||
},
|
||||
ingest_scss_tokens: {
|
||||
name: 'ingest_scss_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Ingest tokens from SCSS variables',
|
||||
parameters: ['source'],
|
||||
icon: '📥'
|
||||
},
|
||||
ingest_tailwind_tokens: {
|
||||
name: 'ingest_tailwind_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Ingest tokens from Tailwind config',
|
||||
parameters: ['source'],
|
||||
icon: '📥'
|
||||
},
|
||||
ingest_json_tokens: {
|
||||
name: 'ingest_json_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Ingest tokens from JSON format',
|
||||
parameters: ['source'],
|
||||
icon: '📥'
|
||||
},
|
||||
merge_tokens: {
|
||||
name: 'merge_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Merge tokens from multiple sources',
|
||||
parameters: ['sources', 'strategy'],
|
||||
icon: '🔗'
|
||||
},
|
||||
export_tokens: {
|
||||
name: 'export_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Export tokens to specified format',
|
||||
parameters: ['format', 'output_path'],
|
||||
icon: '📤'
|
||||
},
|
||||
validate_tokens: {
|
||||
name: 'validate_tokens',
|
||||
category: 'Ingestion',
|
||||
description: 'Validate token structure and values',
|
||||
parameters: ['source'],
|
||||
icon: '✓'
|
||||
},
|
||||
|
||||
// Analysis
|
||||
discover_project: {
|
||||
name: 'discover_project',
|
||||
category: 'Analysis',
|
||||
description: 'Discover project structure and frameworks',
|
||||
parameters: ['path'],
|
||||
icon: '🔎',
|
||||
quickWin: true
|
||||
},
|
||||
analyze_react_components: {
|
||||
name: 'analyze_react_components',
|
||||
category: 'Analysis',
|
||||
description: 'Analyze React components in project',
|
||||
parameters: ['path'],
|
||||
icon: '⚛️',
|
||||
quickWin: true
|
||||
},
|
||||
find_inline_styles: {
|
||||
name: 'find_inline_styles',
|
||||
category: 'Analysis',
|
||||
description: 'Find components with inline styles',
|
||||
parameters: ['path'],
|
||||
icon: '🎯',
|
||||
quickWin: true
|
||||
},
|
||||
find_style_patterns: {
|
||||
name: 'find_style_patterns',
|
||||
category: 'Analysis',
|
||||
description: 'Identify common style patterns',
|
||||
parameters: ['path'],
|
||||
icon: '📊',
|
||||
quickWin: true
|
||||
},
|
||||
analyze_style_values: {
|
||||
name: 'analyze_style_values',
|
||||
category: 'Analysis',
|
||||
description: 'Analyze style property values',
|
||||
parameters: ['path', 'property'],
|
||||
icon: '📈',
|
||||
quickWin: true
|
||||
},
|
||||
find_unused_styles: {
|
||||
name: 'find_unused_styles',
|
||||
category: 'Analysis',
|
||||
description: 'Find unused style definitions',
|
||||
parameters: ['path'],
|
||||
icon: '🗑️',
|
||||
quickWin: true
|
||||
},
|
||||
build_source_graph: {
|
||||
name: 'build_source_graph',
|
||||
category: 'Analysis',
|
||||
description: 'Build component dependency graph',
|
||||
parameters: ['path', 'depth'],
|
||||
icon: '🕸️'
|
||||
},
|
||||
get_quick_wins: {
|
||||
name: 'get_quick_wins',
|
||||
category: 'Analysis',
|
||||
description: 'Get actionable improvement opportunities',
|
||||
parameters: ['path'],
|
||||
icon: '⚡',
|
||||
quickWin: true,
|
||||
featured: true
|
||||
},
|
||||
get_quick_wins_report: {
|
||||
name: 'get_quick_wins_report',
|
||||
category: 'Analysis',
|
||||
description: 'Generate detailed quick wins report',
|
||||
parameters: ['path'],
|
||||
icon: '📋',
|
||||
quickWin: true
|
||||
},
|
||||
check_naming_consistency: {
|
||||
name: 'check_naming_consistency',
|
||||
category: 'Analysis',
|
||||
description: 'Check component naming consistency',
|
||||
parameters: ['path'],
|
||||
icon: '🏷️',
|
||||
quickWin: true
|
||||
},
|
||||
|
||||
// Storybook
|
||||
scan_storybook: {
|
||||
name: 'scan_storybook',
|
||||
category: 'Storybook',
|
||||
description: 'Scan existing Storybook configuration',
|
||||
parameters: ['path'],
|
||||
icon: '📚'
|
||||
},
|
||||
generate_story: {
|
||||
name: 'generate_story',
|
||||
category: 'Storybook',
|
||||
description: 'Generate Storybook story for component',
|
||||
parameters: ['component_path', 'template'],
|
||||
icon: '📖'
|
||||
},
|
||||
generate_stories_batch: {
|
||||
name: 'generate_stories_batch',
|
||||
category: 'Storybook',
|
||||
description: 'Generate stories for multiple components',
|
||||
parameters: ['components', 'template'],
|
||||
icon: '📚'
|
||||
},
|
||||
generate_storybook_theme: {
|
||||
name: 'generate_storybook_theme',
|
||||
category: 'Storybook',
|
||||
description: 'Generate Storybook theme from tokens',
|
||||
parameters: ['tokens', 'output_path'],
|
||||
icon: '🎨'
|
||||
},
|
||||
get_story_coverage: {
|
||||
name: 'get_story_coverage',
|
||||
category: 'Storybook',
|
||||
description: 'Check which components have stories',
|
||||
parameters: ['path'],
|
||||
icon: '📊'
|
||||
},
|
||||
|
||||
// Activity
|
||||
get_status: {
|
||||
name: 'get_status',
|
||||
category: 'Activity',
|
||||
description: 'Get server status and statistics',
|
||||
parameters: [],
|
||||
icon: '💚'
|
||||
},
|
||||
get_sync_history: {
|
||||
name: 'get_sync_history',
|
||||
category: 'Activity',
|
||||
description: 'Get token sync history',
|
||||
parameters: ['limit'],
|
||||
icon: '📜'
|
||||
},
|
||||
get_activity: {
|
||||
name: 'get_activity',
|
||||
category: 'Activity',
|
||||
description: 'Get recent activity log',
|
||||
parameters: ['limit'],
|
||||
icon: '📝'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tools with metadata
|
||||
*/
|
||||
getAllTools() {
|
||||
return Object.values(this.toolsMetadata).map(tool => ({
|
||||
...tool,
|
||||
status: this.toolStatus[tool.name] || 'available',
|
||||
lastUsed: this._getLastUsed(tool.name)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools by category
|
||||
*/
|
||||
getToolsByCategory(category) {
|
||||
return this.getAllTools().filter(tool => tool.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quick win tools
|
||||
*/
|
||||
getQuickWinTools() {
|
||||
return this.getAllTools().filter(tool => tool.quickWin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get featured tools
|
||||
*/
|
||||
getFeaturedTools() {
|
||||
return this.getAllTools().filter(tool => tool.featured);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tools filtered by team context
|
||||
*/
|
||||
getToolsByTeam(teamContext = 'all') {
|
||||
if (teamContext === 'all' || !this.teamToolMappings[teamContext]) {
|
||||
return this.getAllTools();
|
||||
}
|
||||
|
||||
const teamTools = this.teamToolMappings[teamContext];
|
||||
return this.getAllTools().filter(tool => teamTools.includes(tool.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool
|
||||
*/
|
||||
async executeTool(toolName, params = {}) {
|
||||
logger.info('Tools', `Executing tool: ${toolName}`, params);
|
||||
|
||||
this.toolStatus[toolName] = 'executing';
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Map tool name to API endpoint
|
||||
const result = await api.executeMCPTool(toolName, params);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.toolStatus[toolName] = 'success';
|
||||
|
||||
// Log execution
|
||||
this._logExecution(toolName, params, result, duration, 'success');
|
||||
|
||||
logger.info('Tools', `Tool ${toolName} succeeded in ${duration}ms`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.toolStatus[toolName] = 'error';
|
||||
|
||||
// Log error
|
||||
this._logExecution(toolName, params, null, duration, 'error', error.message);
|
||||
|
||||
logger.error('Tools', `Tool ${toolName} failed: ${error.message}`, error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log tool execution
|
||||
*/
|
||||
_logExecution(toolName, params, result, duration, status, error = null) {
|
||||
const execution = {
|
||||
toolName,
|
||||
params,
|
||||
result,
|
||||
duration,
|
||||
status,
|
||||
error,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.executionHistory.push(execution);
|
||||
|
||||
// Keep only last 100 executions
|
||||
if (this.executionHistory.length > 100) {
|
||||
this.executionHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get execution history for a tool
|
||||
*/
|
||||
getToolHistory(toolName) {
|
||||
return this.executionHistory
|
||||
.filter(exec => exec.toolName === toolName)
|
||||
.slice(-10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last used timestamp
|
||||
*/
|
||||
_getLastUsed(toolName) {
|
||||
const executions = this.executionHistory.filter(exec => exec.toolName === toolName);
|
||||
if (executions.length === 0) return null;
|
||||
return executions[executions.length - 1].timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool success rate
|
||||
*/
|
||||
getSuccessRate(toolName) {
|
||||
const executions = this.executionHistory.filter(exec => exec.toolName === toolName);
|
||||
if (executions.length === 0) return null;
|
||||
|
||||
const successful = executions.filter(exec => exec.status === 'success').length;
|
||||
return successful / executions.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*/
|
||||
getCategories() {
|
||||
const categories = new Set();
|
||||
Object.values(this.toolsMetadata).forEach(tool => {
|
||||
categories.add(tool.category);
|
||||
});
|
||||
return Array.from(categories);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const toolsService = new ToolsService();
|
||||
|
||||
export default toolsService;
|
||||
export { ToolsService };
|
||||
76
admin-ui/js/services/translation-service.js
Normal file
76
admin-ui/js/services/translation-service.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* translation-service.js
|
||||
* API wrapper for Translation Dictionary endpoints
|
||||
*/
|
||||
|
||||
import apiClient from './api-client.js';
|
||||
|
||||
class TranslationService {
|
||||
constructor() {
|
||||
this.baseUrl = '/translations';
|
||||
}
|
||||
|
||||
// ========== Dictionary Operations ==========
|
||||
|
||||
async listDictionaries(filters = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.projectId) params.set('projectId', filters.projectId);
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
if (filters.limit) params.set('limit', filters.limit);
|
||||
if (filters.offset) params.set('offset', filters.offset);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
|
||||
return apiClient.request('GET', url);
|
||||
}
|
||||
|
||||
async getDictionary(id) {
|
||||
return apiClient.request('GET', `${this.baseUrl}/${id}`);
|
||||
}
|
||||
|
||||
async createDictionary(data) {
|
||||
return apiClient.request('POST', this.baseUrl, data);
|
||||
}
|
||||
|
||||
async updateDictionary(id, data) {
|
||||
return apiClient.request('PUT', `${this.baseUrl}/${id}`, data);
|
||||
}
|
||||
|
||||
async deleteDictionary(id) {
|
||||
return apiClient.request('DELETE', `${this.baseUrl}/${id}`);
|
||||
}
|
||||
|
||||
// ========== Mapping Operations ==========
|
||||
|
||||
async createMapping(dictionaryId, data) {
|
||||
return apiClient.request('POST', `${this.baseUrl}/${dictionaryId}/mappings`, data);
|
||||
}
|
||||
|
||||
async updateMapping(dictionaryId, mappingId, data) {
|
||||
return apiClient.request('PUT', `${this.baseUrl}/${dictionaryId}/mappings/${mappingId}`, data);
|
||||
}
|
||||
|
||||
async deleteMapping(dictionaryId, mappingId) {
|
||||
return apiClient.request('DELETE', `${this.baseUrl}/${dictionaryId}/mappings/${mappingId}`);
|
||||
}
|
||||
|
||||
async bulkImportMappings(dictionaryId, mappings) {
|
||||
return apiClient.request('POST', `${this.baseUrl}/${dictionaryId}/mappings/bulk`, { mappings });
|
||||
}
|
||||
|
||||
// ========== Validation & Analysis ==========
|
||||
|
||||
async validateDictionary(id) {
|
||||
return apiClient.request('GET', `${this.baseUrl}/${id}/validate`);
|
||||
}
|
||||
|
||||
async getCoverage(id) {
|
||||
return apiClient.request('GET', `${this.baseUrl}/${id}/coverage`);
|
||||
}
|
||||
|
||||
async exportDictionary(id) {
|
||||
return apiClient.request('GET', `${this.baseUrl}/${id}/export`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TranslationService();
|
||||
Reference in New Issue
Block a user