admin-ui: align with API, add auth, fix integrations
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 ============
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 */}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
alert('No Figma file key configured for this project. Set it in Admin → Projects.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await endpoints.figma.syncTokens(fileKey, './tokens.css', 'css');
|
await endpoints.figma.syncTokens(fileKey, './tokens.css', 'css');
|
||||||
loadDashboardData();
|
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);
|
||||||
|
setRawResult(null);
|
||||||
|
setQuickWins([]);
|
||||||
|
|
||||||
|
const projectId = currentProject.value?.id;
|
||||||
|
if (!projectId) {
|
||||||
|
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();
|
await endpoints.discovery.scan();
|
||||||
// Mock data for now - will be replaced with actual API response parsing
|
setError('No MCP "quick wins" tool found; ran discovery scan instead.');
|
||||||
setQuickWins([
|
return;
|
||||||
{ id: 1, title: 'Replace hardcoded color #333', priority: 'high', effort: 'low', file: 'Button.tsx' },
|
}
|
||||||
{ id: 2, title: 'Use spacing token instead of 16px', priority: 'medium', effort: 'low', file: 'Card.tsx' },
|
|
||||||
{ id: 3, title: 'Update font weight to token', priority: 'low', effort: 'low', file: 'Header.tsx' }
|
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">
|
||||||
|
{win.priority && (
|
||||||
<Badge
|
<Badge
|
||||||
variant={win.priority === 'high' ? 'error' : win.priority === 'medium' ? 'warning' : 'default'}
|
variant={win.priority === 'high' ? 'error' : win.priority === 'medium' ? 'warning' : 'default'}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{win.priority}
|
{win.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
)}
|
||||||
|
{win.effort && (
|
||||||
<Badge variant="success" size="sm">
|
<Badge variant="success" size="sm">
|
||||||
{win.effort} effort
|
{win.effort} effort
|
||||||
</Badge>
|
</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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user