admin-ui: align with API, add auth, fix integrations
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled

This commit is contained in:
DSS
2025-12-12 15:46:08 -03:00
parent ec09a0a662
commit d13e1cd76a
16 changed files with 1565 additions and 671 deletions

View File

@@ -23,7 +23,8 @@ class ApiClient {
data?: unknown, data?: unknown,
options?: RequestInit options?: RequestInit
): Promise<T> { ): Promise<T> {
const url = `${this.baseUrl}${path}`; // Most endpoints live under `/api/*`, but health is intentionally exposed at `/health`.
const url = path === '/health' ? path : `${this.baseUrl}${path}`;
// Deduplicate GET requests // Deduplicate GET requests
if (method === 'GET') { if (method === 'GET') {
@@ -58,6 +59,14 @@ class ApiClient {
...options?.headers ...options?.headers
}; };
// Attach auth token when present (headless server auth).
if (typeof window !== 'undefined') {
const token = localStorage.getItem('dss-token');
if (token) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
}
}
const config: RequestInit = { const config: RequestInit = {
method, method,
headers, headers,
@@ -124,35 +133,64 @@ export const api = new ApiClient();
// Import types // Import types
import type { import type {
Project, ProjectCreateData, ProjectConfig, AuthLoginRequest,
FigmaFile, FigmaExtractResult, FigmaHealthStatus, AuthLoginResponse,
UserProfile,
Project,
ProjectCreateData,
ProjectUpdateData,
ProjectConfig,
ProjectDashboardSummary,
FigmaFile,
FigmaFileCreateData,
FigmaHealthStatus,
FigmaExtractTokensResult,
FigmaExtractComponentsResult,
FigmaExtractStylesResult,
FigmaSyncTokensResult,
FigmaValidateResult,
FigmaGenerateCodeResult,
TokenDrift, TokenDrift,
TokenDriftListResponse,
Component, Component,
ESREDefinition, ESRECreateData, ESREDefinition,
AuditEntry, AuditStats, ESRECreateData,
SystemHealth, RuntimeConfig, AuditLogResponse,
DiscoveryResult, DiscoveryStats, AuditStats,
Team, TeamCreateData, RuntimeConfigEnvelope,
Integration, IntegrationCreateData, ConfigUpdateRequest,
ChatResponse, MCPTool, FigmaConfigStatus,
Service FigmaConnectionTestResult,
SystemHealth,
ServiceDiscoveryEnvelope,
MCPIntegrationsResponse,
ProjectIntegrationsResponse,
ChatResponse,
MCPTool,
MCPStatus
} from './types'; } from './types';
// Type-safe API endpoints - Mapped to actual DSS backend // Type-safe API endpoints - Mapped to actual DSS backend
export const endpoints = { export const endpoints = {
// Auth
auth: {
login: (data: AuthLoginRequest) => api.post<AuthLoginResponse>('/auth/login', data),
me: () => api.get<UserProfile>('/auth/me')
},
// Projects // Projects
projects: { projects: {
list: (status?: string) => list: (status?: string) =>
api.get<Project[]>(status ? `/projects?status=${status}` : '/projects'), api.get<Project[]>(status ? `/projects?status=${status}` : '/projects'),
get: (id: string) => api.get<Project>(`/projects/${id}`), get: (id: string) => api.get<Project>(`/projects/${id}`),
create: (data: ProjectCreateData) => api.post<Project>('/projects', data), create: (data: ProjectCreateData) => api.post<Project>('/projects', data),
update: (id: string, data: Partial<Project>) => api.put<Project>(`/projects/${id}`, data), update: (id: string, data: ProjectUpdateData) => api.put<Project>(`/projects/${id}`, data),
delete: (id: string) => api.delete<void>(`/projects/${id}`), delete: (id: string) => api.delete<void>(`/projects/${id}`),
config: (id: string) => api.get<ProjectConfig>(`/projects/${id}/config`), config: (id: string) => api.get<ProjectConfig>(`/projects/${id}/config`),
updateConfig: (id: string, data: ProjectConfig) => api.put<ProjectConfig>(`/projects/${id}/config`, data), updateConfig: (id: string, data: ProjectConfig) => api.put<ProjectConfig>(`/projects/${id}/config`, data),
context: (id: string) => api.get<unknown>(`/projects/${id}/context`), context: (id: string) => api.get<unknown>(`/projects/${id}/context`),
components: (id: string) => api.get<Component[]>(`/projects/${id}/components`), components: (id: string) => api.get<Component[]>(`/projects/${id}/components`),
dashboard: (id: string) => api.get<unknown>(`/projects/${id}/dashboard/summary`), dashboard: (id: string) => api.get<ProjectDashboardSummary>(`/projects/${id}/dashboard/summary`),
// Files (sandboxed) // Files (sandboxed)
files: (id: string, path?: string) => files: (id: string, path?: string) =>
api.get<unknown>(`/projects/${id}/files${path ? `?path=${encodeURIComponent(path)}` : ''}`), api.get<unknown>(`/projects/${id}/files${path ? `?path=${encodeURIComponent(path)}` : ''}`),
@@ -163,7 +201,7 @@ export const endpoints = {
api.post<void>(`/projects/${id}/files/write`, { path, content }), api.post<void>(`/projects/${id}/files/write`, { path, content }),
// Figma files per project // Figma files per project
figmaFiles: (id: string) => api.get<FigmaFile[]>(`/projects/${id}/figma-files`), figmaFiles: (id: string) => api.get<FigmaFile[]>(`/projects/${id}/figma-files`),
addFigmaFile: (id: string, data: { name: string; fileKey: string }) => addFigmaFile: (id: string, data: FigmaFileCreateData) =>
api.post<FigmaFile>(`/projects/${id}/figma-files`, data), api.post<FigmaFile>(`/projects/${id}/figma-files`, data),
syncFigmaFile: (id: string, fileId: string) => syncFigmaFile: (id: string, fileId: string) =>
api.put<FigmaFile>(`/projects/${id}/figma-files/${fileId}/sync`, {}), api.put<FigmaFile>(`/projects/${id}/figma-files/${fileId}/sync`, {}),
@@ -174,23 +212,26 @@ export const endpoints = {
// Figma Integration // Figma Integration
figma: { figma: {
health: () => api.get<FigmaHealthStatus>('/figma/health'), health: () => api.get<FigmaHealthStatus>('/figma/health'),
extractVariables: (fileKey: string, nodeId?: string) => extractVariables: (fileKey: string, format: string = 'css') =>
api.post<FigmaExtractResult>('/figma/extract-variables', { file_key: fileKey, node_id: nodeId }), api.post<FigmaExtractTokensResult>('/figma/extract-variables', { file_key: fileKey, format }),
extractComponents: (fileKey: string, nodeId?: string) => extractComponents: (fileKey: string) =>
api.post<FigmaExtractResult>('/figma/extract-components', { file_key: fileKey, node_id: nodeId }), api.post<FigmaExtractComponentsResult>('/figma/extract-components', { file_key: fileKey, format: 'json' }),
extractStyles: (fileKey: string, nodeId?: string) => extractStyles: (fileKey: string) =>
api.post<FigmaExtractResult>('/figma/extract-styles', { file_key: fileKey, node_id: nodeId }), api.post<FigmaExtractStylesResult>('/figma/extract-styles', { file_key: fileKey, format: 'json' }),
syncTokens: (fileKey: string, targetPath: string, format?: string) => syncTokens: (fileKey: string, targetPath: string, format?: string) =>
api.post<{ synced: number }>('/figma/sync-tokens', { file_key: fileKey, target_path: targetPath, format }), api.post<FigmaSyncTokensResult>('/figma/sync-tokens', { file_key: fileKey, target_path: targetPath, format }),
validate: (componentDef: unknown) => validateComponents: (fileKey: string) =>
api.post<{ valid: boolean; errors: string[] }>('/figma/validate', componentDef), api.post<FigmaValidateResult>('/figma/validate', { file_key: fileKey, format: 'json' }),
generateCode: (componentDef: unknown, framework?: string) => generateCode: (fileKey: string, componentName: string, framework: string = 'webcomponent') =>
api.post<{ code: string }>('/figma/generate-code', { component: componentDef, framework }) api.post<FigmaGenerateCodeResult>(
`/figma/generate-code?file_key=${encodeURIComponent(fileKey)}&component_name=${encodeURIComponent(componentName)}&framework=${encodeURIComponent(framework)}`,
{}
)
}, },
// Token Drift // Token Drift
tokens: { tokens: {
drift: (projectId: string) => api.get<TokenDrift[]>(`/projects/${projectId}/token-drift`), drift: (projectId: string) => api.get<TokenDriftListResponse>(`/projects/${projectId}/token-drift`),
recordDrift: (projectId: string, data: Partial<TokenDrift>) => recordDrift: (projectId: string, data: Partial<TokenDrift>) =>
api.post<TokenDrift>(`/projects/${projectId}/token-drift`, data), api.post<TokenDrift>(`/projects/${projectId}/token-drift`, data),
updateDriftStatus: (projectId: string, driftId: string, status: string) => updateDriftStatus: (projectId: string, driftId: string, status: string) =>
@@ -208,22 +249,11 @@ export const endpoints = {
api.delete<void>(`/projects/${projectId}/esre/${esreId}`) api.delete<void>(`/projects/${projectId}/esre/${esreId}`)
}, },
// Integrations per project
integrations: {
list: (projectId: string) => api.get<Integration[]>(`/projects/${projectId}/integrations`),
create: (projectId: string, data: IntegrationCreateData) =>
api.post<Integration>(`/projects/${projectId}/integrations`, data),
update: (projectId: string, type: string, data: Partial<Integration>) =>
api.put<Integration>(`/projects/${projectId}/integrations/${type}`, data),
delete: (projectId: string, type: string) =>
api.delete<void>(`/projects/${projectId}/integrations/${type}`)
},
// Discovery // Discovery
discovery: { discovery: {
run: () => api.get<DiscoveryResult>('/discovery'), run: () => api.get<unknown>('/discovery'),
scan: () => api.post<DiscoveryResult>('/discovery/scan', {}), scan: () => api.post<unknown>('/discovery/scan', {}),
stats: () => api.get<DiscoveryStats>('/discovery/stats'), stats: () => api.get<unknown>('/discovery/stats'),
activity: () => api.get<unknown[]>('/discovery/activity'), activity: () => api.get<unknown[]>('/discovery/activity'),
ports: () => api.get<unknown[]>('/discovery/ports'), ports: () => api.get<unknown[]>('/discovery/ports'),
env: () => api.get<unknown>('/discovery/env') env: () => api.get<unknown>('/discovery/env')
@@ -231,20 +261,20 @@ export const endpoints = {
// Teams // Teams
teams: { teams: {
list: () => api.get<Team[]>('/teams'), list: () => api.get<unknown[]>('/teams'),
get: (id: string) => api.get<Team>(`/teams/${id}`), get: (id: string) => api.get<unknown>(`/teams/${id}`),
create: (data: TeamCreateData) => api.post<Team>('/teams', data) create: (data: Record<string, unknown>) => api.post<unknown>('/teams', data)
}, },
// System & Config // System & Config
system: { system: {
health: () => api.get<SystemHealth>('/health'), health: () => api.get<SystemHealth>('/health'),
stats: () => api.get<unknown>('/stats'), stats: () => api.get<unknown>('/stats'),
config: () => api.get<RuntimeConfig>('/config'), config: () => api.get<RuntimeConfigEnvelope>('/config'),
updateConfig: (data: Partial<RuntimeConfig>) => api.put<RuntimeConfig>('/config', data), updateConfig: (data: ConfigUpdateRequest) => api.put<Record<string, unknown>>('/config', data),
figmaConfig: () => api.get<{ configured: boolean }>('/config/figma'), figmaConfig: () => api.get<FigmaConfigStatus>('/config/figma'),
testFigma: () => api.post<{ success: boolean; message: string }>('/config/figma/test', {}), testFigma: () => api.post<FigmaConnectionTestResult>('/config/figma/test', {}),
reset: () => api.post<void>('/system/reset', {}), reset: (confirm: 'RESET') => api.post<void>('/system/reset', { confirm }),
mode: () => api.get<{ mode: string }>('/mode'), mode: () => api.get<{ mode: string }>('/mode'),
setMode: (mode: string) => api.put<{ mode: string }>('/mode', { mode }) setMode: (mode: string) => api.put<{ mode: string }>('/mode', { mode })
}, },
@@ -252,15 +282,15 @@ export const endpoints = {
// Cache Management // Cache Management
cache: { cache: {
clear: () => api.post<{ cleared: number }>('/cache/clear', {}), clear: () => api.post<{ cleared: number }>('/cache/clear', {}),
purge: () => api.delete<{ purged: number }>('/cache') purge: () => api.delete<{ success: boolean }>('/cache')
}, },
// Services // Services
services: { services: {
list: () => api.get<Service[]>('/services'), list: () => api.get<ServiceDiscoveryEnvelope>('/services'),
configure: (name: string, config: unknown) => api.put<Service>(`/services/${name}`, config), configure: (name: string, config: unknown) => api.put<Record<string, unknown>>(`/services/${name}`, config),
storybook: () => api.get<{ running: boolean; url?: string }>('/services/storybook'), storybook: () => api.get<{ running: boolean; url?: string }>('/services/storybook'),
initStorybook: () => api.post<void>('/storybook/init', {}), initStorybook: (projectId?: string) => api.post<void>('/storybook/init', projectId ? { project_id: projectId } : {}),
clearStories: () => api.delete<void>('/storybook/stories') clearStories: () => api.delete<void>('/storybook/stories')
}, },
@@ -268,9 +298,9 @@ export const endpoints = {
audit: { audit: {
list: (params?: Record<string, string>) => { list: (params?: Record<string, string>) => {
const query = params ? '?' + new URLSearchParams(params).toString() : ''; const query = params ? '?' + new URLSearchParams(params).toString() : '';
return api.get<AuditEntry[]>(`/audit${query}`); return api.get<AuditLogResponse>(`/audit${query}`);
}, },
create: (entry: Partial<AuditEntry>) => api.post<AuditEntry>('/audit', entry), create: (entry: Record<string, unknown>) => api.post<{ success: boolean; message: string }>('/audit', entry),
stats: () => api.get<AuditStats>('/audit/stats'), stats: () => api.get<AuditStats>('/audit/stats'),
categories: () => api.get<string[]>('/audit/categories'), categories: () => api.get<string[]>('/audit/categories'),
actions: () => api.get<string[]>('/audit/actions'), actions: () => api.get<string[]>('/audit/actions'),
@@ -285,17 +315,59 @@ export const endpoints = {
// Claude AI Chat // Claude AI Chat
claude: { claude: {
chat: (message: string, projectId?: string, context?: unknown) => chat: (
api.post<ChatResponse>('/claude/chat', { message, project_id: projectId, context }) message: string,
projectId?: string,
context?: unknown,
options?: { user_id?: number; model?: 'claude' | 'gemini'; enable_tools?: boolean; history?: unknown[] }
) =>
api.post<ChatResponse>('/claude/chat', {
message,
project_id: projectId,
user_id: options?.user_id,
model: options?.model,
enable_tools: options?.enable_tools,
history: options?.history,
context
})
}, },
// MCP Tools // MCP Tools
mcp: { mcp: {
integrations: () => api.get<string[]>('/mcp/integrations'), integrations: () => api.get<MCPIntegrationsResponse>('/mcp/integrations'),
projectIntegrations: (projectId: string, userId?: number) =>
api.get<ProjectIntegrationsResponse>(
`/projects/${projectId}/integrations${typeof userId === 'number' ? `?user_id=${userId}` : ''}`
),
upsertProjectIntegration: (
projectId: string,
userId: number,
integration_type: string,
config: Record<string, unknown>,
enabled: boolean = true
) =>
api.post<Record<string, unknown>>(
`/projects/${projectId}/integrations?user_id=${userId}`,
{ integration_type, config, enabled }
),
updateProjectIntegration: (
projectId: string,
userId: number,
integration_type: string,
update: { config?: Record<string, unknown>; enabled?: boolean }
) =>
api.put<Record<string, unknown>>(
`/projects/${projectId}/integrations/${integration_type}?user_id=${userId}`,
update
),
deleteProjectIntegration: (projectId: string, userId: number, integration_type: string) =>
api.delete<Record<string, unknown>>(
`/projects/${projectId}/integrations/${integration_type}?user_id=${userId}`
),
tools: () => api.get<MCPTool[]>('/mcp/tools'), tools: () => api.get<MCPTool[]>('/mcp/tools'),
tool: (name: string) => api.get<MCPTool>(`/mcp/tools/${name}`), tool: (name: string) => api.get<MCPTool>(`/mcp/tools/${name}`),
execute: (name: string, params: unknown) => api.post<unknown>(`/mcp/tools/${name}/execute`, params), execute: (name: string, params: unknown) => api.post<unknown>(`/mcp/tools/${name}/execute`, params),
status: () => api.get<{ connected: boolean; tools: number }>('/mcp/status') status: () => api.get<MCPStatus>('/mcp/status')
}, },
// Browser Logs // Browser Logs
@@ -307,7 +379,7 @@ export const endpoints = {
// Debug // Debug
debug: { debug: {
diagnostic: () => api.get<unknown>('/debug/diagnostic'), diagnostic: () => api.get<unknown>('/debug/diagnostic'),
workflows: () => api.get<unknown[]>('/debug/workflows') workflows: () => api.get<Record<string, unknown>>('/debug/workflows')
}, },
// Design System Ingestion // Design System Ingestion

View File

@@ -1,4 +1,39 @@
// DSS Admin UI - API Types // DSS Admin UI - API Types
//
// These types match the headless server in `apps/api/server.py`.
// ============ Auth ============
export type AtlassianService = 'jira' | 'confluence';
export interface AuthLoginRequest {
url: string;
email: string;
api_token: string;
service?: AtlassianService;
}
export interface AuthLoginResponse {
token: string;
user: {
id: number;
email: string;
display_name: string;
atlassian_url?: string;
service?: AtlassianService;
};
expires_at: string;
}
export interface UserProfile {
id: number;
email: string;
display_name: string;
atlassian_url?: string;
atlassian_service?: AtlassianService;
created_at?: string;
last_login?: string;
}
// ============ Projects ============ // ============ Projects ============
@@ -6,127 +41,201 @@ export interface Project {
id: string; id: string;
name: string; name: string;
description?: string; description?: string;
path: string; figma_file_key?: string;
status: 'active' | 'archived' | 'draft'; root_path?: string;
status: 'active' | 'archived' | 'draft' | string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
components_count?: number;
tokens_count?: number;
} }
export interface ProjectCreateData { export interface ProjectCreateData {
name: string; name: string;
description?: string; description?: string;
path: string; figma_file_key?: string;
root_path?: string;
} }
export interface ProjectUpdateData {
name?: string;
description?: string;
figma_file_key?: string;
root_path?: string;
status?: string;
}
// `.dss/config.json` (managed by `ConfigService`)
export interface ProjectConfig { export interface ProjectConfig {
schema_version?: string;
figma?: { figma?: {
file_key?: string; file_id?: string | null;
access_token?: string; team_id?: string | null;
};
storybook?: {
url?: string;
config_path?: string;
}; };
tokens?: { tokens?: {
source?: string; output_path?: string;
output?: string; format?: 'css' | 'scss' | 'json' | 'js' | string;
format?: 'css' | 'scss' | 'json' | 'js';
}; };
components?: { ai?: {
source_dir?: string; allowed_operations?: string[];
patterns?: string[]; context_files?: string[];
max_file_size_kb?: number;
};
[key: string]: unknown;
}
export interface ProjectDashboardSummary {
project_id: string;
ux: {
figma_files_count: number;
figma_files: unknown[];
};
ui: {
token_drift: TokenDriftStats;
code_metrics: unknown;
};
qa: {
esre_count: number;
test_summary: unknown;
}; };
} }
// ============ Figma ============ // ============ Figma ============
export interface FigmaHealthStatus {
status: 'ok' | 'degraded' | string;
mode: 'live' | 'mock' | string;
message: string;
}
export interface FigmaToken {
name: string;
value: unknown;
type: string;
description?: string;
category?: string;
}
export interface FigmaExtractTokensResult {
success: boolean;
tokens_count: number;
collections: string[];
output_path: string;
tokens: FigmaToken[];
formatted_output: string;
}
export interface FigmaComponentDefinition {
name: string;
key: string;
description: string;
properties: Record<string, unknown>;
variants: unknown[];
}
export interface FigmaExtractComponentsResult {
success: boolean;
components_count: number;
output_path?: string;
components: FigmaComponentDefinition[];
error?: string;
}
export interface FigmaStyleDefinition {
name: string;
key: string;
type: string;
properties: Record<string, unknown>;
}
export interface FigmaExtractStylesResult {
success: boolean;
styles_count: number;
output_path?: string;
styles: FigmaStyleDefinition[];
error?: string;
}
export interface FigmaSyncTokensResult {
success: boolean;
has_changes: boolean;
tokens_synced: number;
target_path: string;
backup_created: boolean;
}
export interface FigmaValidateResult {
success: boolean;
valid: boolean;
components_checked: number;
issues: Array<Record<string, unknown>>;
summary: { errors: number; warnings: number; info: number };
}
export interface FigmaGenerateCodeResult {
success: boolean;
component: string;
framework: string;
output_path?: string;
code: string;
}
// Per-project saved Figma files (UX dashboard)
export interface FigmaFile { export interface FigmaFile {
id: string; id: string;
project_id: string; project_id: string;
name: string; figma_url: string;
file_name: string;
file_key: string; file_key: string;
status: 'synced' | 'pending' | 'error'; sync_status: 'pending' | 'synced' | 'error' | string;
last_synced?: string; last_synced?: string | null;
created_at: string;
error_message?: string; error_message?: string;
created_at: string;
} }
export interface FigmaHealthStatus { export interface FigmaFileCreateData {
connected: boolean; figma_url: string;
token_configured: boolean; file_name: string;
last_request?: string; file_key: string;
rate_limit?: {
remaining: number;
reset_at: string;
};
} }
export interface FigmaExtractResult { // ============ Components (stored) ============
success: boolean;
count: number;
items: FigmaExtractedItem[];
warnings?: string[];
errors?: string[];
}
export interface FigmaExtractedItem {
name: string;
type: string;
value: unknown;
description?: string;
metadata?: Record<string, unknown>;
}
// ============ Tokens ============
export interface Token {
name: string;
value: string;
type: 'color' | 'spacing' | 'typography' | 'radius' | 'shadow' | 'other';
category?: string;
description?: string;
source?: 'figma' | 'code' | 'manual';
}
export interface TokenDrift {
id: string;
project_id: string;
token_name: string;
expected_value: string;
actual_value: string;
file_path: string;
line_number?: number;
severity: 'low' | 'medium' | 'high' | 'critical';
status: 'open' | 'resolved' | 'ignored';
created_at: string;
resolved_at?: string;
}
// ============ Components ============
export interface Component { export interface Component {
id: string; id: string;
project_id: string;
name: string; name: string;
display_name?: string; figma_key?: string;
description?: string; description?: string;
file_path: string; properties?: Record<string, unknown>;
type: 'react' | 'vue' | 'web-component' | 'other'; variants?: unknown[];
props?: ComponentProp[]; code_generated?: boolean;
variants?: string[];
has_story?: boolean;
figma_node_id?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export interface ComponentProp { // ============ Tokens / Drift ============
name: string;
type: string; export interface TokenDrift {
required: boolean; id: string;
default_value?: unknown; component_id: string;
description?: string; property_name: string;
hardcoded_value: string;
suggested_token?: string | null;
severity: 'info' | 'warning' | 'critical' | string;
file_path: string;
line_number: number;
status: 'pending' | 'fixed' | 'ignored' | string;
detected_at: string;
}
export interface TokenDriftStats {
total: number;
by_status: Record<string, number>;
by_severity: Record<string, number>;
}
export interface TokenDriftListResponse {
drifts: TokenDrift[];
stats: TokenDriftStats;
} }
// ============ ESRE (QA) ============ // ============ ESRE (QA) ============
@@ -135,154 +244,137 @@ export interface ESREDefinition {
id: string; id: string;
project_id: string; project_id: string;
name: string; name: string;
component_name: string; definition_text: string;
description?: string; expected_value?: string | null;
expected_value: string; component_name?: string | null;
selector?: string;
css_property?: string;
status: 'active' | 'inactive';
last_result?: 'pass' | 'fail' | 'skip';
last_tested?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export interface ESRECreateData { export interface ESRECreateData {
name: string; name: string;
component_name: string; definition_text: string;
description?: string; expected_value?: string | null;
expected_value: string; component_name?: string | null;
selector?: string;
css_property?: string;
} }
// ============ Audit ============ // ============ Activity / Audit ============
export interface AuditEntry { export interface AuditEntry {
id: string; id: string;
timestamp: string; timestamp: string;
category: string;
action: string; action: string;
user?: string; category?: string;
severity?: string;
description?: string;
entity_type?: string;
entity_id?: string;
entity_name?: string;
project_id?: string; project_id?: string;
details?: Record<string, unknown>; user_id?: string;
metadata?: Record<string, unknown>; user_name?: string;
team_context?: string;
details?: Record<string, unknown> | null;
ip_address?: string;
user_agent?: string;
}
export interface AuditLogResponse {
activities: AuditEntry[];
total: number;
limit: number;
offset: number;
has_more: boolean;
} }
export interface AuditStats { export interface AuditStats {
total_entries: number;
by_category: Record<string, number>; by_category: Record<string, number>;
by_action: Record<string, number>; by_user: Record<string, number>;
recent_activity: number; total_count: number;
} }
// ============ System ============ // ============ System / Runtime Config ============
export interface RuntimeConfigEnvelope {
config: Record<string, unknown>;
env: Record<string, unknown>;
mode?: string;
}
export interface ConfigUpdateRequest {
mode?: 'local' | 'server';
figma_token?: string;
services?: Record<string, unknown>;
features?: Record<string, boolean>;
}
export interface FigmaConfigStatus {
configured: boolean;
mode: string;
features: Record<string, boolean>;
}
export interface FigmaConnectionTestResult {
success: boolean;
user?: string;
handle?: string;
error?: string;
}
export interface SystemResetRequest {
confirm: 'RESET';
}
export interface SystemHealth { export interface SystemHealth {
status: 'healthy' | 'degraded' | 'unhealthy'; status: 'healthy' | 'degraded' | 'unhealthy' | string;
version?: string; uptime_seconds: number;
uptime?: number; version: string;
timestamp: string;
services: { services: {
storage: 'up' | 'down'; storage: string;
mcp: 'up' | 'down'; mcp: string;
figma: 'up' | 'down' | 'not_configured'; figma: string;
}; };
timestamp: string;
} }
export interface RuntimeConfig { export interface MCPStatus {
server_host: string; connected: boolean;
server_port: number; tools: number;
figma_token?: string; details?: Record<string, unknown>;
storybook_url?: string;
data_dir?: string;
log_level?: 'debug' | 'info' | 'warning' | 'error';
} }
export interface CacheStatus { export interface ServiceDiscoveryEnvelope {
size: number; configured: Record<string, unknown>;
entries: number; discovered: Record<string, unknown>;
hit_rate: number; storybook: { running: boolean; url?: string };
last_cleared?: string;
} }
// ============ Discovery ============ // ============ MCP Integrations ============
export interface DiscoveryResult { export interface MCPIntegrationHealth {
projects: DiscoveredProject[]; integration_type: string;
services: DiscoveredService[]; is_healthy: boolean;
timestamp: string; failure_count: number;
} }
export interface DiscoveredProject { export interface MCPIntegrationsResponse {
path: string; integrations: MCPIntegrationHealth[];
name: string;
type: string;
has_dss_config: boolean;
framework?: string;
} }
export interface DiscoveredService { export interface ProjectIntegrationRecord {
name: string;
port: number;
type: string;
url?: string;
status: 'running' | 'stopped';
}
export interface DiscoveryStats {
projects_discovered: number;
services_running: number;
last_scan: string;
}
// ============ Teams ============
export interface Team {
id: string; id: string;
name: string;
description?: string;
members?: TeamMember[];
created_at: string;
}
export interface TeamMember {
id: string;
name: string;
email: string;
role: 'admin' | 'member' | 'viewer';
}
export interface TeamCreateData {
name: string;
description?: string;
}
// ============ Integrations ============
export interface Integration {
type: 'figma' | 'jira' | 'confluence' | 'github' | 'gitlab' | 'slack';
project_id: string; project_id: string;
status: 'connected' | 'disconnected' | 'error'; user_id: number;
config: Record<string, unknown>; integration_type: string;
last_sync?: string; config: string;
error_message?: string; enabled: boolean;
created_at: string;
updated_at: string;
last_used_at?: string | null;
} }
export interface IntegrationCreateData { export interface ProjectIntegrationsResponse {
type: Integration['type']; integrations: ProjectIntegrationRecord[];
config: Record<string, unknown>;
}
// ============ Services ============
export interface Service {
name: string;
type: string;
url?: string;
port?: number;
status: 'running' | 'stopped' | 'unknown';
config?: Record<string, unknown>;
} }
// ============ Chat / Claude ============ // ============ Chat / Claude ============

View File

@@ -33,6 +33,22 @@
gap: var(--spacing-1); gap: var(--spacing-1);
} }
.chat-model-select {
appearance: none;
background-color: var(--color-surface-1);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--spacing-1) var(--spacing-2);
font-size: var(--font-size-xs);
color: var(--color-foreground);
cursor: pointer;
}
.chat-model-select:focus {
outline: none;
border-color: var(--color-ring);
}
/* Quick Actions */ /* Quick Actions */
.chat-quick-actions { .chat-quick-actions {
display: flex; display: flex;

View File

@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'preact/hooks';
import { chatSidebarOpen } from '../../state/app'; import { chatSidebarOpen } from '../../state/app';
import { currentProject } from '../../state/project'; import { currentProject } from '../../state/project';
import { activeTeam } from '../../state/team'; import { activeTeam } from '../../state/team';
import { userId as authenticatedUserId } from '../../state/user';
import { endpoints } from '../../api/client'; import { endpoints } from '../../api/client';
import { Button } from '../base/Button'; import { Button } from '../base/Button';
import { Badge } from '../base/Badge'; import { Badge } from '../base/Badge';
@@ -30,6 +31,7 @@ export function ChatSidebar() {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [mcpTools, setMcpTools] = useState<string[]>([]); const [mcpTools, setMcpTools] = useState<string[]>([]);
const [model, setModel] = useState<'claude' | 'gemini'>('claude');
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
@@ -74,7 +76,9 @@ export function ChatSidebar() {
project: currentProject.value ? { project: currentProject.value ? {
id: currentProject.value.id, id: currentProject.value.id,
name: currentProject.value.name, name: currentProject.value.name,
path: currentProject.value.path root_path: currentProject.value.root_path,
// Backwards compatible field for older tool prompts.
path: currentProject.value.root_path
} : null, } : null,
currentView: window.location.hash currentView: window.location.hash
}; };
@@ -82,7 +86,8 @@ export function ChatSidebar() {
const response = await endpoints.claude.chat( const response = await endpoints.claude.chat(
userMessage.content, userMessage.content,
currentProject.value?.id, currentProject.value?.id,
context context,
{ user_id: authenticatedUserId.value, model }
); );
const assistantMessage: Message = { const assistantMessage: Message = {
@@ -146,6 +151,15 @@ export function ChatSidebar() {
<Badge variant="success" size="sm">{mcpTools.length} tools</Badge> <Badge variant="success" size="sm">{mcpTools.length} tools</Badge>
)} )}
</div> </div>
<select
className="chat-model-select"
value={model}
onChange={(e) => setModel((e.target as HTMLSelectElement).value as 'claude' | 'gemini')}
aria-label="AI model"
>
<option value="claude">Claude</option>
<option value="gemini">Gemini</option>
</select>
<div className="chat-header-actions"> <div className="chat-header-actions">
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -33,6 +33,16 @@
justify-content: flex-end; justify-content: flex-end;
} }
/* User */
.header-user {
font-size: var(--font-size-sm);
color: var(--color-muted-foreground);
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Logo */ /* Logo */
.header-logo { .header-logo {
display: flex; display: flex;

View File

@@ -8,7 +8,12 @@ import {
} from '../../state/app'; } from '../../state/app';
import { activeTeam, setActiveTeam, TEAM_CONFIGS, TeamId } from '../../state/team'; import { activeTeam, setActiveTeam, TEAM_CONFIGS, TeamId } from '../../state/team';
import { currentProject, projects, setCurrentProject } from '../../state/project'; import { currentProject, projects, setCurrentProject } from '../../state/project';
import { isAuthenticated, loginWithAtlassian, logout, user } from '../../state/user';
import { Button } from '../base/Button'; import { Button } from '../base/Button';
import { Badge } from '../base/Badge';
import { Input, Select } from '../base/Input';
import { Modal } from '../shared/Modal';
import { useState } from 'preact/hooks';
import './Header.css'; import './Header.css';
// Icons as inline SVG // Icons as inline SVG
@@ -48,6 +53,15 @@ const MoonIcon = () => (
export function Header() { export function Header() {
const teamKeys = Object.keys(TEAM_CONFIGS) as TeamId[]; const teamKeys = Object.keys(TEAM_CONFIGS) as TeamId[];
const [loginOpen, setLoginOpen] = useState(false);
const [loggingIn, setLoggingIn] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
const [credentials, setCredentials] = useState({
url: '',
email: '',
api_token: '',
service: 'jira' as 'jira' | 'confluence'
});
const cycleTheme = () => { const cycleTheme = () => {
const themes = ['light', 'dark', 'auto'] as const; const themes = ['light', 'dark', 'auto'] as const;
@@ -56,6 +70,20 @@ export function Header() {
setTheme(themes[nextIndex]); setTheme(themes[nextIndex]);
}; };
async function handleLogin() {
setLoggingIn(true);
setLoginError(null);
try {
await loginWithAtlassian(credentials);
setLoginOpen(false);
setCredentials({ url: '', email: '', api_token: '', service: 'jira' });
} catch (err) {
setLoginError(err instanceof Error ? err.message : 'Login failed');
} finally {
setLoggingIn(false);
}
}
return ( return (
<header className="header"> <header className="header">
<div className="header-left"> <div className="header-left">
@@ -105,6 +133,18 @@ export function Header() {
</div> </div>
<div className="header-right"> <div className="header-right">
{/* Auth */}
{isAuthenticated.value ? (
<>
<span className="header-user">
{user.value?.display_name || user.value?.email || 'User'}
</span>
<Button variant="ghost" size="sm" onClick={logout}>Logout</Button>
</>
) : (
<Button variant="ghost" size="sm" onClick={() => setLoginOpen(true)}>Login</Button>
)}
{/* Theme Toggle */} {/* Theme Toggle */}
<Button <Button
variant="ghost" variant="ghost"
@@ -125,6 +165,66 @@ export function Header() {
AI AI
</Button> </Button>
</div> </div>
<Modal
isOpen={loginOpen}
onClose={() => { setLoginOpen(false); setLoginError(null); }}
title="Login"
size="md"
footer={
<>
<Button variant="ghost" onClick={() => { setLoginOpen(false); setLoginError(null); }} disabled={loggingIn}>
Cancel
</Button>
<Button variant="primary" onClick={handleLogin} loading={loggingIn}>
Login
</Button>
</>
}
>
<div className="settings-form">
{loginError && (
<div className="form-error">
<Badge variant="error">{loginError}</Badge>
</div>
)}
<Input
label="Atlassian URL"
value={credentials.url}
onChange={(e) => setCredentials(c => ({ ...c, url: (e.target as HTMLInputElement).value }))}
placeholder="https://your-domain.atlassian.net"
fullWidth
/>
<Select
label="Service"
value={credentials.service}
onChange={(e) => setCredentials(c => ({ ...c, service: (e.target as HTMLSelectElement).value as 'jira' | 'confluence' }))}
options={[
{ value: 'jira', label: 'Jira' },
{ value: 'confluence', label: 'Confluence' }
]}
/>
<Input
label="Email"
type="email"
value={credentials.email}
onChange={(e) => setCredentials(c => ({ ...c, email: (e.target as HTMLInputElement).value }))}
placeholder="you@company.com"
fullWidth
/>
<Input
label="API Token"
type="password"
value={credentials.api_token}
onChange={(e) => setCredentials(c => ({ ...c, api_token: (e.target as HTMLInputElement).value }))}
placeholder="••••••••••••"
fullWidth
/>
<p className="text-muted" style={{ margin: 0 }}>
This creates a DSS user and stores a session token in your browser.
</p>
</div>
</Modal>
</header> </header>
); );
} }

View File

@@ -1,5 +1,6 @@
import { lazy, Suspense } from 'preact/compat'; import { lazy, Suspense } from 'preact/compat';
import { Spinner } from '../base/Spinner'; import { Spinner } from '../base/Spinner';
import { activeTeam, TEAM_CONFIGS, TeamId } from '../../state/team';
import './Stage.css'; import './Stage.css';
// Lazy load workdesks // Lazy load workdesks
@@ -12,6 +13,13 @@ interface StageProps {
activeTool: string | null; activeTool: string | null;
} }
const WORKDESK_BY_TEAM: Record<TeamId, any> = {
ui: UIWorkdesk,
ux: UXWorkdesk,
qa: QAWorkdesk,
admin: AdminWorkdesk
};
// Loading fallback // Loading fallback
function StageLoader() { function StageLoader() {
return ( return (
@@ -22,55 +30,31 @@ function StageLoader() {
); );
} }
// Tool component mapping function findOwnerTeam(toolId: string): TeamId {
function getToolComponent(toolId: string | null) { if (toolId === 'dashboard') return activeTeam.value;
// For now, return placeholder based on tool category
// Later we'll add specific tool components
switch (toolId) { const preferred = activeTeam.value;
case 'dashboard': if (TEAM_CONFIGS[preferred].tools.some(t => t.id === toolId)) return preferred;
case 'figma-extraction':
case 'figma-components':
case 'storybook-figma-compare':
case 'storybook-live-compare':
case 'project-analysis':
case 'quick-wins':
case 'regression-testing':
case 'code-generator':
return <UIWorkdesk activeTool={toolId} />;
case 'figma-plugin': const teams = Object.keys(TEAM_CONFIGS) as TeamId[];
case 'token-list': for (const team of teams) {
case 'asset-list': if (TEAM_CONFIGS[team].tools.some(t => t.id === toolId)) return team;
case 'component-list':
case 'navigation-demos':
return <UXWorkdesk activeTool={toolId} />;
case 'figma-live-compare':
case 'esre-editor':
case 'console-viewer':
case 'network-monitor':
case 'error-tracker':
return <QAWorkdesk activeTool={toolId} />;
case 'settings':
case 'projects':
case 'integrations':
case 'audit-log':
case 'cache-management':
case 'health-monitor':
return <AdminWorkdesk activeTool={toolId} />;
default:
return <UIWorkdesk activeTool="dashboard" />;
} }
return preferred;
} }
export function Stage({ activeTool }: StageProps) { export function Stage({ activeTool }: StageProps) {
const rawToolId = activeTool || 'dashboard';
const parts = rawToolId.split('/').filter(Boolean);
const toolId = (parts.length >= 2 && (parts[0] as TeamId) in TEAM_CONFIGS) ? parts[1] : rawToolId;
const team = findOwnerTeam(toolId);
const Workdesk = WORKDESK_BY_TEAM[team];
return ( return (
<main className="stage" role="main"> <main className="stage" role="main">
<Suspense fallback={<StageLoader />}> <Suspense fallback={<StageLoader />}>
{getToolComponent(activeTool)} <Workdesk activeTool={toolId} />
</Suspense> </Suspense>
</main> </main>
); );

View File

@@ -7,7 +7,7 @@ export * from './team';
export * from './user'; export * from './user';
import { loadProjects } from './project'; import { loadProjects } from './project';
import { loadUserPreferences } from './user'; import { loadUserPreferences, refreshUser } from './user';
/** /**
* Initialize the application state * Initialize the application state
@@ -17,6 +17,9 @@ export async function initializeApp(): Promise<void> {
// Load user preferences from localStorage // Load user preferences from localStorage
loadUserPreferences(); loadUserPreferences();
// Restore session if token exists
await refreshUser();
// Load projects from API // Load projects from API
await loadProjects(); await loadProjects();
} }

View File

@@ -1,30 +1,11 @@
import { signal, computed } from '@preact/signals'; import { signal, computed } from '@preact/signals';
import { api } from '../api/client'; import { endpoints } from '../api/client';
import type { Project, ProjectCreateData, ProjectDashboardSummary, ProjectUpdateData } from '../api/types';
// Types
export interface Project {
id: string;
name: string;
description?: string;
path?: string;
figmaFileKey?: string;
storybookUrl?: string;
createdAt: string;
updatedAt: string;
status: 'active' | 'archived' | 'draft';
}
export interface ProjectStats {
components: number;
tokens: number;
figmaFiles: number;
healthScore: number;
}
// Project State Signals // Project State Signals
export const projects = signal<Project[]>([]); export const projects = signal<Project[]>([]);
export const currentProjectId = signal<string | null>(null); export const currentProjectId = signal<string | null>(null);
export const projectStats = signal<ProjectStats | null>(null); export const projectDashboard = signal<ProjectDashboardSummary | null>(null);
export const projectsLoading = signal(false); export const projectsLoading = signal(false);
export const projectsError = signal<string | null>(null); export const projectsError = signal<string | null>(null);
@@ -45,7 +26,7 @@ export async function loadProjects(): Promise<void> {
projectsError.value = null; projectsError.value = null;
try { try {
const data = await api.get<Project[]>('/projects'); const data = await endpoints.projects.list();
projects.value = data; projects.value = data;
// Set first project as current if none selected // Set first project as current if none selected
@@ -64,34 +45,34 @@ export async function setCurrentProject(projectId: string): Promise<void> {
currentProjectId.value = projectId; currentProjectId.value = projectId;
localStorage.setItem('dss-current-project', projectId); localStorage.setItem('dss-current-project', projectId);
// Load project stats // Load dashboard summary (optional)
await loadProjectStats(projectId); await loadProjectDashboard(projectId);
} }
export async function loadProjectStats(projectId: string): Promise<void> { export async function loadProjectDashboard(projectId: string): Promise<void> {
try { try {
const stats = await api.get<ProjectStats>(`/projects/${projectId}/stats`); const summary = await endpoints.projects.dashboard(projectId);
projectStats.value = stats; projectDashboard.value = summary;
} catch (error) { } catch (error) {
console.error('Failed to load project stats:', error); console.error('Failed to load project dashboard:', error);
projectStats.value = null; projectDashboard.value = null;
} }
} }
export async function createProject(data: Partial<Project>): Promise<Project> { export async function createProject(data: ProjectCreateData): Promise<Project> {
const project = await api.post<Project>('/projects', data); const project = await endpoints.projects.create(data);
projects.value = [...projects.value, project]; projects.value = [...projects.value, project];
return project; return project;
} }
export async function updateProject(projectId: string, data: Partial<Project>): Promise<Project> { export async function updateProject(projectId: string, data: ProjectUpdateData): Promise<Project> {
const updated = await api.put<Project>(`/projects/${projectId}`, data); const updated = await endpoints.projects.update(projectId, data);
projects.value = projects.value.map(p => p.id === projectId ? updated : p); projects.value = projects.value.map(p => p.id === projectId ? updated : p);
return updated; return updated;
} }
export async function deleteProject(projectId: string): Promise<void> { export async function deleteProject(projectId: string): Promise<void> {
await api.delete(`/projects/${projectId}`); await endpoints.projects.delete(projectId);
projects.value = projects.value.filter(p => p.id !== projectId); projects.value = projects.value.filter(p => p.id !== projectId);
// Clear current if deleted // Clear current if deleted

View File

@@ -1,4 +1,6 @@
import { signal, computed } from '@preact/signals'; import { signal, computed } from '@preact/signals';
import { endpoints } from '../api/client';
import type { AuthLoginRequest, AuthLoginResponse, UserProfile } from '../api/types';
// Types // Types
export interface UserPreferences { export interface UserPreferences {
@@ -10,14 +12,6 @@ export interface UserPreferences {
notificationsEnabled: boolean; notificationsEnabled: boolean;
} }
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'developer' | 'viewer';
}
// Default preferences // Default preferences
const DEFAULT_PREFERENCES: UserPreferences = { const DEFAULT_PREFERENCES: UserPreferences = {
theme: 'auto', theme: 'auto',
@@ -29,16 +23,16 @@ const DEFAULT_PREFERENCES: UserPreferences = {
}; };
// User State Signals // User State Signals
export const user = signal<User | null>(null); export const user = signal<UserProfile | null>(null);
export const preferences = signal<UserPreferences>(DEFAULT_PREFERENCES); export const preferences = signal<UserPreferences>(DEFAULT_PREFERENCES);
// Computed Values // Computed Values
export const isAuthenticated = computed(() => user.value !== null); export const isAuthenticated = computed(() => user.value !== null);
export const isAdmin = computed(() => user.value?.role === 'admin'); export const userId = computed(() => user.value?.id ?? 1);
export const userName = computed(() => user.value?.name ?? 'Guest'); export const userName = computed(() => user.value?.display_name ?? user.value?.email ?? 'Guest');
export const userInitials = computed(() => { export const userInitials = computed(() => {
const name = user.value?.name ?? 'Guest'; const name = user.value?.display_name ?? user.value?.email ?? 'Guest';
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); return name.split(' ').filter(Boolean).map(n => n[0]).join('').toUpperCase().slice(0, 2);
}); });
// Actions // Actions
@@ -66,7 +60,7 @@ export function updatePreferences(updates: Partial<UserPreferences>): void {
saveUserPreferences(); saveUserPreferences();
} }
export function setUser(userData: User | null): void { export function setUser(userData: UserProfile | null): void {
user.value = userData; user.value = userData;
} }
@@ -75,6 +69,40 @@ export function logout(): void {
localStorage.removeItem('dss-token'); localStorage.removeItem('dss-token');
} }
export async function refreshUser(): Promise<void> {
if (typeof window === 'undefined') return;
const token = localStorage.getItem('dss-token');
if (!token) {
user.value = null;
return;
}
try {
const profile = await endpoints.auth.me();
user.value = profile;
} catch {
// Token is invalid/expired; clear it.
localStorage.removeItem('dss-token');
user.value = null;
}
}
export async function loginWithAtlassian(credentials: AuthLoginRequest): Promise<AuthLoginResponse> {
const response = await endpoints.auth.login(credentials);
localStorage.setItem('dss-token', response.token);
// Prefer server truth; also supports older login response shapes.
user.value = {
id: response.user.id,
email: response.user.email,
display_name: response.user.display_name,
atlassian_url: response.user.atlassian_url,
atlassian_service: response.user.service,
last_login: new Date().toISOString(),
};
return response;
}
// Keyboard shortcuts state // Keyboard shortcuts state
export const keyboardShortcutsEnabled = computed(() => export const keyboardShortcutsEnabled = computed(() =>
preferences.value.keyboardShortcutsEnabled preferences.value.keyboardShortcutsEnabled

File diff suppressed because it is too large Load Diff

View File

@@ -63,24 +63,22 @@ function QADashboard() {
if (esreResult.status === 'fulfilled') { if (esreResult.status === 'fulfilled') {
const esres = esreResult.value; const esres = esreResult.value;
const passed = esres.filter(e => e.last_result === 'pass').length;
const total = esres.length; const total = esres.length;
const healthScore = total > 0 ? Math.round((passed / total) * 100) : 100;
setMetrics({ setMetrics({
healthScore, healthScore: 100,
esreDefinitions: total, esreDefinitions: total,
testsRun: total, testsRun: 0,
testsPassed: passed testsPassed: 0
}); });
} }
if (auditResult.status === 'fulfilled') { if (auditResult.status === 'fulfilled') {
const audits = auditResult.value as AuditEntry[]; const audits = auditResult.value.activities;
setRecentTests(audits.slice(0, 5).map((entry, idx) => ({ setRecentTests(audits.slice(0, 5).map((entry: AuditEntry, idx: number) => ({
id: idx, id: idx,
name: entry.action || 'Test', name: entry.action || 'Test',
status: entry.details?.status as string || 'info', status: (entry.details?.status as string) || entry.severity || 'info',
time: formatTimeAgo(entry.timestamp) time: formatTimeAgo(entry.timestamp)
}))); })));
} }
@@ -240,11 +238,9 @@ function ESREEditorTool() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState<ESRECreateData>({ const [formData, setFormData] = useState<ESRECreateData>({
name: '', name: '',
component_name: '', definition_text: '',
description: '',
expected_value: '', expected_value: '',
selector: '', component_name: ''
css_property: ''
}); });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -276,8 +272,8 @@ function ESREEditorTool() {
return; return;
} }
if (!formData.name || !formData.component_name || !formData.expected_value) { if (!formData.name || !formData.definition_text) {
setError('Name, Component, and Expected Value are required'); setError('Name and Definition Text are required');
return; return;
} }
@@ -285,14 +281,17 @@ function ESREEditorTool() {
setError(null); setError(null);
try { try {
await endpoints.esre.create(projectId, formData); await endpoints.esre.create(projectId, {
name: formData.name,
definition_text: formData.definition_text,
expected_value: formData.expected_value ? formData.expected_value : undefined,
component_name: formData.component_name ? formData.component_name : undefined
});
setFormData({ setFormData({
name: '', name: '',
component_name: '', definition_text: '',
description: '',
expected_value: '', expected_value: '',
selector: '', component_name: ''
css_property: ''
}); });
loadESRE(); loadESRE();
} catch (err) { } catch (err) {
@@ -348,39 +347,25 @@ function ESREEditorTool() {
/> />
<Input <Input
label="Component Name" label="Component Name"
value={formData.component_name} value={formData.component_name || ''}
onChange={(e) => setFormData(d => ({ ...d, component_name: (e.target as HTMLInputElement).value }))} onChange={(e) => setFormData(d => ({ ...d, component_name: (e.target as HTMLInputElement).value }))}
placeholder="e.g., Button" placeholder="e.g., Button"
fullWidth fullWidth
/> />
<Textarea <Textarea
label="Description" label="Definition Text"
value={formData.description} value={formData.definition_text}
onChange={(e) => setFormData(d => ({ ...d, description: (e.target as HTMLTextAreaElement).value }))} onChange={(e) => setFormData(d => ({ ...d, definition_text: (e.target as HTMLTextAreaElement).value }))}
placeholder="Describe the expected behavior..." placeholder="Describe the expected behavior..."
fullWidth fullWidth
/> />
<Input <Input
label="Expected Value" label="Expected Value"
value={formData.expected_value} value={formData.expected_value || ''}
onChange={(e) => setFormData(d => ({ ...d, expected_value: (e.target as HTMLInputElement).value }))} onChange={(e) => setFormData(d => ({ ...d, expected_value: (e.target as HTMLInputElement).value }))}
placeholder="e.g., 4px or var(--radius-md)" placeholder="e.g., 4px or var(--radius-md)"
fullWidth fullWidth
/> />
<Input
label="CSS Selector (optional)"
value={formData.selector}
onChange={(e) => setFormData(d => ({ ...d, selector: (e.target as HTMLInputElement).value }))}
placeholder="e.g., .btn-primary"
fullWidth
/>
<Input
label="CSS Property (optional)"
value={formData.css_property}
onChange={(e) => setFormData(d => ({ ...d, css_property: (e.target as HTMLInputElement).value }))}
placeholder="e.g., border-radius"
fullWidth
/>
</div> </div>
{error && ( {error && (
<div className="form-error"> <div className="form-error">
@@ -416,16 +401,6 @@ function ESREEditorTool() {
<div className="esre-expected"> <div className="esre-expected">
<code>{esre.expected_value}</code> <code>{esre.expected_value}</code>
</div> </div>
<div className="esre-status">
{esre.last_result && (
<Badge
variant={esre.last_result === 'pass' ? 'success' : esre.last_result === 'fail' ? 'error' : 'default'}
size="sm"
>
{esre.last_result}
</Badge>
)}
</div>
<div className="esre-actions"> <div className="esre-actions">
<Button variant="ghost" size="sm" onClick={() => handleDelete(esre.id)}> <Button variant="ghost" size="sm" onClick={() => handleDelete(esre.id)}>
Delete Delete
@@ -758,9 +733,9 @@ function TestResultsTool() {
setResults(esres.map(esre => ({ setResults(esres.map(esre => ({
id: esre.id, id: esre.id,
name: esre.name, name: esre.name,
status: esre.last_result || 'skip', status: 'skip',
duration: 0, duration: 0,
error: esre.last_result === 'fail' ? 'Expected value mismatch' : undefined error: undefined
}))); })));
} catch (err) { } catch (err) {
console.error('Failed to load results:', err); console.error('Failed to load results:', err);
@@ -788,7 +763,7 @@ function TestResultsTool() {
<div className="workdesk"> <div className="workdesk">
<div className="workdesk-header"> <div className="workdesk-header">
<h1 className="workdesk-title">Test Results</h1> <h1 className="workdesk-title">Test Results</h1>
<p className="workdesk-subtitle">ESRE test execution results</p> <p className="workdesk-subtitle">ESRE definitions (runner not implemented yet)</p>
</div> </div>
{/* Summary */} {/* Summary */}

View File

@@ -7,7 +7,14 @@ import { Input, Select } from '../components/base/Input';
import { Spinner } from '../components/base/Spinner'; import { Spinner } from '../components/base/Spinner';
import { endpoints } from '../api/client'; import { endpoints } from '../api/client';
import { currentProject } from '../state/project'; import { currentProject } from '../state/project';
import type { TokenDrift, Component, FigmaExtractResult } from '../api/types'; import type {
Component,
FigmaExtractComponentsResult,
FigmaExtractTokensResult,
FigmaHealthStatus,
TokenDrift,
TokenDriftStats,
} from '../api/types';
import './Workdesk.css'; import './Workdesk.css';
interface UIWorkdeskProps { interface UIWorkdeskProps {
@@ -45,7 +52,7 @@ function UIDashboard() {
time: string; time: string;
status: string; status: string;
}>>([]); }>>([]);
const [figmaHealth, setFigmaHealth] = useState<{ connected: boolean } | null>(null); const [figmaHealth, setFigmaHealth] = useState<FigmaHealthStatus | null>(null);
useEffect(() => { useEffect(() => {
loadDashboardData(); loadDashboardData();
@@ -92,12 +99,12 @@ function UIDashboard() {
} }
if (driftResult.status === 'fulfilled') { if (driftResult.status === 'fulfilled') {
const drifts = driftResult.value; const { stats } = driftResult.value;
setMetrics(m => ({ setMetrics(m => ({
...m, ...m,
tokenDrift: drifts.length, tokenDrift: stats.total,
criticalIssues: drifts.filter(d => d.severity === 'critical').length, criticalIssues: stats.by_severity.critical || 0,
warnings: drifts.filter(d => d.severity === 'medium' || d.severity === 'low').length warnings: (stats.by_severity.warning || 0) + (stats.by_severity.info || 0)
})); }));
} }
} }
@@ -125,22 +132,24 @@ function UIDashboard() {
switch (actionId) { switch (actionId) {
case 'extractTokens': case 'extractTokens':
window.location.hash = '#ui/figma-extraction'; window.location.hash = '#figma-extraction';
break; break;
case 'extractComponents': case 'extractComponents':
window.location.hash = '#ui/figma-components'; window.location.hash = '#figma-components';
break; break;
case 'generateCode': case 'generateCode':
window.location.hash = '#ui/code-generator'; window.location.hash = '#code-generator';
break; break;
case 'syncTokens': case 'syncTokens':
// Quick sync using default settings // Quick sync using default settings
try { try {
const fileKey = currentProject.value?.path; const fileKey = currentProject.value?.figma_file_key;
if (fileKey) { if (!fileKey) {
await endpoints.figma.syncTokens(fileKey, './tokens.css', 'css'); alert('No Figma file key configured for this project. Set it in Admin → Projects.');
loadDashboardData(); return;
} }
await endpoints.figma.syncTokens(fileKey, './tokens.css', 'css');
loadDashboardData();
} catch (err) { } catch (err) {
console.error('Sync failed:', err); console.error('Sync failed:', err);
} }
@@ -176,9 +185,9 @@ function UIDashboard() {
{/* Figma Connection Status */} {/* Figma Connection Status */}
{figmaHealth && ( {figmaHealth && (
<div className={`connection-status ${figmaHealth.connected ? 'connected' : 'disconnected'}`}> <div className={`connection-status ${figmaHealth.status === 'ok' ? 'connected' : 'disconnected'}`}>
<Badge variant={figmaHealth.connected ? 'success' : 'error'} size="sm"> <Badge variant={figmaHealth.status === 'ok' ? 'success' : 'warning'} size="sm" title={figmaHealth.message}>
Figma: {figmaHealth.connected ? 'Connected' : 'Not Connected'} Figma: {figmaHealth.status} ({figmaHealth.mode})
</Badge> </Badge>
</div> </div>
)} )}
@@ -263,12 +272,15 @@ function UIDashboard() {
function FigmaExtractionTool() { function FigmaExtractionTool() {
const [fileKey, setFileKey] = useState(''); const [fileKey, setFileKey] = useState('');
const [nodeId, setNodeId] = useState('');
const [format, setFormat] = useState('css'); const [format, setFormat] = useState('css');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [result, setResult] = useState<FigmaExtractResult | null>(null); const [result, setResult] = useState<FigmaExtractTokensResult | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => {
setFileKey(currentProject.value?.figma_file_key || '');
}, [currentProject.value?.id]);
async function handleExtract() { async function handleExtract() {
if (!fileKey.trim()) { if (!fileKey.trim()) {
setError('Please enter a Figma file key or URL'); setError('Please enter a Figma file key or URL');
@@ -282,7 +294,7 @@ function FigmaExtractionTool() {
try { try {
// Extract file key from URL if needed // Extract file key from URL if needed
const key = extractFigmaFileKey(fileKey); const key = extractFigmaFileKey(fileKey);
const extractResult = await endpoints.figma.extractVariables(key, nodeId || undefined); const extractResult = await endpoints.figma.extractVariables(key, format);
setResult(extractResult); setResult(extractResult);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Extraction failed'); setError(err instanceof Error ? err.message : 'Extraction failed');
@@ -298,8 +310,8 @@ function FigmaExtractionTool() {
try { try {
const key = extractFigmaFileKey(fileKey); const key = extractFigmaFileKey(fileKey);
const targetPath = `./tokens.${format === 'scss' ? 'scss' : format === 'json' ? 'json' : 'css'}`; const targetPath = `./tokens.${format === 'scss' ? 'scss' : format === 'json' ? 'json' : 'css'}`;
await endpoints.figma.syncTokens(key, targetPath, format); const syncResult = await endpoints.figma.syncTokens(key, targetPath, format);
alert(`Tokens synced to ${targetPath}`); alert(`${syncResult.tokens_synced} tokens synced to ${targetPath}${syncResult.has_changes ? '' : ' (no changes)'}`);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Sync failed'); setError(err instanceof Error ? err.message : 'Sync failed');
} finally { } finally {
@@ -325,14 +337,6 @@ function FigmaExtractionTool() {
placeholder="Enter Figma file key or paste URL" placeholder="Enter Figma file key or paste URL"
fullWidth fullWidth
/> />
<Input
label="Node ID (optional)"
value={nodeId}
onChange={(e) => setNodeId((e.target as HTMLInputElement).value)}
placeholder="Specific node ID to extract from"
hint="Leave empty to extract from entire file"
fullWidth
/>
<Select <Select
label="Output Format" label="Output Format"
value={format} value={format}
@@ -368,22 +372,13 @@ function FigmaExtractionTool() {
<Card variant="bordered" padding="md"> <Card variant="bordered" padding="md">
<CardHeader <CardHeader
title="Extraction Results" title="Extraction Results"
subtitle={`${result.count} tokens extracted`} subtitle={`${result.tokens_count} tokens extracted`}
/> />
<CardContent> <CardContent>
{result.warnings && result.warnings.length > 0 && (
<div className="result-warnings">
{result.warnings.map((warning, idx) => (
<Badge key={idx} variant="warning" size="sm">{warning}</Badge>
))}
</div>
)}
<div className="tokens-preview"> <div className="tokens-preview">
<pre className="code-preview"> <pre className="code-preview">
{result.items.slice(0, 10).map(item => ( {result.tokens.slice(0, 10).map(t => `${t.name}: ${JSON.stringify(t.value)}`).join('\n')}
`${item.name}: ${JSON.stringify(item.value)}\n` {result.tokens.length > 10 && `\n... and ${result.tokens.length - 10} more`}
)).join('')}
{result.items.length > 10 && `\n... and ${result.items.length - 10} more`}
</pre> </pre>
</div> </div>
</CardContent> </CardContent>
@@ -396,9 +391,13 @@ function FigmaExtractionTool() {
function FigmaComponentsTool() { function FigmaComponentsTool() {
const [fileKey, setFileKey] = useState(''); const [fileKey, setFileKey] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [components, setComponents] = useState<FigmaExtractResult | null>(null); const [components, setComponents] = useState<FigmaExtractComponentsResult | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => {
setFileKey(currentProject.value?.figma_file_key || '');
}, [currentProject.value?.id]);
async function handleExtract() { async function handleExtract() {
if (!fileKey.trim()) { if (!fileKey.trim()) {
setError('Please enter a Figma file key or URL'); setError('Please enter a Figma file key or URL');
@@ -456,14 +455,14 @@ function FigmaComponentsTool() {
<Card variant="bordered" padding="md"> <Card variant="bordered" padding="md">
<CardHeader <CardHeader
title="Components Found" title="Components Found"
subtitle={`${components.count} components`} subtitle={`${components.components_count} components`}
/> />
<CardContent> <CardContent>
<div className="components-list"> <div className="components-list">
{components.items.map((comp, idx) => ( {components.components.map(comp => (
<div key={idx} className="component-item"> <div key={comp.key} className="component-item">
<span className="component-name">{comp.name}</span> <span className="component-name">{comp.name}</span>
<Badge size="sm">{comp.type}</Badge> <Badge size="sm">{comp.key}</Badge>
</div> </div>
))} ))}
</div> </div>
@@ -478,6 +477,7 @@ function CodeGeneratorTool() {
const [components, setComponents] = useState<Component[]>([]); const [components, setComponents] = useState<Component[]>([]);
const [selectedComponent, setSelectedComponent] = useState(''); const [selectedComponent, setSelectedComponent] = useState('');
const [framework, setFramework] = useState('react'); const [framework, setFramework] = useState('react');
const [fileKey, setFileKey] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [generatedCode, setGeneratedCode] = useState<string | null>(null); const [generatedCode, setGeneratedCode] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -486,6 +486,10 @@ function CodeGeneratorTool() {
loadComponents(); loadComponents();
}, []); }, []);
useEffect(() => {
setFileKey(currentProject.value?.figma_file_key || '');
}, [currentProject.value?.id]);
async function loadComponents() { async function loadComponents() {
const projectId = currentProject.value?.id; const projectId = currentProject.value?.id;
if (!projectId) return; if (!projectId) return;
@@ -503,6 +507,10 @@ function CodeGeneratorTool() {
setError('Please select a component'); setError('Please select a component');
return; return;
} }
if (!fileKey.trim()) {
setError('Set a Figma file key (Admin → Projects) or enter one here.');
return;
}
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -511,10 +519,7 @@ function CodeGeneratorTool() {
const component = components.find(c => c.id === selectedComponent); const component = components.find(c => c.id === selectedComponent);
if (!component) throw new Error('Component not found'); if (!component) throw new Error('Component not found');
const result = await endpoints.figma.generateCode( const result = await endpoints.figma.generateCode(fileKey, component.name, framework);
{ name: component.name, figma_node_id: component.figma_node_id },
framework
);
setGeneratedCode(result.code); setGeneratedCode(result.code);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Code generation failed'); setError(err instanceof Error ? err.message : 'Code generation failed');
@@ -534,13 +539,20 @@ function CodeGeneratorTool() {
<CardHeader title="Generate Component" /> <CardHeader title="Generate Component" />
<CardContent> <CardContent>
<div className="settings-form"> <div className="settings-form">
<Input
label="Figma File Key"
value={fileKey}
onChange={(e) => setFileKey((e.target as HTMLInputElement).value)}
placeholder="Figma file key"
fullWidth
/>
<Select <Select
label="Component" label="Component"
value={selectedComponent} value={selectedComponent}
onChange={(e) => setSelectedComponent((e.target as HTMLSelectElement).value)} onChange={(e) => setSelectedComponent((e.target as HTMLSelectElement).value)}
options={[ options={[
{ value: '', label: 'Select a component...' }, { value: '', label: 'Select a component...' },
...components.map(c => ({ value: c.id, label: c.display_name || c.name })) ...components.map(c => ({ value: c.id, label: c.name }))
]} ]}
/> />
<Select <Select
@@ -590,6 +602,7 @@ function CodeGeneratorTool() {
function TokenDriftTool() { function TokenDriftTool() {
const [drifts, setDrifts] = useState<TokenDrift[]>([]); const [drifts, setDrifts] = useState<TokenDrift[]>([]);
const [stats, setStats] = useState<TokenDriftStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -605,7 +618,8 @@ function TokenDriftTool() {
try { try {
const result = await endpoints.tokens.drift(projectId); const result = await endpoints.tokens.drift(projectId);
setDrifts(result); setDrifts(result.drifts);
setStats(result.stats);
} catch (err) { } catch (err) {
console.error('Failed to load drifts:', err); console.error('Failed to load drifts:', err);
} finally { } finally {
@@ -618,8 +632,8 @@ function TokenDriftTool() {
if (!projectId) return; if (!projectId) return;
try { try {
await endpoints.tokens.updateDriftStatus(projectId, driftId, 'resolved'); await endpoints.tokens.updateDriftStatus(projectId, driftId, 'fixed');
loadDrifts(); void loadDrifts();
} catch (err) { } catch (err) {
console.error('Failed to resolve drift:', err); console.error('Failed to resolve drift:', err);
} }
@@ -646,7 +660,7 @@ function TokenDriftTool() {
<Card variant="bordered" padding="md"> <Card variant="bordered" padding="md">
<CardHeader <CardHeader
title="Drift Issues" title="Drift Issues"
subtitle={`${drifts.length} issues found`} subtitle={`${stats?.total ?? drifts.length} issues found`}
action={<Button variant="outline" size="sm" onClick={loadDrifts}>Refresh</Button>} action={<Button variant="outline" size="sm" onClick={loadDrifts}>Refresh</Button>}
/> />
<CardContent> <CardContent>
@@ -657,23 +671,22 @@ function TokenDriftTool() {
{drifts.map(drift => ( {drifts.map(drift => (
<div key={drift.id} className="drift-item"> <div key={drift.id} className="drift-item">
<div className="drift-info"> <div className="drift-info">
<span className="drift-token">{drift.token_name}</span> <span className="drift-token">{drift.component_id} {drift.property_name}</span>
<span className="drift-file">{drift.file_path}</span> <span className="drift-file">{drift.file_path}:{drift.line_number}</span>
</div> </div>
<div className="drift-values"> <div className="drift-values">
<code className="drift-expected">Expected: {drift.expected_value}</code> <code className="drift-expected">Hardcoded: {drift.hardcoded_value}</code>
<code className="drift-actual">Actual: {drift.actual_value}</code> <code className="drift-actual">Suggested: {drift.suggested_token || '-'}</code>
</div> </div>
<div className="drift-actions"> <div className="drift-actions">
<Badge variant={ <Badge variant={
drift.severity === 'critical' ? 'error' : drift.severity === 'critical' ? 'error' :
drift.severity === 'high' ? 'error' : drift.severity === 'warning' ? 'warning' : 'default'
drift.severity === 'medium' ? 'warning' : 'default'
} size="sm"> } size="sm">
{drift.severity} {drift.severity}
</Badge> </Badge>
<Button variant="ghost" size="sm" onClick={() => handleResolve(drift.id)}> <Button variant="ghost" size="sm" onClick={() => handleResolve(drift.id)}>
Resolve Mark Fixed
</Button> </Button>
</div> </div>
</div> </div>
@@ -689,26 +702,69 @@ function TokenDriftTool() {
function QuickWinsTool() { function QuickWinsTool() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [quickWins, setQuickWins] = useState<Array<{ const [quickWins, setQuickWins] = useState<Array<{
id: number; id: string;
title: string; title: string;
priority: string; priority?: string;
effort: string; effort?: string;
file?: string; file?: string;
}>>([]); }>>([]);
const [rawResult, setRawResult] = useState<unknown | null>(null);
const [error, setError] = useState<string | null>(null);
async function scanProject() { async function scanProject() {
setLoading(true); setLoading(true);
try { try {
// Use discovery scan to find quick wins setError(null);
await endpoints.discovery.scan(); setRawResult(null);
// Mock data for now - will be replaced with actual API response parsing setQuickWins([]);
setQuickWins([
{ id: 1, title: 'Replace hardcoded color #333', priority: 'high', effort: 'low', file: 'Button.tsx' }, const projectId = currentProject.value?.id;
{ id: 2, title: 'Use spacing token instead of 16px', priority: 'medium', effort: 'low', file: 'Card.tsx' }, if (!projectId) {
{ id: 3, title: 'Update font weight to token', priority: 'low', effort: 'low', file: 'Header.tsx' } setError('Select a project first');
]); return;
}
// Prefer an MCP tool (portable across environments); fall back to discovery scan only if needed.
const tools = await endpoints.mcp.tools();
const candidate = tools.find(t =>
/quick|win/i.test(t.name) || /quick|win/i.test(t.description)
);
if (!candidate) {
await endpoints.discovery.scan();
setError('No MCP "quick wins" tool found; ran discovery scan instead.');
return;
}
type ExecResponse = { success: boolean; result?: unknown; error?: string };
const exec = await endpoints.mcp.execute(candidate.name, { project_id: projectId, arguments: {} }) as ExecResponse;
if (!exec.success) {
throw new Error(exec.error || 'Quick wins tool failed');
}
const result = exec.result;
setRawResult(result ?? null);
const listCandidate =
Array.isArray(result) ? result :
(result && typeof result === 'object' && Array.isArray((result as Record<string, unknown>).quick_wins)) ? (result as Record<string, unknown>).quick_wins :
(result && typeof result === 'object' && Array.isArray((result as Record<string, unknown>).items)) ? (result as Record<string, unknown>).items :
null;
if (!Array.isArray(listCandidate)) return;
setQuickWins(listCandidate.map((item, idx) => {
const obj = (item && typeof item === 'object') ? (item as Record<string, unknown>) : {};
return {
id: String(obj.id || idx),
title: String(obj.title || obj.message || obj.name || 'Quick win'),
priority: typeof obj.priority === 'string' ? obj.priority : undefined,
effort: typeof obj.effort === 'string' ? obj.effort : undefined,
file: typeof obj.file === 'string' ? obj.file : undefined,
};
}));
} catch (err) { } catch (err) {
console.error('Scan failed:', err); setError(err instanceof Error ? err.message : 'Scan failed');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -731,6 +787,11 @@ function QuickWinsTool() {
} }
/> />
<CardContent> <CardContent>
{error && (
<div className="form-error">
<Badge variant="error">{error}</Badge>
</div>
)}
{quickWins.length === 0 ? ( {quickWins.length === 0 ? (
<p className="text-muted">Click "Scan Project" to find quick wins</p> <p className="text-muted">Click "Scan Project" to find quick wins</p>
) : ( ) : (
@@ -742,20 +803,32 @@ function QuickWinsTool() {
{win.file && <span className="quick-win-file">{win.file}</span>} {win.file && <span className="quick-win-file">{win.file}</span>}
</div> </div>
<div className="quick-win-badges"> <div className="quick-win-badges">
<Badge {win.priority && (
variant={win.priority === 'high' ? 'error' : win.priority === 'medium' ? 'warning' : 'default'} <Badge
size="sm" variant={win.priority === 'high' ? 'error' : win.priority === 'medium' ? 'warning' : 'default'}
> size="sm"
{win.priority} >
</Badge> {win.priority}
<Badge variant="success" size="sm"> </Badge>
{win.effort} effort )}
</Badge> {win.effort && (
<Badge variant="success" size="sm">
{win.effort} effort
</Badge>
)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
{rawResult && (
<div style={{ marginTop: 12 }}>
<details>
<summary className="text-muted">Raw tool output</summary>
<pre className="code-preview">{JSON.stringify(rawResult, null, 2)}</pre>
</details>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { Input } from '../components/base/Input';
import { Spinner } from '../components/base/Spinner'; import { Spinner } from '../components/base/Spinner';
import { endpoints } from '../api/client'; import { endpoints } from '../api/client';
import { currentProject } from '../state/project'; import { currentProject } from '../state/project';
import type { FigmaFile } from '../api/types'; import type { Component, FigmaFile, FigmaHealthStatus, FigmaStyleDefinition, FigmaToken } from '../api/types';
import './Workdesk.css'; import './Workdesk.css';
interface UXWorkdeskProps { interface UXWorkdeskProps {
@@ -34,7 +34,7 @@ export default function UXWorkdesk({ activeTool }: UXWorkdeskProps) {
function UXDashboard() { function UXDashboard() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [figmaFiles, setFigmaFiles] = useState<FigmaFile[]>([]); const [figmaFiles, setFigmaFiles] = useState<FigmaFile[]>([]);
const [figmaHealth, setFigmaHealth] = useState<{ connected: boolean } | null>(null); const [figmaHealth, setFigmaHealth] = useState<FigmaHealthStatus | null>(null);
const [metrics, setMetrics] = useState({ const [metrics, setMetrics] = useState({
figmaFiles: 0, figmaFiles: 0,
synced: 0, synced: 0,
@@ -52,7 +52,7 @@ function UXDashboard() {
const projectId = currentProject.value?.id; const projectId = currentProject.value?.id;
// Load Figma health // Load Figma health
const healthResult = await endpoints.figma.health().catch(() => ({ connected: false })); const healthResult = await endpoints.figma.health().catch(() => null);
setFigmaHealth(healthResult); setFigmaHealth(healthResult);
// Load project-specific data // Load project-specific data
@@ -60,8 +60,8 @@ function UXDashboard() {
const filesResult = await endpoints.projects.figmaFiles(projectId).catch(() => []); const filesResult = await endpoints.projects.figmaFiles(projectId).catch(() => []);
setFigmaFiles(filesResult); setFigmaFiles(filesResult);
const synced = filesResult.filter(f => f.status === 'synced').length; const synced = filesResult.filter(f => f.sync_status === 'synced').length;
const pending = filesResult.filter(f => f.status === 'pending').length; const pending = filesResult.filter(f => f.sync_status === 'pending').length;
setMetrics({ setMetrics({
figmaFiles: filesResult.length, figmaFiles: filesResult.length,
@@ -119,9 +119,9 @@ function UXDashboard() {
{/* Connection Status */} {/* Connection Status */}
{figmaHealth && ( {figmaHealth && (
<div className={`connection-status ${figmaHealth.connected ? 'connected' : 'disconnected'}`}> <div className={`connection-status ${figmaHealth.status === 'ok' ? 'connected' : 'disconnected'}`}>
<Badge variant={figmaHealth.connected ? 'success' : 'error'} size="sm"> <Badge variant={figmaHealth.status === 'ok' ? 'success' : 'warning'} size="sm" title={figmaHealth.message}>
Figma: {figmaHealth.connected ? 'Connected' : 'Not Connected'} Figma: {figmaHealth.status} ({figmaHealth.mode})
</Badge> </Badge>
</div> </div>
)} )}
@@ -173,15 +173,15 @@ function UXDashboard() {
{figmaFiles.map(file => ( {figmaFiles.map(file => (
<div key={file.id} className="figma-file-item"> <div key={file.id} className="figma-file-item">
<div className="figma-file-info"> <div className="figma-file-info">
<span className="figma-file-name">{file.name}</span> <span className="figma-file-name">{file.file_name}</span>
<span className="figma-file-key">{file.file_key}</span> <span className="figma-file-key">{file.file_key}</span>
</div> </div>
<div className="figma-file-status"> <div className="figma-file-status">
<Badge <Badge
variant={file.status === 'synced' ? 'success' : file.status === 'error' ? 'error' : 'warning'} variant={file.sync_status === 'synced' ? 'success' : file.sync_status === 'error' ? 'error' : 'warning'}
size="sm" size="sm"
> >
{file.status === 'synced' ? 'Synced' : file.status === 'error' ? 'Error' : 'Pending'} {file.sync_status === 'synced' ? 'Synced' : file.sync_status === 'error' ? 'Error' : 'Pending'}
</Badge> </Badge>
{file.last_synced && ( {file.last_synced && (
<span className="figma-file-sync">{formatTimeAgo(file.last_synced)}</span> <span className="figma-file-sync">{formatTimeAgo(file.last_synced)}</span>
@@ -201,7 +201,7 @@ function UXDashboard() {
} }
function AddFigmaFileCard({ onAdded }: { onAdded: () => void }) { function AddFigmaFileCard({ onAdded }: { onAdded: () => void }) {
const [formData, setFormData] = useState({ name: '', fileKey: '' }); const [formData, setFormData] = useState({ fileName: '', figmaUrl: '' });
const [adding, setAdding] = useState(false); const [adding, setAdding] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -212,8 +212,8 @@ function AddFigmaFileCard({ onAdded }: { onAdded: () => void }) {
return; return;
} }
if (!formData.name || !formData.fileKey) { if (!formData.fileName || !formData.figmaUrl) {
setError('Name and File Key are required'); setError('File Name and Figma URL/File Key are required');
return; return;
} }
@@ -222,9 +222,18 @@ function AddFigmaFileCard({ onAdded }: { onAdded: () => void }) {
try { try {
// Extract file key from URL if needed // Extract file key from URL if needed
const fileKey = extractFigmaFileKey(formData.fileKey); const fileKey = extractFigmaFileKey(formData.figmaUrl);
await endpoints.projects.addFigmaFile(projectId, { name: formData.name, fileKey }); const figmaUrl = formData.figmaUrl.includes('figma.com')
setFormData({ name: '', fileKey: '' }); ? formData.figmaUrl
: `https://www.figma.com/file/${fileKey}`;
await endpoints.projects.addFigmaFile(projectId, {
figma_url: figmaUrl,
file_name: formData.fileName,
file_key: fileKey
});
setFormData({ fileName: '', figmaUrl: '' });
onAdded(); onAdded();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to add Figma file'); setError(err instanceof Error ? err.message : 'Failed to add Figma file');
@@ -240,15 +249,15 @@ function AddFigmaFileCard({ onAdded }: { onAdded: () => void }) {
<form className="add-figma-form" onSubmit={(e) => { e.preventDefault(); handleAdd(); }}> <form className="add-figma-form" onSubmit={(e) => { e.preventDefault(); handleAdd(); }}>
<Input <Input
label="File Name" label="File Name"
value={formData.name} value={formData.fileName}
onChange={(e) => setFormData(d => ({ ...d, name: (e.target as HTMLInputElement).value }))} onChange={(e) => setFormData(d => ({ ...d, fileName: (e.target as HTMLInputElement).value }))}
placeholder="e.g., Design System" placeholder="e.g., Design System"
fullWidth fullWidth
/> />
<Input <Input
label="Figma URL or File Key" label="Figma URL or File Key"
value={formData.fileKey} value={formData.figmaUrl}
onChange={(e) => setFormData(d => ({ ...d, fileKey: (e.target as HTMLInputElement).value }))} onChange={(e) => setFormData(d => ({ ...d, figmaUrl: (e.target as HTMLInputElement).value }))}
placeholder="https://figma.com/file/... or file key" placeholder="https://figma.com/file/... or file key"
fullWidth fullWidth
/> />
@@ -288,12 +297,12 @@ function TokenListTool() {
if (projectId) { if (projectId) {
const files = await endpoints.projects.figmaFiles(projectId); const files = await endpoints.projects.figmaFiles(projectId);
if (files.length > 0) { if (files.length > 0) {
const result = await endpoints.figma.extractVariables(files[0].file_key); const result = await endpoints.figma.extractVariables(files[0].file_key, 'json');
setTokens(result.items.map(item => ({ setTokens(result.tokens.map((token: FigmaToken) => ({
name: item.name, name: token.name,
value: String(item.value), value: typeof token.value === 'string' ? token.value : JSON.stringify(token.value),
type: item.type, type: token.type,
category: item.metadata?.category as string category: token.category
}))); })));
} }
} }
@@ -462,15 +471,15 @@ function FigmaFilesTool() {
{files.map(file => ( {files.map(file => (
<div key={file.id} className="figma-file-item"> <div key={file.id} className="figma-file-item">
<div className="figma-file-info"> <div className="figma-file-info">
<span className="figma-file-name">{file.name}</span> <span className="figma-file-name">{file.file_name}</span>
<span className="figma-file-key">{file.file_key}</span> <span className="figma-file-key">{file.file_key}</span>
</div> </div>
<div className="figma-file-status"> <div className="figma-file-status">
<Badge <Badge
variant={file.status === 'synced' ? 'success' : file.status === 'error' ? 'error' : 'warning'} variant={file.sync_status === 'synced' ? 'success' : file.sync_status === 'error' ? 'error' : 'warning'}
size="sm" size="sm"
> >
{file.status} {file.sync_status}
</Badge> </Badge>
</div> </div>
<div className="figma-file-actions"> <div className="figma-file-actions">
@@ -522,13 +531,13 @@ function AssetListTool() {
if (files.length > 0) { if (files.length > 0) {
const result = await endpoints.figma.extractStyles(files[0].file_key); const result = await endpoints.figma.extractStyles(files[0].file_key);
// Transform styles into assets // Transform styles into assets
const extractedAssets = result.items const extractedAssets = result.styles
.filter(item => item.type === 'effect' || item.type === 'paint') .filter((style: FigmaStyleDefinition) => style.type === 'effect' || style.type === 'paint')
.map((item, idx) => ({ .map((style: FigmaStyleDefinition, idx: number) => ({
id: String(idx), id: style.key || String(idx),
name: item.name, name: style.name,
type: 'icon' as const, type: 'icon' as const,
format: 'svg', format: 'figma-style',
size: '-' size: '-'
})); }));
setAssets(extractedAssets); setAssets(extractedAssets);
@@ -643,12 +652,12 @@ function ComponentListTool() {
const projectId = currentProject.value?.id; const projectId = currentProject.value?.id;
if (projectId) { if (projectId) {
const result = await endpoints.projects.components(projectId); const result = await endpoints.projects.components(projectId);
setComponents(result.map(c => ({ setComponents(result.map((c: Component) => ({
id: c.id, id: c.id,
name: c.display_name || c.name, name: c.name,
description: c.description, description: c.description,
variants: c.variants?.length || 0, variants: c.variants?.length || 0,
status: (c as unknown as { status?: string }).status === 'active' ? 'ready' : 'in-progress' status: c.code_generated ? 'ready' : 'planned'
}))); })));
} }
} catch (err) { } catch (err) {

View File

@@ -783,22 +783,22 @@ async def list_workflows():
return {"workflows": workflows, "count": len(workflows), "directory": str(workflows_dir)} return {"workflows": workflows, "count": len(workflows), "directory": str(workflows_dir)}
@app.get("/api/config") @app.get("/api/public-config")
async def get_config(): async def get_public_config():
""" """
Public configuration endpoint. Public configuration endpoint.
Returns ONLY safe, non-sensitive configuration values that are safe Returns ONLY safe, non-sensitive configuration values that are safe
to expose to the client browser. to expose to the client browser.
SECURITY: This endpoint is the ONLY place where configuration is exposed. NOTE: The Admin UI uses `/api/config` for runtime settings (with secrets masked).
All other config values (secrets, API keys, etc.) must be server-only. This endpoint is kept for backwards compatibility with older clients.
""" """
# Import here to avoid circular imports # Import here to avoid circular imports
try: try:
from config import get_public_config from config import get_public_config as _get_public_config
return get_public_config() return _get_public_config()
except ImportError: except ImportError:
# Fallback for legacy deployments # Fallback for legacy deployments
return { return {
@@ -848,6 +848,7 @@ async def create_project(project: ProjectCreate):
name=project.name, name=project.name,
description=project.description, description=project.description,
figma_file_key=project.figma_file_key, figma_file_key=project.figma_file_key,
root_path=project.root_path,
) )
ActivityLog.log( ActivityLog.log(
action="project_created", action="project_created",

View File

@@ -218,7 +218,13 @@ class Projects:
(base / subdir).mkdir(parents=True, exist_ok=True) (base / subdir).mkdir(parents=True, exist_ok=True)
@staticmethod @staticmethod
def create(id: str, name: str, description: str = "", figma_file_key: str = "") -> Dict: def create(
id: str,
name: str,
description: str = "",
figma_file_key: str = "",
root_path: str = "",
) -> Dict:
"""Create a new project.""" """Create a new project."""
Projects._init_project_structure(id) Projects._init_project_structure(id)
@@ -228,6 +234,7 @@ class Projects:
"name": name, "name": name,
"description": description, "description": description,
"figma_file_key": figma_file_key, "figma_file_key": figma_file_key,
"root_path": root_path,
"status": "active", "status": "active",
"created_at": now, "created_at": now,
"updated_at": now, "updated_at": now,