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:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View 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();

View 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 };

View 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 };

View 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;

View 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;

View 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;

View 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;

View 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 };

View 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();
}
}

View 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;

View 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 };

View 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();