/**
* Design System Server (DSS) - App Shell
*
* Main application controller.
* Handles routing, state, and integration with services.
*/
import store from '../stores/app-store.js';
import figmaService from '../services/figma-service.js';
import discoveryService from '../services/discovery-service.js';
import teamService from '../services/team-service.js';
import toolsService from '../services/tools-service.js';
import claudeService from '../services/claude-service.js';
import DashboardService from '../services/dashboard-service.js';
import pluginService from '../services/plugin-service.js';
import { initializePlugins } from '../plugins/index.js';
import logger from './logger.js';
import themeManager from './theme.js';
import { UITeamDashboard, UXTeamDashboard, QATeamDashboard } from './team-dashboards.js';
// Enterprise Architecture Modules
import router from './router.js';
import { subscribe as subscribeToNotifications, notifySuccess, notifyError, notifyInfo, ErrorCode } from './messaging.js';
import { createProjectWorkflow } from './workflows.js';
import { handleError } from './error-handler.js';
import { loadConfig, getStorybookUrl, getDssHost } from './config-loader.js';
import { getEnabledComponents, getComponentSetting, setComponentSetting } from './component-config.js';
import { sanitizeHtml, setSafeHtml, sanitizeText, escapeHtml } from './sanitizer.js';
import LandingPage from './landing-page.js';
import themeLoader from './theme-loader.js';
class App {
constructor() {
this.store = store;
this.services = {
figma: figmaService,
discovery: discoveryService,
team: teamService
};
// Track listener setup to prevent duplicates
this.listeners = {
teamContextChanged: null,
hasTeamContextListener: false
};
}
// === Initialization ===
async init() {
logger.info('App', 'Initializing application...');
// FIRST: Load server configuration (blocking operation)
// This must complete before any component uses the config
try {
await loadConfig();
this.store.set({ isConfigLoading: false });
logger.info('App', 'Server configuration loaded');
} catch (error) {
this.store.set({
isConfigLoading: false,
configError: error.message
});
logger.error('App', 'Failed to load server configuration', { error: error.message });
notifyError('Failed to load server configuration. Some features may not work correctly.', ErrorCode.SYSTEM_STARTUP_FAILED);
}
// Initialize DSS Theme Loader (validates CSS layers)
try {
await themeLoader.init();
this.themeLoader = themeLoader;
logger.info('App', 'DSS Theme Loader initialized');
} catch (error) {
logger.warn('App', 'Theme loader initialization failed, using fallback styles', { error: error.message });
}
// Initialize plugins
initializePlugins();
logger.info('App', 'Plugins initialized');
// Initialize enterprise messaging system
this.initializeMessaging();
// Initialize router with route definitions
this.initializeRouter();
// Subscribe to state changes
this.store.subscribe('currentPage', () => this.render());
this.store.subscribe('notifications', (notifs) => this.renderNotifications(notifs));
this.store.subscribe('currentProject', (projectId) => {
if (projectId) {
this.loadDashboardData(projectId);
}
});
// Listen for team context changes from chat sidebar (only add listener once)
if (!this.listeners.hasTeamContextListener) {
this.listeners.teamContextChanged = (e) => {
logger.info('App', 'Team context changed', { team: e.detail.team });
this.store.set({ teamContext: e.detail.team });
this.render();
};
window.addEventListener('team-context-changed', this.listeners.teamContextChanged);
this.listeners.hasTeamContextListener = true;
}
// Load initial data
await this.loadInitialData();
// Update Storybook link with configured URL
this.updateStorybookLink();
// Initialize landing page with dashboard cards
const appElement = document.getElementById('app');
if (appElement) {
this.landingPage = new LandingPage(appElement);
logger.info('App', 'Landing page initialized');
}
// Render the app
this.render();
// Set up polling
setInterval(() => this.refreshHealth(), 30000);
logger.info('App', 'Application initialized successfully');
}
/**
* Initialize enterprise messaging system
*/
initializeMessaging() {
// Subscribe to notification events
subscribeToNotifications((notification) => {
// Use the existing store notification system for UI display
const duration = notification.duration || 5000;
this.store.notify(notification.message, notification.type, duration);
// Log to console for debugging
logger.info('Notification', notification.message, {
code: notification.code,
correlationId: notification.correlationId,
});
});
logger.info('App', 'Messaging system initialized');
}
/**
* Initialize router with route definitions
*/
initializeRouter() {
// Register all application routes
router.registerAll([
{
path: 'dashboard',
name: 'dashboard',
handler: ({ router }) => {
this.store.set({ currentPage: 'dashboard' });
this.render();
},
},
{
path: 'projects',
name: 'projects',
handler: ({ router }) => {
this.store.set({ currentPage: 'projects' });
this.render();
},
},
{
path: 'tokens',
name: 'tokens',
handler: ({ router }) => {
this.store.set({ currentPage: 'tokens' });
this.render();
},
},
{
path: 'components',
name: 'components',
handler: ({ router }) => {
this.store.set({ currentPage: 'components' });
this.render();
},
},
{
path: 'figma',
name: 'figma',
handler: ({ router }) => {
this.store.set({ currentPage: 'figma' });
this.render();
},
},
{
path: 'docs',
name: 'docs',
handler: ({ router }) => {
this.store.set({ currentPage: 'docs' });
this.render();
},
},
{
path: 'teams',
name: 'teams',
handler: ({ router }) => {
this.store.set({ currentPage: 'teams' });
this.render();
},
},
{
path: 'audit',
name: 'audit',
handler: ({ router }) => {
this.store.set({ currentPage: 'audit' });
this.render();
},
},
{
path: 'settings',
name: 'settings',
handler: ({ router }) => {
this.store.set({ currentPage: 'settings' });
this.render();
},
},
{
path: 'services',
name: 'services',
handler: ({ router }) => {
this.store.set({ currentPage: 'services' });
this.render();
},
},
{
path: 'quick-wins',
name: 'quick-wins',
handler: ({ router }) => {
this.store.set({ currentPage: 'quick-wins' });
this.render();
},
},
{
path: 'chat',
name: 'chat',
handler: ({ router }) => {
this.store.set({ currentPage: 'chat' });
this.render();
},
},
{
path: 'plugins',
name: 'plugins',
handler: ({ router }) => {
this.store.set({ currentPage: 'plugins' });
this.render();
},
},
{
path: 'workdesk',
name: 'workdesk',
handler: async ({ router }) => {
logger.info('App', 'Mounting workdesk UI');
// Get app container
const appRoot = document.getElementById('app');
if (!appRoot) {
logger.error('App', 'Cannot mount workdesk: #app element not found');
return;
}
// Full page takeover - clear legacy layout
appRoot.innerHTML = '';
// Dynamically import and mount ds-shell
try {
await import('../components/layout/ds-shell.js');
const shell = document.createElement('ds-shell');
shell.setAttribute('mode', 'production');
appRoot.appendChild(shell);
logger.info('App', 'Workdesk mounted successfully');
} catch (error) {
logger.error('App', 'Failed to mount workdesk', { error: error.message });
appRoot.innerHTML = `
Failed to Load Workdesk
Error: ${error.message}
Return to Dashboard
`;
}
},
},
]);
// Set default route
router.setDefaultRoute('dashboard');
// Initialize router
router.init();
logger.info('App', 'Router initialized with 13 routes');
}
async loadInitialData() {
this.store.setLoading('init', true);
try {
// Check API connection first
const health = await this.services.discovery.getHealth();
this.store.setHealth(health);
this.store.set({ apiConnected: true });
// Load runtime configuration and services
await this.loadConfig();
// Load discovery data
const discovery = await this.services.discovery.discover('.', false);
this.store.setDiscovery(discovery);
// Load stats
const stats = await this.services.discovery.getProjectStats();
this.store.setStats(stats);
// Load recent activity
const activity = await this.services.discovery.getRecentActivity();
this.store.setActivity(activity.items || []);
// Load projects
await this.loadProjects();
// Set user from session or default
this.store.setUser(
{ id: '1', name: 'User', email: 'user@local' },
'team-1',
'TEAM_LEAD'
);
} catch (error) {
logger.error('App', 'Failed to load initial data', error);
this.store.set({ apiConnected: false });
this.store.setError('init', 'Cannot connect to DSS server. Run: python tools/api/server.py');
this.store.notify('Server offline. Start DSS API first.', 'error', 0);
}
this.store.setLoading('init', false);
}
async refreshHealth() {
try {
const health = await this.services.discovery.getHealth();
this.store.setHealth(health);
} catch (error) {
logger.error('App', 'Health check failed', error);
}
}
// === Navigation ===
navigate(page) {
window.location.hash = page;
}
// === Figma Actions ===
async extractTokens() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('extractTokens', true);
try {
const result = await this.services.figma.extractVariables(fileKey, 'css');
this.store.setTokens(result.tokens);
notifySuccess('Tokens extracted successfully!', ErrorCode.SUCCESS_OPERATION, {
fileKey,
tokenCount: result.tokens?.length || 0,
});
return result;
} catch (error) {
// Use enterprise error handler for user-friendly messages
handleError(error, {
operation: 'extract tokens',
service: 'figma',
fileKey,
});
throw error;
} finally {
this.store.setLoading('extractTokens', false);
}
}
async extractComponents() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('extractComponents', true);
try {
const result = await this.services.figma.extractComponents(fileKey);
this.store.setComponents(result.components);
this.store.notify('Components extracted successfully', 'success');
return result;
} catch (error) {
this.store.notify(`Failed to extract components: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('extractComponents', false);
}
}
async syncTokens() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('syncTokens', true);
try {
const result = await this.services.figma.syncTokens(fileKey, './admin-ui/css/tokens.css');
this.store.setLastSync(new Date().toISOString());
const count = result?.tokens_synced ?? 0;
this.store.notify(count > 0 ? `Synced ${count} tokens` : 'Sync complete', 'success');
return result;
} catch (error) {
this.store.notify(`Sync failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('syncTokens', false);
}
}
async runVisualDiff() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('visualDiff', true);
try {
const result = await this.services.figma.visualDiff(fileKey);
if (result.changes_detected) {
this.store.notify(`Visual diff: ${result.summary.changed} changes detected`, 'warning');
} else {
this.store.notify('No visual changes detected', 'success');
}
return result;
} catch (error) {
this.store.notify(`Visual diff failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('visualDiff', false);
}
}
async validateComponents() {
const fileKey = this.store.get('figmaFileKey') || 'demo';
this.store.setLoading('validate', true);
try {
const result = await this.services.figma.validateComponents(fileKey);
const { errors, warnings } = result.summary;
if (errors > 0) {
this.store.notify(`Validation: ${errors} errors, ${warnings} warnings`, 'error');
} else if (warnings > 0) {
this.store.notify(`Validation passed with ${warnings} warnings`, 'warning');
} else {
this.store.notify('All components valid', 'success');
}
return result;
} catch (error) {
this.store.notify(`Validation failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('validate', false);
}
}
async generateCode(componentName, framework = null) {
const fileKey = this.store.get('figmaFileKey') || 'demo';
const targetFramework = framework || this.store.get('componentFramework') || 'react';
this.store.setLoading('generateCode', true);
try {
const result = await this.services.figma.generateCode(fileKey, componentName, targetFramework);
this.store.set({
generatedCode: {
component: componentName,
framework: targetFramework,
code: result.code || result.generated_code || '// Code generation in progress...'
}
});
this.store.notify(`Generated ${componentName} for ${targetFramework}`, 'success');
this.render();
return result;
} catch (error) {
this.store.notify(`Code generation failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('generateCode', false);
}
}
// === Discovery Actions ===
getProjectRoot() {
const project = this.store.get('selectedProject');
return project?.root_path || project?.path || '.';
}
async runDiscovery(fullScan = false) {
this.store.setLoading('discovery', true);
const projectRoot = this.getProjectRoot();
try {
const result = await this.services.discovery.discover(projectRoot, fullScan);
this.store.setDiscovery(result);
const pathInfo = projectRoot !== '.' ? ` (${projectRoot})` : '';
this.store.notify(`Discovery complete${pathInfo}`, 'success');
return result;
} catch (error) {
this.store.notify(`Discovery failed: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('discovery', false);
}
}
// === Rendering ===
render() {
const contentArea = document.getElementById('page-content');
if (!contentArea) return;
const page = this.store.get('currentPage');
setSafeHtml(contentArea, this.getPageContent(page));
// Update active nav item
document.querySelectorAll('.nav-item[data-page]').forEach(item => {
item.classList.toggle('active', item.dataset.page === page);
});
// Attach event handlers
this.attachEventHandlers();
}
getPageContent(page) {
switch (page) {
case 'dashboard': return this.renderDashboard();
case 'services': return this.renderServices();
case 'quick-wins': return this.renderQuickWins();
case 'chat': return this.renderChat();
case 'projects': return this.renderProjects();
case 'tokens': return this.renderTokens();
case 'components': return this.renderComponents();
case 'figma': return this.renderFigma();
case 'docs': return this.renderDocs();
case 'teams': return this.renderTeams();
case 'plugins': return this.renderPlugins();
case 'audit': return this.renderAuditLog();
case 'settings': return this.renderSettings();
default: return this.renderDashboard();
}
}
renderDashboard() {
const teamContext = this.store.get('teamContext') || localStorage.getItem('dss_team_context') || 'all';
const health = this.store.get('health') || {};
const stats = this.store.get('stats') || {};
const discovery = this.store.get('discovery') || {};
const activity = this.store.get('activity') || [];
const dashboardData = this.store.get('dashboardData') || {
ux: { figma_files_count: 0, figma_files: [] },
ui: { token_drift: { total: 0, by_severity: {} }, code_metrics: {} },
qa: { esre_count: 0, test_summary: {} }
};
const healthScore = discovery.health?.score || 0;
const healthGrade = discovery.health?.grade || '-';
const teamNames = {
'all': 'All Teams',
'ui': 'UI Team',
'ux': 'UX Team',
'qa': 'QA Team'
};
// Get current project
const currentProjectId = this.store.get('currentProject');
const projects = this.store.get('projects') || [];
const currentProject = projects.find(p => p.id === currentProjectId);
const projectName = currentProject?.name || 'No Project Selected';
// Render team-specific dashboard
switch(teamContext) {
case 'ui':
return UITeamDashboard(health, stats, discovery, activity, projectName, dashboardData, currentProjectId, this);
case 'ux':
return UXTeamDashboard(health, stats, discovery, activity, projectName, dashboardData, currentProjectId, this);
case 'qa':
return QATeamDashboard(health, stats, discovery, activity, projectName, dashboardData, currentProjectId, this);
default:
return this.renderAllTeamsDashboard(health, stats, discovery, activity, healthScore, healthGrade);
}
}
renderAllTeamsDashboard(health, stats, discovery, activity, healthScore, healthGrade) {
return `
Health Score
${healthScore}% (${healthGrade})
Design Tokens
${stats.tokens?.total || 0}
Components
${stats.components?.total || discovery.files?.components || 0}
Syncs Today
${stats.syncs?.today || 0}
Ingest Design System
Add a design system using natural language
Ingest
Browse
${this.renderIngestionResult()}
Quick Actions
Common operations
Extract Tokens
Sync Tokens
Validate
Visual Diff
Re-scan Project
Project Info
Discovered configuration
Project Types:
${(discovery.project?.types || []).join(', ') || 'Unknown'}
Frameworks:
${(discovery.project?.frameworks || []).join(', ') || 'None detected'}
Total Files:
${discovery.files?.total || 0}
CSS Files:
${discovery.files?.css || 0}
Git Branch:
${discovery.git?.branch || 'N/A'}
Uncommitted:
${discovery.git?.uncommitted_changes || 0} changes
Recent Activity
${activity.length > 0 ? `
${activity.slice(0, 5).map(item => `
${item.message}
${this.formatTime(item.timestamp)}
`).join('')}
` : 'No recent activity
'}
System Health
${health.checks ? `
${health.checks.map(check => `
${check.name}
${check.message || (check.latency ? `${check.latency}ms` : 'OK')}
`).join('')}
` : 'Loading health status...
'}
${discovery.health?.issues?.length > 0 ? `
Improvement Suggestions
${discovery.health.issues.map(issue => `
Suggestion
${issue}
`).join('')}
` : ''}
`;
}
renderTokens() {
const tokens = this.store.get('tokens') || [];
const selectedProject = this.store.get('selectedProject');
const exportFormat = this.store.get('tokenExportFormat') || 'css';
const byCategory = tokens.reduce((acc, t) => {
const cat = t.category || 'other';
if (!acc[cat]) acc[cat] = [];
acc[cat].push(t);
return acc;
}, {});
const categoryOrder = ['color', 'spacing', 'sizing', 'typography', 'radius', 'shadow', 'other'];
const sortedCategories = Object.keys(byCategory).sort((a, b) => {
return categoryOrder.indexOf(a) - categoryOrder.indexOf(b);
});
const stats = {
total: tokens.length,
colors: byCategory.color?.length || 0,
spacing: byCategory.spacing?.length || 0,
typography: byCategory.typography?.length || 0
};
return `
Total Tokens
${stats.total}
Typography
${stats.typography}
Extract from Figma
Sync to Files
Export as:
CSS
SCSS
JSON
TypeScript
${tokens.length > 0 ? sortedCategories.map(category => {
const categoryTokens = byCategory[category];
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
const isColor = category === 'color';
return `
${categoryName}
${categoryTokens.length} tokens
${isColor ? `
${categoryTokens.map(token => `
--${token.name}
${token.value}
`).join('')}
` : `
${categoryTokens.map(token => `
${category === 'spacing' || category === 'sizing' ? `
` : category === 'radius' ? `
` : category === 'shadow' ? `
` : ''}
--${token.name}
${token.value}
`).join('')}
`}
`}).join('') : `
No tokens extracted yet.
Connect to Figma and extract design tokens to see them here.
Extract Tokens from Figma
`}
`;
}
renderComponents() {
const components = this.store.get('components') || [];
const selectedFramework = this.store.get('componentFramework') || 'react';
const generatedCode = this.store.get('generatedCode');
const frameworks = [
{ id: 'react', name: 'React', icon: 'R' },
{ id: 'vue', name: 'Vue', icon: 'V' },
{ id: 'webcomponent', name: 'Web Component', icon: 'W' },
{ id: 'svelte', name: 'Svelte', icon: 'S' }
];
return `
Total Components
${components.length}
With Variants
${components.filter(c => c.variants?.length > 0).length}
With Properties
${components.filter(c => c.properties?.length > 0).length}
Target Framework
${frameworks.find(f => f.id === selectedFramework)?.name || 'React'}
Extract from Figma
Validate All
Generate for:
${frameworks.map(fw => `
${fw.icon}
`).join('')}
${generatedCode ? `
Generated Code: ${generatedCode.component}
Close
${this.escapeHtml(generatedCode.code)}
Copy to Clipboard
` : ''}
${components.length > 0 ? `
${components.map(comp => `
${comp.name.charAt(0).toUpperCase()}
${comp.name}
${comp.description || 'Component from Figma'}
${comp.variants?.length > 0 ? `
Variants:
${comp.variants.slice(0, 5).map(v => `${v} `).join('')}
${comp.variants.length > 5 ? `+${comp.variants.length - 5} ` : ''}
` : ''}
${comp.properties?.length > 0 ? `
Properties:
${comp.properties.slice(0, 4).map(p => `${p.name}: ${p.type} `).join('')}
${comp.properties.length > 4 ? `+${comp.properties.length - 4} ` : ''}
` : ''}
Key: ${comp.key?.slice(0, 8) || 'N/A'}...
Generate ${frameworks.find(f => f.id === selectedFramework)?.name || 'Code'}
Details
`).join('')}
` : `
No components extracted yet.
Extract components from your Figma file to generate code.
Extract Components from Figma
`}
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
renderFigma() {
const figmaFileKey = this.store.get('figmaFileKey') || '';
const selectedProject = this.store.get('selectedProject');
const projectDescription = selectedProject?.description || '';
const lastSync = this.store.get('lastSync');
return `
Project Details
Describe your design system project
Save Description
Figma Connection
Configure your Figma file
Find this in your Figma URL: figma.com/file/[FILE_KEY] /...
Save & Connect
Sync Status
Connection:
${figmaFileKey ? 'Connected' : 'Not configured'}
Last Sync:
${lastSync ? this.formatTime(lastSync) : 'Never'}
Available Tools
Figma integration capabilities
Extract Variables
Pull design tokens from Figma variables
Extract Components
Get component definitions and variants
Extract Styles
Pull text, color, and effect styles
Sync Tokens
Sync tokens to CSS/SCSS/JSON files
Visual Diff
Compare visual changes between versions
Validate Components
Check components against naming rules
Generate Code
Create component code from designs
`;
}
renderTeams() {
const user = this.store.get('user');
const role = this.store.get('role');
return `
${this.store.hasPermission('manage_team_members') ? `
Create Team
` : ''}
Design System Core
Active
Members:
5
Projects:
3
Your Role:
${role}
View Details
Product Team A
Member
View Details
Role Permissions
Super Admin
Full system access
Team Lead
Manage team, sync, generate
Developer
Read, write, sync
`;
}
renderProjects() {
const projects = this.store.get('projects') || [];
const showCreateForm = this.store.get('showCreateProjectForm');
return `
${showCreateForm ? 'Cancel' : 'New Project'}
${showCreateForm ? `
Create New Project
Create Project
` : ''}
${projects.length > 0 ? `
${projects.map(p => `
${p.name}
${p.status || 'active'}
${p.description || 'No description'}
Figma Key:
${p.figma_file_key || 'Not configured'}
Last Sync:
${p.last_sync ? this.formatTime(p.last_sync) : 'Never'}
Created:
${p.created_at ? this.formatTime(p.created_at) : 'Unknown'}
Open
Sync Tokens
Delete
`).join('')}
` : `
No projects yet. Create your first project to get started.
Create Your First Project
`}
`;
}
renderSettings() {
const config = this.store.get('runtimeConfig') || {};
const services = this.store.get('services') || {};
const figmaConfig = this.store.get('figmaConfig') || {};
const mode = config.mode || 'local';
return `
Server Mode
Choose how DSS operates
Local Dev Companion
Run alongside your project, provides UI dev assistance, component preview, and local services.
Remote Server
Deployed centrally, serves design systems to teams, multi-project management.
Figma Integration
Connect to Figma API
${figmaConfig.configured ? 'Connected' : 'Not configured'}
Get your token from Figma Settings → Personal Access Tokens
Save Token
Test Connection
${this.store.get('figmaTestResult') ? `
${this.store.get('figmaTestResult').success ?
`Connected as ${this.store.get('figmaTestResult').user}` :
`Error: ${this.store.get('figmaTestResult').error}`}
` : ''}
External Tools & Integrations
Configure connected tools and services
${this.renderComponentSettings()}
Companion Services
Discovered and configured services
${this.renderServiceCard('storybook', 'Storybook', services)}
${this.renderServiceCard('vite', 'Vite Dev Server', services)}
${this.renderServiceCard('nextjs', 'Next.js', services)}
Refresh Services
Features
Enable or disable DSS features
${this.renderFeatureToggle('visual_qa', 'Visual QA', 'Compare Figma designs with implementation', config.features)}
${this.renderFeatureToggle('token_sync', 'Token Sync', 'Sync design tokens to code', config.features)}
${this.renderFeatureToggle('code_gen', 'Code Generation', 'Generate component code from Figma', config.features)}
${this.renderFeatureToggle('ai_advisor', 'AI Advisor', 'Get AI suggestions for design system improvements', config.features)}
Appearance
Customize the interface
Dark Mode
Toggle Theme
Output Configuration
Token and component generation settings
⚠️ Danger Zone
Irreversible operations - use with caution
Reset DSS to Fresh State
This will delete all user-created themes, cached data, and project databases.
The DSS structure and default themes will be preserved.
Reset DSS
API Status
API Mode:
${this.store.get('useMock') ? 'Mock (Backend unavailable)' : 'Live'}
Base URL:
${window.location.hostname === 'localhost' ? 'http://localhost:3456/api' : '/api'}
`;
}
renderPlugins() {
const plugins = pluginService.getAll();
const enabledCount = plugins.filter(p => p.enabled).length;
return `
${plugins.map(plugin => this.renderPluginCard(plugin)).join('')}
`;
}
renderPluginCard(plugin) {
const settings = pluginService.getPluginSettings(plugin.id);
return `
${plugin.description}
${plugin.enabled && plugin.settings.length > 0 ? `
${plugin.settings.map(setting => this.renderPluginSetting(plugin.id, setting, settings)).join('')}
` : ''}
by ${plugin.author}
${plugin.enabled ? 'Enabled' : 'Disabled'}
`;
}
renderPluginSetting(pluginId, setting, currentSettings) {
const value = currentSettings[setting.key] ?? setting.default;
if (setting.type === 'select') {
return `
${setting.label}
${setting.options.map(opt => `
${opt.label}
`).join('')}
`;
}
if (setting.type === 'boolean') {
return `
`;
}
return '';
}
renderDocs() {
return `
${this.getDocContent('overview')}
`;
}
getDocContent(docId) {
const docs = {
overview: `
What is DSS?
Design System Server (DSS) is a platform that helps teams manage, sync, and evolve their design systems by connecting Figma designs to code.
Core Features
Token Extraction — Pull design tokens from Figma variables
Token Sync — Generate CSS/JSON from Figma tokens
Component Analysis — Scan your codebase for components
Visual Diff — Detect changes between Figma versions
AI Assistant — Get help via the built-in chat
Architecture
REST API — Port 3456 (python tools/api/server.py)
MCP Server — Port 3457 for AI tools
Admin UI — This dashboard
CLI — Command-line interface
`,
quickstart: `
Quick Start
1. Start the Server
cd apps/dss
python tools/api/server.py
2. Create a Project
Go to Projects → Create Project
3. Add Figma File
In your project settings, paste your Figma file key:
https://www.figma.com/file/FILE_KEY /...
4. Extract Tokens
Use the dashboard or CLI:
dss ingest figma YOUR_FILE_KEY
5. Sync to Code
Click "Sync Tokens" or:
dss sync tokens --output ./tokens.css
`,
concepts: `
Key Concepts
Design Tokens
Tokens are the atomic values of your design system: colors, spacing, typography. DSS extracts these from Figma variables and converts them to CSS custom properties or JSON.
Translation Dictionaries
DSS uses a canonical internal structure. When importing from external sources, translation dictionaries map external names to DSS standard names.
Token Drift
When code diverges from Figma designs, that's "drift". The UI Team dashboard tracks drift issues and helps resolve them.
ESRE (Expected State, Real State, Evidence)
QA team uses ESRE to define test cases: what should happen, what actually happens, and proof.
`,
'ui-team': `
UI Team Guide
As a UI developer, you'll use DSS to keep code in sync with designs.
Daily Workflow
Check the Dashboard for token drift alerts
Run token sync when Figma updates
Generate component code from new Figma components
Review and resolve drift issues
Key Tools
Extract Tokens — Pull latest from Figma
Sync Tokens — Update CSS variables
Generate Code — Create React/Web Components
Token Drift — Track code/design mismatches
CLI Commands
dss ingest figma FILE_KEY
dss sync tokens -o ./tokens.css
dss analyze ./src
`,
'ux-team': `
UX Team Guide
As a UX designer, DSS helps you maintain design consistency and validate implementations.
Daily Workflow
Add Figma files to projects
Run visual diff after design changes
Review token consistency reports
Validate component implementations
Key Tools
Figma Files — Manage connected design files
Visual Diff — Compare Figma versions
Token Validation — Ensure token consistency
Component Validation — Check naming conventions
Best Practices
Use Figma variables for all tokens
Follow component naming conventions
Document component variants
Run visual diff before handoff
`,
'qa-team': `
QA Team Guide
DSS helps QA teams validate design system implementations.
Daily Workflow
Review visual regression reports
Define ESRE test cases for components
Run component validation
Export audit logs for compliance
ESRE Testing
Define expected behaviors:
Expected State — What should happen
Real State — What actually happens
Evidence — Screenshots, logs, recordings
Key Tools
Component Validation — Automated checks
Visual Diff — Regression detection
Audit Log — Track all changes
ESRE Definitions — Test case management
`,
tokens: `
Design Tokens
Token Categories
Colors — Primary, secondary, semantic colors
Spacing — Margin, padding scales
Typography — Font sizes, weights, line heights
Radius — Border radius values
Shadows — Box shadow definitions
Export Formats
CSS — Custom properties (:root { --color-primary: ... })
JSON — Structured token data
SCSS — Sass variables
Token Naming
--{category}-{name}[-{variant}]
Examples:
--color-primary
--space-4
--text-lg
--radius-md
`,
figma: `
Figma Integration
Setup
Get a Personal Access Token from Figma Settings
Go to Settings → Figma Integration
Paste and save your token
Test the connection
File Key
Extract from your Figma URL:
https://www.figma.com/file/ABC123 /My-Design
↑ This is your file key
What Gets Extracted
Variables → Design tokens
Components → Component metadata
Styles → Text and color styles
`,
components: `
Components
Component Analysis
DSS scans your codebase to find React and Web Components:
dss analyze ./src
Code Generation
Generate component code from Figma:
React — Functional components with CSS modules
Web Components — Custom elements with Shadow DOM
Storybook Integration
Generate stories for your components:
dss storybook generate ./src/components
`,
'ai-chat': `
AI Chat
The AI assistant helps with design system tasks.
What It Can Do
Answer questions about your design system
Suggest improvements
Help debug issues
Execute DSS tools
Example Prompts
"Extract tokens from my Figma file"
"Show me quick wins for my codebase"
"What's the status of my project?"
"Help me fix token drift issues"
Tool Execution
The AI can run DSS tools directly. Look for tool suggestions in responses and click to execute.
`,
cli: `
CLI Commands
Installation
# Add to PATH
export PATH="$PATH:/path/to/dss/bin"
# Or use directly
./bin/dss --help
Commands
dss init
Initialize DSS in current directory
dss ingest figma FILE_KEY
Extract tokens from Figma file
dss sync tokens -o PATH
Sync tokens to CSS file
dss analyze PATH
Analyze codebase for components
dss storybook generate PATH
Generate Storybook stories
dss start
Start DSS server
`,
troubleshooting: `
Troubleshooting
Server Won't Start
# Check Python version (need 3.10+)
python --version
# Install dependencies
pip install -r requirements.txt
# Check port availability
lsof -i :3456
Figma Connection Failed
Verify token hasn't expired
Check token has read access to the file
Ensure file key is correct (not the full URL)
Tokens Not Syncing
Confirm Figma file has variables defined
Check output path is writable
Look for errors in browser console
API Errors
Check server is running: curl http://localhost:3456/health
Review server logs for details
Verify CORS settings for remote access
Getting Help
Use the AI Chat for guidance
Check logs: Settings → Debug Console
Export logs for support
`
};
return docs[docId] || docs.overview;
}
renderServiceCard(serviceId, serviceName, services) {
const discovered = services.discovered?.[serviceId] || {};
const configured = services.configured?.[serviceId] || {};
const running = discovered.running || false;
return `
${serviceName}
${running ? `Running on :${discovered.port}` : 'Not detected'}
${running ? `
Open
` : ''}
`;
}
/**
* Render component settings from the registry
* Generates dynamic UI for all enabled components
*/
renderComponentSettings() {
let dssHost = 'localhost';
try {
dssHost = getDssHost();
} catch {
// Config not loaded yet
}
const components = getEnabledComponents();
return components.map(component => `
${component.name}
${component.category}
${component.getUrl() ? `
Open
` : ''}
${component.description}
${component.id === 'storybook' ? `
URL: ${component.getUrl() || 'Not configured'}
Host from server config: ${dssHost}
Initialize Storybook
Clear Stories
` : ''}
${component.id === 'figma' ? `
Token status: Check connection above
` : ''}
`).join('');
}
renderFeatureToggle(featureId, name, description, features = {}) {
const enabled = features?.[featureId] !== false;
return `
${enabled ? 'Enabled' : 'Disabled'}
`;
}
/**
* Render ingestion result panel (after parsing a prompt)
*/
renderIngestionResult() {
const result = this.store.get('ingestionResult');
const browsing = this.store.get('browsingDesignSystems');
const systems = this.store.get('designSystems') || [];
// If browsing design systems
if (browsing) {
return `
Available Design Systems
Close
${systems.map(sys => `
${sys.name}
${sys.description?.slice(0, 60) || ''}...
${sys.category || 'library'}
${sys.framework ? `${sys.framework} ` : ''}
`).join('')}
`;
}
if (!result) return '';
const { intent, sources, next_steps, suggestions } = result;
// Found a design system to ingest
if (sources?.length > 0 && sources[0].matched_system) {
const system = sources[0].matched_system;
return `
${system.name}
${system.description}
${system.category || 'library'}
npm packages
${(system.npm_packages || []).join(', ') || 'N/A'}
Primary method
${system.primary_ingestion || 'npm'}
${system.figma_community_url ? `
` : ''}
Ingest ${system.name}
Other Methods
Cancel
`;
}
// Show next steps or suggestions
if (next_steps?.length > 0) {
const step = next_steps[0];
if (step.action === 'request_source') {
return `
${step.message}
${(step.alternatives || []).map(alt => `
${alt.name}
${alt.description}
`).join('')}
Cancel
`;
}
if (step.action === 'search_npm') {
return `
Not found in registry
${step.message}
Search npm
Cancel
`;
}
}
// Show suggestions
if (suggestions?.length > 0) {
return `
${suggestions.map(s => `${s} `).join('')}
`;
}
return '';
}
// === Event Handlers ===
attachEventHandlers() {
// Use event delegation for all [data-action] buttons
// Only attach the listener once to prevent accumulation
if (!this.listeners.hasActionListener) {
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const action = btn.dataset.action;
const component = btn.dataset.component;
switch (action) {
case 'extractTokens': this.extractTokens(); break;
case 'extractComponents': this.extractComponents(); break;
case 'syncTokens': this.syncTokens(); break;
case 'runVisualDiff': this.runVisualDiff(); break;
case 'validateComponents': this.validateComponents(); break;
case 'runDiscovery': this.runDiscovery(true); break;
case 'generateCode':
if (component) this.generateCode(component);
break;
case 'saveFigmaKey':
const input = document.getElementById('figma-file-key');
if (input) {
this.store.set({ figmaFileKey: input.value });
this.store.notify('Figma connection saved', 'success');
}
break;
case 'saveProjectDescription':
const descInput = document.getElementById('project-description');
const selectedProject = this.store.get('selectedProject');
if (descInput && selectedProject) {
this.updateProjectDescription(selectedProject.id, descInput.value);
}
break;
// Settings actions
case 'saveFigmaToken':
this.saveFigmaToken();
break;
case 'testFigmaConnection':
this.testFigmaConnection();
break;
case 'saveDssHost':
this.saveDssHost();
break;
case 'setMode':
const mode = btn.dataset.mode;
if (mode) this.setServerMode(mode);
break;
case 'toggleFeature':
const feature = btn.dataset.feature;
if (feature) this.toggleFeature(feature);
break;
case 'refreshServices':
this.loadServices();
break;
case 'initStorybook':
this.initStorybook();
break;
case 'clearStorybook':
this.clearStorybook();
break;
case 'resetDSS':
this.resetDSS();
break;
// Ingestion actions
case 'parseIngestion':
this.parseIngestionPrompt();
break;
case 'browseDesignSystems':
this.browseDesignSystems();
break;
case 'closeBrowse':
this.store.set({ browsingDesignSystems: false });
this.render();
break;
case 'selectDesignSystem':
const systemId = btn.dataset.systemId;
if (systemId) this.selectDesignSystem(systemId);
break;
case 'confirmIngestion':
const confirmSystemId = btn.dataset.systemId;
if (confirmSystemId) this.confirmIngestion(confirmSystemId);
break;
case 'showAlternatives':
const altSystemId = btn.dataset.systemId;
if (altSystemId) this.showIngestionAlternatives(altSystemId);
break;
case 'clearIngestion':
this.store.set({ ingestionResult: null, browsingDesignSystems: false });
this.render();
break;
case 'navigate-quick-wins':
window.location.hash = 'quick-wins';
break;
case 'searchNpm':
const npmQuery = btn.dataset.query;
if (npmQuery) this.searchNpmPackages(npmQuery);
break;
case 'selectIngestionMethod':
const methodType = btn.dataset.method;
if (methodType) this.selectIngestionMethod(methodType);
break;
// Project actions
case 'toggleCreateProject':
this.store.set({ showCreateProjectForm: !this.store.get('showCreateProjectForm') });
this.render();
break;
case 'submitCreateProject':
const nameInput = document.getElementById('new-project-name');
if (nameInput && nameInput.value) {
this.createProject(
nameInput.value,
'', // description
'' // figma key
);
} else {
this.store.notify('Please enter a project name', 'warning');
}
break;
case 'selectProject':
const selectProjectId = btn.dataset.projectId;
if (selectProjectId) this.selectProject(selectProjectId);
break;
case 'syncProject':
const syncProjectId = btn.dataset.projectId;
const syncFigmaKey = btn.dataset.figmaKey;
if (syncProjectId) this.syncProjectTokens(syncProjectId, syncFigmaKey);
break;
case 'deleteProject':
const deleteProjectId = btn.dataset.projectId;
if (deleteProjectId) this.deleteProject(deleteProjectId);
break;
// Token export format
case 'setExportFormat':
const format = btn.dataset.format;
if (format) {
this.store.set({ tokenExportFormat: format });
this.render();
}
break;
// Component actions
case 'viewComponentCode':
const compName = btn.dataset.component;
const compFramework = btn.dataset.framework || 'react';
if (compName) this.showComponentCode(compName, compFramework);
break;
case 'setFramework':
const fw = btn.dataset.framework;
if (fw) {
this.store.set({ componentFramework: fw });
this.render();
}
break;
case 'closeGeneratedCode':
this.store.set({ generatedCode: null });
this.render();
break;
case 'copyCode':
const code = this.store.get('generatedCode')?.code;
if (code) {
navigator.clipboard.writeText(code);
this.store.notify('Code copied to clipboard', 'success');
}
break;
// New services and quick wins actions
case 'executeTool':
const toolName = btn.dataset.tool;
if (toolName) this.executeToolWithParams(toolName);
break;
case 'loadQuickWins':
this.loadQuickWins();
break;
case 'investigateWin':
const winId = btn.dataset.winId;
if (winId) this.investigateWin(winId);
break;
case 'markDone':
const doneWinId = btn.dataset.winId;
if (doneWinId) this.markWinDone(doneWinId);
break;
// Chat actions
case 'sendChatMessage':
this.sendChatMessage({ target: btn });
break;
case 'clearChat':
claudeService.clearHistory();
this.render();
break;
case 'exportChat':
claudeService.exportConversation();
break;
// Team Dashboard actions
case 'sync-figma-file':
const fileId = btn.dataset.fileId;
if (fileId) this.syncFigmaFile(fileId);
break;
case 'delete-figma-file':
const deleteFileId = btn.dataset.fileId;
if (deleteFileId && confirm('Delete this Figma file?')) {
this.deleteFigmaFile(deleteFileId);
}
break;
// Plugin actions
case 'togglePlugin':
const pluginId = btn.dataset.plugin;
if (pluginId) this.togglePlugin(pluginId);
break;
case 'setPluginSetting':
const settingPluginId = btn.dataset.plugin;
const settingKey = btn.dataset.key;
const settingType = btn.dataset.type;
if (settingPluginId && settingKey) {
let settingValue;
if (settingType === 'boolean') {
const current = pluginService.getPluginSettings(settingPluginId)[settingKey];
settingValue = !current;
} else if (btn.tagName === 'SELECT') {
settingValue = btn.value;
}
this.setPluginSetting(settingPluginId, settingKey, settingValue);
}
break;
// Theme actions
case 'toggle-theme':
themeManager.toggle();
this.render();
break;
// Audit actions
case 'load-audit':
this.loadAuditLog();
break;
case 'clear-audit-filters':
this.clearAuditFilters();
break;
case 'export-audit':
const exportFormat = btn.dataset.format || 'json';
this.exportAuditLog(exportFormat);
break;
case 'prev-audit-page':
this.prevAuditPage();
break;
case 'next-audit-page':
this.nextAuditPage();
break;
case 'show-audit-details':
const auditId = btn.dataset.auditId;
if (auditId) this.showAuditDetails(parseInt(auditId));
break;
}
});
this.listeners.hasActionListener = true;
}
// Chat form submission
const chatForm = document.getElementById('chatForm');
if (chatForm) {
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendChatMessage(e);
});
}
// Team Dashboard form submissions
const addFigmaFileForm = document.getElementById('add-figma-file-form');
if (addFigmaFileForm) {
addFigmaFileForm.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
this.addFigmaFile({
file_name: formData.get('file_name'),
figma_url: formData.get('figma_url'),
file_key: formData.get('file_key')
});
});
}
const addESREForm = document.getElementById('add-esre-form');
if (addESREForm) {
addESREForm.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
this.addESREDefinition({
name: formData.get('name'),
definition_text: formData.get('definition_text'),
component_name: formData.get('component_name') || null,
expected_value: formData.get('expected_value') || null
});
});
}
// Search and filter handlers for services page
const toolSearch = document.getElementById('toolSearch');
if (toolSearch) {
toolSearch.addEventListener('input', (e) => {
this.filterTools(e.target.value, document.getElementById('categoryFilter')?.value || '');
});
}
const categoryFilter = document.getElementById('categoryFilter');
if (categoryFilter) {
categoryFilter.addEventListener('change', (e) => {
this.filterTools(document.getElementById('toolSearch')?.value || '', e.target.value);
});
}
// Docs navigation
document.querySelectorAll('.docs-nav__link[data-doc]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const docId = e.currentTarget.dataset.doc;
const content = document.getElementById('docs-content');
if (content && docId) {
setSafeHtml(content, this.getDocContent(docId));
document.querySelectorAll('.docs-nav__link').forEach(l => l.classList.remove('active'));
e.currentTarget.classList.add('active');
}
});
});
// Setup keyboard shortcuts
this.setupKeyboardHandlers();
}
setupKeyboardHandlers() {
// Handle global keyboard shortcuts
if (!this.listeners.hasKeyboardListener) {
this.listeners.keyboardHandler = (e) => {
// Escape key: Close sidebars and modals
if (e.key === 'Escape') {
// Close AI sidebar if visible
const aiSidebar = document.getElementById('ai-sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
if (aiSidebar && !aiSidebar.classList.contains('hidden')) {
if (toggleBtn) {
toggleBtn.click();
// Update ARIA state
setTimeout(() => {
const isExpanded = !aiSidebar.classList.contains('hidden');
toggleBtn.setAttribute('aria-expanded', isExpanded);
}, 0);
}
}
// Close any open details/summary elements
document.querySelectorAll('details[open]').forEach(details => {
details.open = false;
});
}
// Alt+N: Navigate to next page
if (e.altKey && e.key === 'n') {
e.preventDefault();
const navItems = Array.from(document.querySelectorAll('.nav-item'));
const currentPage = this.store.get('currentPage');
const currentIndex = navItems.findIndex(item => item.dataset.page === currentPage);
if (currentIndex >= 0 && currentIndex < navItems.length - 1) {
navItems[currentIndex + 1].focus();
navItems[currentIndex + 1].click();
}
}
// Alt+P: Navigate to previous page
if (e.altKey && e.key === 'p') {
e.preventDefault();
const navItems = Array.from(document.querySelectorAll('.nav-item'));
const currentPage = this.store.get('currentPage');
const currentIndex = navItems.findIndex(item => item.dataset.page === currentPage);
if (currentIndex > 0) {
navItems[currentIndex - 1].focus();
navItems[currentIndex - 1].click();
}
}
};
document.addEventListener('keydown', this.listeners.keyboardHandler);
this.listeners.hasKeyboardListener = true;
}
}
// === Settings Actions ===
async saveFigmaToken() {
const input = document.getElementById('figma-token-input');
if (!input || !input.value || input.value.includes('••••')) {
this.store.notify('Please enter a valid Figma token', 'warning');
return;
}
this.store.setLoading('saveFigmaToken', true);
try {
const api = (await import('./api.js')).default;
await api.setFigmaToken(input.value);
this.store.set({ figmaConfig: { configured: true } });
this.store.notify('Figma token saved', 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to save token: ${error.message}`, 'error');
} finally {
this.store.setLoading('saveFigmaToken', false);
}
}
async testFigmaConnection() {
this.store.setLoading('testFigma', true);
try {
const api = (await import('./api.js')).default;
const result = await api.testFigmaConnection();
this.store.set({ figmaTestResult: result });
if (result.success) {
this.store.notify('Figma connection successful!', 'success');
} else {
this.store.notify(`Connection failed: ${result.error}`, 'error');
}
this.render();
} catch (error) {
this.store.set({ figmaTestResult: { success: false, error: error.message } });
this.store.notify(`Test failed: ${error.message}`, 'error');
this.render();
} finally {
this.store.setLoading('testFigma', false);
}
}
async saveDssHost() {
const input = document.getElementById('dss-host-input');
if (!input) {
notifyError('DSS host input not found', ErrorCode.SYSTEM_UNEXPECTED);
return;
}
const newHost = input.value.trim() || 'localhost';
this.store.setLoading('saveDssHost', true);
try {
// Validate hostname format (alphanumeric, dots, hyphens)
if (!/^[a-zA-Z0-9.-]+$/.test(newHost)) {
notifyError('Invalid hostname format', ErrorCode.VALIDATION_INVALID_FORMAT, {
host: newHost
});
this.store.setLoading('saveDssHost', false);
return;
}
// Save to store (will auto-persist via subscription)
this.store.set({ dssHost: newHost });
// Update the link immediately
this.updateStorybookLink();
// Notify success
notifySuccess('Host configuration saved successfully!', ErrorCode.SUCCESS_UPDATED, {
host: newHost,
storybookUrl: `${window.location.protocol}//${newHost}:6006`
});
this.render();
} catch (error) {
handleError(error, {
operation: 'save DSS host',
host: newHost
});
} finally {
this.store.setLoading('saveDssHost', false);
}
}
async resetDSS() {
// Confirm with user
const confirmed = window.confirm(
'⚠️ WARNING: This will delete all user-created themes, projects, cached data, and databases.\n\n' +
'The DSS structure and default themes will be preserved.\n\n' +
'Type "RESET" in the next prompt to confirm.'
);
if (!confirmed) {
return;
}
const confirmText = window.prompt('Type RESET (all caps) to confirm:');
if (confirmText !== 'RESET') {
this.store.notify('Reset cancelled', 'info');
return;
}
this.store.setLoading('resetDSS', true);
try {
this.store.notify('Resetting DSS... This may take a moment.', 'info');
// Call Python reset command via shell
const response = await fetch('/api/system/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirm: 'RESET' })
});
if (!response.ok) {
throw new Error('Reset command failed');
}
const result = await response.json();
this.store.notify('✅ DSS has been reset to fresh state!', 'success');
// Reload after a short delay
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (error) {
this.store.notify(`Reset failed: ${error.message}`, 'error');
} finally {
this.store.setLoading('resetDSS', false);
}
}
async setServerMode(mode) {
try {
const api = (await import('./api.js')).default;
await api.setMode(mode);
const config = this.store.get('runtimeConfig') || {};
config.mode = mode;
this.store.set({ runtimeConfig: config });
this.store.notify(`Server mode set to ${mode}`, 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to set mode: ${error.message}`, 'error');
}
}
async toggleFeature(featureId) {
const config = this.store.get('runtimeConfig') || {};
const features = config.features || {};
const newValue = features[featureId] === false;
try {
const api = (await import('./api.js')).default;
await api.updateConfig({ features: { [featureId]: newValue } });
features[featureId] = newValue;
config.features = features;
this.store.set({ runtimeConfig: config });
this.store.notify(`${featureId} ${newValue ? 'enabled' : 'disabled'}`, 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to toggle feature: ${error.message}`, 'error');
}
}
async togglePlugin(pluginId) {
try {
await pluginService.toggle(pluginId);
const plugin = pluginService.get(pluginId);
this.store.notify(`${plugin.name} ${plugin.enabled ? 'enabled' : 'disabled'}`, 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to toggle plugin: ${error.message}`, 'error');
}
}
setPluginSetting(pluginId, key, value) {
pluginService.setPluginSetting(pluginId, key, value);
this.store.notify('Setting updated', 'success');
this.render();
}
async loadServices() {
try {
const api = (await import('./api.js')).default;
const services = await api.getServices();
this.store.set({ services });
this.render();
} catch (error) {
console.error('Failed to load services:', error);
}
}
async initStorybook() {
try {
this.store.notify('Initializing Storybook...', 'info');
const api = (await import('./api.js')).default;
const result = await api.post('/storybook/init', {});
if (result.success) {
this.store.notify(result.message || 'Storybook initialized', 'success');
} else {
this.store.notify('Failed to initialize Storybook', 'error');
}
} catch (error) {
console.error('Failed to initialize Storybook:', error);
this.store.notify(`Failed to initialize Storybook: ${error.message}`, 'error');
}
}
async clearStorybook() {
try {
this.store.notify('Clearing Storybook stories...', 'info');
const api = (await import('./api.js')).default;
const result = await api.delete('/storybook/stories');
if (result.success) {
this.store.notify(result.message || 'Stories cleared', 'success');
} else {
this.store.notify('Failed to clear stories', 'error');
}
} catch (error) {
console.error('Failed to clear Storybook:', error);
this.store.notify(`Failed to clear stories: ${error.message}`, 'error');
}
}
// === Design System Ingestion ===
async parseIngestionPrompt() {
const input = document.getElementById('ingest-prompt');
if (!input || !input.value.trim()) {
this.store.notify('Please enter a design system name or prompt', 'warning');
return;
}
try {
this.store.setLoading('ingestion', true);
this.store.notify('Parsing ingestion prompt...', 'info');
const api = (await import('./api.js')).default;
const result = await api.post('/ingest/parse', {
prompt: input.value.trim(),
project_id: this.store.get('currentProject')
});
this.store.set({ ingestionResult: result });
this.render();
// Show helpful message
if (result.sources?.length > 0 && result.sources[0].matched_system) {
this.store.notify(`Found: ${result.sources[0].matched_system.name}`, 'success');
} else if (result.next_steps?.length > 0) {
this.store.notify(result.next_steps[0].message || 'Processing...', 'info');
}
} catch (error) {
console.error('Ingestion parse failed:', error);
this.store.notify(`Failed to parse: ${error.message}`, 'error');
} finally {
this.store.setLoading('ingestion', false);
}
}
async browseDesignSystems() {
try {
this.store.notify('Loading design systems...', 'info');
const api = (await import('./api.js')).default;
const result = await api.get('/ingest/systems');
this.store.set({
designSystems: result.systems || [],
browsingDesignSystems: true,
ingestionResult: null
});
this.render();
} catch (error) {
console.error('Failed to load design systems:', error);
this.store.notify(`Failed to load: ${error.message}`, 'error');
}
}
async selectDesignSystem(systemId) {
try {
const api = (await import('./api.js')).default;
const result = await api.get(`/ingest/systems/${systemId}`);
this.store.set({
ingestionResult: {
sources: [{ matched_system: result.system }],
alternatives: result.alternatives
},
browsingDesignSystems: false
});
this.render();
} catch (error) {
console.error('Failed to select design system:', error);
this.store.notify(`Failed: ${error.message}`, 'error');
}
}
async confirmIngestion(systemId) {
try {
this.store.setLoading('ingestion', true);
this.store.notify('Starting ingestion...', 'info');
const api = (await import('./api.js')).default;
const result = await api.post('/ingest/confirm', {
system_id: systemId,
method: 'npm' // Default to npm
});
if (result.success) {
this.store.notify(`Queued: ${result.message}`, 'success');
// Show next steps
if (result.next_steps?.length > 0) {
setTimeout(() => {
this.store.notify(`Next: ${result.next_steps.join(' → ')}`, 'info');
}, 1500);
}
// Clear ingestion UI
this.store.set({ ingestionResult: null });
this.render();
} else {
this.store.notify('Ingestion failed', 'error');
}
} catch (error) {
console.error('Ingestion failed:', error);
this.store.notify(`Failed: ${error.message}`, 'error');
} finally {
this.store.setLoading('ingestion', false);
}
}
async showIngestionAlternatives(systemId) {
try {
const api = (await import('./api.js')).default;
const result = await api.get(`/ingest/alternatives?system_id=${systemId}`);
// Update the ingestion result to show alternatives
const currentResult = this.store.get('ingestionResult') || {};
this.store.set({
ingestionResult: {
...currentResult,
next_steps: [{
action: 'request_source',
message: 'Choose an ingestion method:',
alternatives: result.alternatives || []
}]
}
});
this.render();
} catch (error) {
console.error('Failed to get alternatives:', error);
this.store.notify(`Failed: ${error.message}`, 'error');
}
}
async searchNpmPackages(query) {
try {
this.store.notify(`Searching npm for "${query}"...`, 'info');
const api = (await import('./api.js')).default;
const result = await api.get(`/ingest/npm/search?query=${encodeURIComponent(query)}`);
if (result.packages?.length > 0) {
// Show npm results as design systems to choose from
this.store.set({
designSystems: result.packages.map(pkg => ({
id: pkg.name,
name: pkg.name,
description: pkg.description,
npm_packages: [pkg.name],
category: pkg.is_design_system ? 'npm-design-system' : 'npm-package',
homepage: pkg.homepage
})),
browsingDesignSystems: true,
ingestionResult: null
});
this.store.notify(`Found ${result.packages.length} packages`, 'success');
} else {
this.store.notify('No packages found on npm', 'warning');
}
this.render();
} catch (error) {
console.error('npm search failed:', error);
this.store.notify(`Search failed: ${error.message}`, 'error');
}
}
selectIngestionMethod(methodType) {
// Show appropriate UI for the selected method
const prompts = {
figma_url: 'Enter Figma file URL:',
css_url: 'Enter CSS file URL:',
github_url: 'Enter GitHub repository URL:',
image_url: 'Upload or enter image URL:',
text_description: 'Describe your design tokens:'
};
const prompt = prompts[methodType] || 'Enter source:';
const value = window.prompt(prompt);
if (value) {
// Store the method and source for later use
this.store.set({
ingestionMethod: methodType,
ingestionSource: value
});
this.store.notify(`Source saved. Click "Ingest" to proceed.`, 'success');
}
}
async loadConfig() {
try {
const api = (await import('./api.js')).default;
const [configData, figmaConfig, services] = await Promise.all([
api.getConfig(),
api.getFigmaConfig(),
api.getServices()
]);
this.store.set({
runtimeConfig: configData.config || {},
figmaConfig: figmaConfig || {},
services: services || {}
});
} catch (error) {
console.error('Failed to load config:', error);
}
}
// === Project Management ===
async loadProjects() {
try {
const api = (await import('./api.js')).default;
const projects = await api.getProjects();
this.store.setProjects(Array.isArray(projects) ? projects : []);
// Render project selector after projects are loaded
this.renderProjectSelector();
} catch (error) {
console.error('Failed to load projects:', error);
this.store.setProjects([]);
this.renderProjectSelector();
}
}
async loadDashboardData(projectId) {
if (!projectId) return;
try {
logger.info('App', 'Loading dashboard data', { projectId });
const dashboardData = await DashboardService.getDashboardSummary(projectId);
this.store.set({ dashboardData });
this.render(); // Re-render to show updated data
} catch (error) {
logger.error('App', 'Failed to load dashboard data', error);
// Set empty dashboard data on error
this.store.set({
dashboardData: {
ux: { figma_files_count: 0, figma_files: [] },
ui: { token_drift: { total: 0, by_severity: {} }, code_metrics: {} },
qa: { esre_count: 0, test_summary: {} }
}
});
}
}
renderProjectSelector() {
const container = document.getElementById('project-selector-container');
if (!container) return;
const projects = this.store.get('projects') || [];
const currentProject = this.store.get('currentProject') || (projects.length > 0 ? projects[0].id : null);
// Restore from localStorage if available
const savedProject = localStorage.getItem('dss_current_project');
if (savedProject && projects.find(p => p.id === savedProject)) {
this.store.set({ currentProject: savedProject });
} else if (currentProject) {
this.store.set({ currentProject });
}
const html = `
Project:
${projects.length === 0
? 'No projects '
: projects.map(p => `
${escapeHtml(p.name)}
`).join('')}
`;
setSafeHtml(container, html);
// Attach event listener
const select = document.getElementById('project-select');
if (select) {
select.addEventListener('change', (e) => {
const projectId = e.target.value;
this.handleProjectChange(projectId);
});
}
}
handleProjectChange(projectId) {
logger.info('App', 'Project context changed', { projectId });
// Save to store and localStorage
this.store.set({ currentProject: projectId });
localStorage.setItem('dss_current_project', projectId);
// Get project details
const projects = this.store.get('projects') || [];
const project = projects.find(p => p.id === projectId);
// Trigger refresh
this.render();
logger.info('App', 'Project context set', { project: project?.name });
}
// === Audit Log ===
renderAuditLog() {
// Load audit log data after render
setTimeout(() => this.loadAuditLog(), 100);
return `
Filters
Apply Filters
Clear Filters
📥 Export JSON
📥 Export CSV
Activity History
Loading...
`;
}
async loadAuditLog() {
const auditService = (await import('../services/audit-service.js')).default;
const content = document.getElementById('audit-log-content');
if (!content) return;
// Get filter values
const filters = {
category: document.getElementById('audit-category-filter')?.value || '',
severity: document.getElementById('audit-severity-filter')?.value || '',
start_date: document.getElementById('audit-start-date')?.value || '',
end_date: document.getElementById('audit-end-date')?.value || '',
limit: 50,
offset: this.auditLogOffset || 0
};
// Add current project filter if set
const currentProject = this.store.get('currentProject');
if (currentProject) {
filters.project_id = currentProject;
}
try {
const result = await auditService.getAuditLog(filters);
// Update total count
const totalCount = document.getElementById('audit-total-count');
if (totalCount) {
totalCount.textContent = `${result.total} total events`;
}
// Render table
if (result.activities && result.activities.length > 0) {
const html = `
Time
User
Action
Category
Description
Severity
${result.activities.map(activity => `
${escapeHtml(auditService.formatTimestamp(activity.created_at))}
${escapeHtml(activity.user_name || activity.user_id || 'System')}
${escapeHtml(activity.action)}
${auditService.getCategoryIcon(activity.category)} ${escapeHtml(activity.category || 'other')}
${escapeHtml(activity.description || '-')}
${escapeHtml(activity.severity)}
`).join('')}
`;
setSafeHtml(content, html);
} else {
content.innerHTML = `
No audit entries found
`;
}
// Update pagination
const pagination = document.getElementById('audit-pagination');
if (pagination && result.total > 0) {
pagination.style.display = 'flex';
const showing = document.getElementById('audit-showing');
const total = document.getElementById('audit-total');
const prevBtn = document.getElementById('audit-prev-btn');
const nextBtn = document.getElementById('audit-next-btn');
const start = filters.offset + 1;
const end = Math.min(filters.offset + result.activities.length, result.total);
if (showing) showing.textContent = `${start}-${end}`;
if (total) total.textContent = result.total;
if (prevBtn) {
if (filters.offset === 0) {
prevBtn.setAttribute('disabled', '');
} else {
prevBtn.removeAttribute('disabled');
}
}
if (nextBtn) {
if (!result.has_more) {
nextBtn.setAttribute('disabled', '');
} else {
nextBtn.removeAttribute('disabled');
}
}
this.auditLogData = result;
}
} catch (error) {
content.innerHTML = `
Failed to load audit log: ${error.message}
`;
}
}
clearAuditFilters() {
document.getElementById('audit-category-filter').value = '';
document.getElementById('audit-severity-filter').value = '';
document.getElementById('audit-start-date').value = '';
document.getElementById('audit-end-date').value = '';
this.auditLogOffset = 0;
this.loadAuditLog();
}
nextAuditPage() {
const limit = 50;
this.auditLogOffset = (this.auditLogOffset || 0) + limit;
this.loadAuditLog();
}
prevAuditPage() {
const limit = 50;
this.auditLogOffset = Math.max(0, (this.auditLogOffset || 0) - limit);
this.loadAuditLog();
}
async exportAuditLog(format = 'json') {
const auditService = (await import('../services/audit-service.js')).default;
const filters = {
category: document.getElementById('audit-category-filter')?.value || '',
start_date: document.getElementById('audit-start-date')?.value || '',
end_date: document.getElementById('audit-end-date')?.value || ''
};
// Add current project filter
const currentProject = this.store.get('currentProject');
if (currentProject) {
filters.project_id = currentProject;
}
await auditService.export(filters, format);
this.store.notify(`Audit log export started (${format})`, 'success');
}
showAuditDetails(id) {
// TODO: Show modal with full audit entry details
console.log('Show audit details for:', id);
}
async createProject(name, description, figmaFileKey) {
this.store.setLoading('createProject', true);
try {
const api = (await import('./api.js')).default;
const project = await api.createProject({
name,
description,
figma_file_key: figmaFileKey
});
this.store.addProject(project);
this.store.set({ showCreateProjectForm: false });
this.store.notify(`Project "${name}" created successfully`, 'success');
this.render();
return project;
} catch (error) {
this.store.notify(`Failed to create project: ${error.message}`, 'error');
throw error;
} finally {
this.store.setLoading('createProject', false);
}
}
async updateProjectDescription(projectId, description) {
this.store.setLoading('updateProject', true);
try {
const api = (await import('./api.js')).default;
await api.updateProject(projectId, { description });
// Update project in store
const projects = this.store.get('projects').map(p =>
p.id === projectId ? { ...p, description } : p
);
this.store.setProjects(projects);
// Update selected project
const selectedProject = this.store.get('selectedProject');
if (selectedProject && selectedProject.id === projectId) {
this.store.set({ selectedProject: { ...selectedProject, description } });
}
this.store.notify('Project description updated', 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to update description: ${error.message}`, 'error');
} finally {
this.store.setLoading('updateProject', false);
}
}
async deleteProject(projectId) {
if (!confirm('Are you sure you want to delete this project? This cannot be undone.')) {
return;
}
this.store.setLoading('deleteProject', true);
try {
const api = (await import('./api.js')).default;
await api.deleteProject(projectId);
const projects = this.store.get('projects').filter(p => p.id !== projectId);
this.store.setProjects(projects);
this.store.notify('Project deleted', 'success');
this.render();
} catch (error) {
this.store.notify(`Failed to delete project: ${error.message}`, 'error');
} finally {
this.store.setLoading('deleteProject', false);
}
}
selectProject(projectId) {
const project = this.store.get('projects').find(p => p.id === projectId);
if (project) {
this.store.set({
selectedProject: project,
figmaFileKey: project.figma_file_key
});
this.store.notify(`Selected project: ${project.name}`, 'info');
// Navigate to dashboard with project context
this.navigate('dashboard');
}
}
async syncProjectTokens(projectId, figmaKey) {
if (!figmaKey) {
this.store.notify('No Figma file key configured for this project', 'warning');
return;
}
this.store.set({ figmaFileKey: figmaKey });
await this.extractTokens();
await this.loadProjects(); // Refresh to get updated last_sync
}
// === Notifications ===
renderNotifications(notifications) {
let container = document.getElementById('notifications');
if (!container) {
container = document.createElement('div');
container.id = 'notifications';
container.className = 'notification-container'; // Use CSS class instead of inline styles
document.body.appendChild(container);
}
const html = notifications.map(n => {
const notificationType = escapeHtml(n.type);
const notificationMessage = escapeHtml(n.message);
return `
${notificationMessage}
`;
}).join('');
setSafeHtml(container, html);
}
// === Services Page ===
renderServices() {
const teamContext = this.store.get('teamContext') || localStorage.getItem('dss_team_context') || 'all';
const tools = toolsService.getToolsByTeam(teamContext);
const categories = toolsService.getCategories();
const teamNames = {
'all': 'All Tools',
'ui': 'UI Team',
'ux': 'UX Team',
'qa': 'QA Team'
};
// Group tools by category
const toolsByCategory = {};
tools.forEach(tool => {
if (!toolsByCategory[tool.category]) {
toolsByCategory[tool.category] = [];
}
toolsByCategory[tool.category].push(tool);
});
// Category icons
const categoryIcons = {
'Projects': ' ',
'Figma': ' ',
'Ingestion': ' ',
'Analysis': ' ',
'Storybook': ' ',
'Activity': ' '
};
logger.info('UI', 'Rendering services page', { toolCount: tools.length, teamContext, categories });
return `
`;
}
renderToolCard(tool) {
return `
`;
}
// === Quick Wins Page ===
renderQuickWins() {
logger.info('UI', 'Rendering quick wins page');
return `
Click "Analyze Project" to discover quick wins
`;
}
async loadQuickWins() {
const path = document.getElementById('projectPath')?.value || '.';
const container = document.getElementById('quickWinsContainer');
if (!container) return;
logger.info('QuickWins', 'Loading quick wins', { path });
container.innerHTML = 'Analyzing project...
';
try {
const result = await toolsService.executeTool('get_quick_wins', { path });
const data = typeof result === 'string' ? JSON.parse(result) : result;
logger.info('QuickWins', 'Quick wins loaded', data);
container.innerHTML = this.renderQuickWinsResults(data);
} catch (error) {
logger.error('QuickWins', 'Failed to load quick wins', error);
container.innerHTML = `
Failed to load quick wins: ${error.message}
`;
}
}
renderQuickWinsResults(data) {
const wins = data.wins || data.quick_wins || [];
if (wins.length === 0) {
return 'No quick wins found. Great job!
';
}
const byPriority = {
CRITICAL: wins.filter(w => w.priority === 'CRITICAL'),
HIGH: wins.filter(w => w.priority === 'HIGH'),
MEDIUM: wins.filter(w => w.priority === 'MEDIUM'),
LOW: wins.filter(w => w.priority === 'LOW')
};
return `
Critical
${byPriority.CRITICAL.length}
High
${byPriority.HIGH.length}
Medium
${byPriority.MEDIUM.length}
Low
${byPriority.LOW.length}
${wins.map(win => this.renderQuickWinCard(win)).join('')}
`;
}
renderQuickWinCard(win) {
const priorityColors = {
CRITICAL: 'destructive',
HIGH: 'warning',
MEDIUM: 'primary',
LOW: 'muted'
};
const color = priorityColors[win.priority] || 'muted';
return `
${win.priority}
${win.type}
${win.effort ? `
Effort: ${win.effort} ` : ''}
${win.title}
${win.description}
${win.file ? `
📄 ${win.file}
` : ''}
${win.impact ? `
Impact: ${win.impact}
` : ''}
🔍 Investigate
✓ Mark Done
`;
}
// === Claude Chat Page ===
renderChat() {
const history = claudeService.getHistory();
const selectedModel = this.store.get('selectedAIModel') || 'claude';
logger.info('UI', 'Rendering chat page', { historyLength: history.length, model: selectedModel });
return `
${history.length === 0 ? `
👋 Hi! I'm Claude
Ask me anything about your design system, tokens, or code
` : history.map(msg => this.renderChatMessage(msg)).join('')}
`;
}
renderChatMessage(msg) {
const isUser = msg.role === 'user';
const bgColor = isUser ? 'bg-primary/10' : 'bg-card';
const align = isUser ? 'ml-auto' : '';
const icon = isUser ? '👤' : '🤖';
return `
${icon}
${isUser ? 'You' : 'Claude'}
${this.formatTime(msg.timestamp)}
${this.escapeHtml(msg.content)}
`;
}
async sendChatMessage(e) {
if (e) e.preventDefault();
const input = document.getElementById('chatInput');
const message = input?.value.trim();
if (!message) return;
logger.info('Chat', 'Sending message', { message });
// Clear input
input.value = '';
// Add user message to UI immediately
const messagesContainer = document.getElementById('chatMessages');
if (messagesContainer) {
messagesContainer.innerHTML += this.renderChatMessage({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
// Add loading indicator
messagesContainer.innerHTML += `
`;
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
try {
const response = await claudeService.chat(message, {
project: this.store.get('currentProject'),
page: 'chat'
});
// Remove loading indicator
document.getElementById('chatLoading')?.remove();
// Add Claude's response
if (messagesContainer) {
messagesContainer.innerHTML += this.renderChatMessage({
role: 'assistant',
content: response,
timestamp: new Date().toISOString()
});
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
} catch (error) {
logger.error('Chat', 'Failed to send message', error);
document.getElementById('chatLoading')?.remove();
if (messagesContainer) {
messagesContainer.innerHTML += `
❌ Failed to get response: ${error.message}
`;
}
this.store.notify(`Chat error: ${error.message}`, 'error');
}
}
// === Tool Execution Actions ===
async executeToolWithParams(toolName) {
logger.info('ToolExecution', `Preparing to execute ${toolName}`);
const tool = toolsService.toolsMetadata[toolName];
if (!tool) {
logger.error('ToolExecution', `Tool not found: ${toolName}`);
return;
}
// For simple tools with no required params, execute directly
if (tool.parameters.length === 0) {
try {
this.store.notify(`Executing ${toolName}...`, 'info');
const result = await toolsService.executeTool(toolName, {});
logger.info('ToolExecution', `Tool ${toolName} completed`, result);
this.store.notify(`${toolName} completed successfully`, 'success');
this.render();
} catch (error) {
logger.error('ToolExecution', `Tool ${toolName} failed`, error);
this.store.notify(`${toolName} failed: ${error.message}`, 'error');
}
return;
}
// TODO: For tools with parameters, show a modal to collect params
this.store.notify(`${toolName} requires parameters. Parameter input UI coming soon.`, 'info');
}
filterTools(searchTerm, category) {
const cards = document.querySelectorAll('.tool-card');
const search = searchTerm.toLowerCase();
cards.forEach(card => {
const toolName = card.dataset.tool?.toLowerCase() || '';
const toolCategory = card.dataset.category || '';
const matchesSearch = !search || toolName.includes(search);
const matchesCategory = !category || toolCategory === category;
card.style.display = (matchesSearch && matchesCategory) ? 'block' : 'none';
});
}
investigateWin(winId) {
logger.info('QuickWins', `Investigating win: ${winId}`);
this.store.notify('Investigation feature coming soon', 'info');
}
markWinDone(winId) {
logger.info('QuickWins', `Marking win as done: ${winId}`);
this.store.notify('Win marked as done', 'success');
// TODO: Persist to backend
}
// === Team Dashboard Actions ===
async addFigmaFile(figmaData) {
const projectId = this.store.get('currentProject');
if (!projectId) {
this.store.notify('Please select a project first', 'warning');
return;
}
try {
const result = await DashboardService.addFigmaFile(projectId, figmaData);
this.store.notify('Figma file added successfully', 'success');
await this.loadDashboardData(projectId);
} catch (error) {
this.store.notify(`Failed to add Figma file: ${error.message}`, 'error');
}
}
async deleteFigmaFile(fileId) {
const projectId = this.store.get('currentProject');
if (!projectId) return;
try {
await DashboardService.deleteFigmaFile(projectId, fileId);
this.store.notify('Figma file deleted', 'success');
await this.loadDashboardData(projectId);
} catch (error) {
this.store.notify(`Failed to delete Figma file: ${error.message}`, 'error');
}
}
async syncFigmaFile(fileId) {
const projectId = this.store.get('currentProject');
if (!projectId) return;
try {
await DashboardService.updateFigmaFileSync(projectId, fileId, 'syncing');
this.store.notify('Syncing Figma file...', 'info');
// TODO: Implement actual sync logic
setTimeout(async () => {
await DashboardService.updateFigmaFileSync(projectId, fileId, 'success');
this.store.notify('Figma file synced successfully', 'success');
await this.loadDashboardData(projectId);
}, 2000);
} catch (error) {
this.store.notify(`Failed to sync Figma file: ${error.message}`, 'error');
}
}
async addESREDefinition(esreData) {
const projectId = this.store.get('currentProject');
if (!projectId) {
this.store.notify('Please select a project first', 'warning');
return;
}
try {
const result = await DashboardService.createESREDefinition(projectId, esreData);
this.store.notify('ESRE definition added successfully', 'success');
await this.loadDashboardData(projectId);
} catch (error) {
this.store.notify(`Failed to add ESRE definition: ${error.message}`, 'error');
}
}
// === Utilities ===
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
}
/**
* Update Storybook link with configured URL
* Called on init and when settings are saved
*/
updateStorybookLink() {
const link = document.getElementById('storybook-link');
if (!link) return;
try {
// Get Storybook URL from server config (loaded from /api/config)
const storybookUrl = getStorybookUrl();
link.href = storybookUrl;
link.style.pointerEvents = 'auto';
link.style.opacity = '1';
link.title = `Open Storybook at ${storybookUrl}`;
} catch (error) {
// Config not loaded yet or error occurred
link.style.pointerEvents = 'none';
link.style.opacity = '0.5';
link.removeAttribute('href');
link.title = 'Storybook URL not available';
logger.warn('App', 'Could not update Storybook link', { error: error.message });
}
}
}
// Global instance
const app = new App();
export default app;