mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
Add E2E tests for internationalization across multiple pages
- Implemented navigation.spec.ts to test language switching and translation of navigation elements. - Created sessions-page.spec.ts to verify translations on the sessions page, including headers, status badges, and date formatting. - Developed settings-page.spec.ts to ensure settings page content is translated and persists across sessions. - Added skills-page.spec.ts to validate translations for skill categories, action buttons, and empty states.
This commit is contained in:
@@ -7,6 +7,62 @@ import type { SessionMetadata, TaskData } from '../types/store';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
/**
|
||||
* Raw backend session data structure matching the backend API response.
|
||||
*
|
||||
* @remarks
|
||||
* This interface represents the exact schema returned by the backend `/api/data` endpoint.
|
||||
* It is used internally during transformation to `SessionMetadata` in the frontend.
|
||||
*
|
||||
* **Field mappings to frontend SessionMetadata:**
|
||||
* - `project` → `title` and `description` (split on ':' separator)
|
||||
* - `status: 'active'` → `status: 'in_progress'` (other statuses remain unchanged)
|
||||
* - `location` is added based on which array (activeSessions/archivedSessions) the data comes from
|
||||
*
|
||||
* **Backend schema location:** `ccw/src/data-aggregator.ts`
|
||||
* **Transformation function:** {@link transformBackendSession}
|
||||
* **Frontend type:** {@link SessionMetadata}
|
||||
*
|
||||
* @warning If backend schema changes, update this interface AND the transformation logic in {@link transformBackendSession}
|
||||
*/
|
||||
interface BackendSessionData {
|
||||
session_id: string;
|
||||
project?: string;
|
||||
status: 'active' | 'completed' | 'archived' | 'planning' | 'paused';
|
||||
type?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard statistics mapped from backend statistics response.
|
||||
*
|
||||
* @remarks
|
||||
* This interface represents the frontend statistics type displayed on the dashboard.
|
||||
* The data is extracted from the backend `/api/data` response's `statistics` field.
|
||||
*
|
||||
* **Backend response structure:**
|
||||
* ```json
|
||||
* {
|
||||
* "statistics": {
|
||||
* "totalSessions": number,
|
||||
* "activeSessions": number,
|
||||
* "archivedSessions": number,
|
||||
* "totalTasks": number,
|
||||
* "completedTasks": number,
|
||||
* "pendingTasks": number,
|
||||
* "failedTasks": number,
|
||||
* "todayActivity": number
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Mapping function:** {@link fetchDashboardStats}
|
||||
* **Fallback:** Returns zero-initialized stats on error via {@link getEmptyDashboardStats}
|
||||
*
|
||||
* @see {@link fetchDashboardStats} for the transformation logic
|
||||
*/
|
||||
export interface DashboardStats {
|
||||
totalSessions: number;
|
||||
activeSessions: number;
|
||||
@@ -92,8 +148,9 @@ async function fetchApi<T>(
|
||||
const body = await response.json();
|
||||
if (body.message) error.message = body.message;
|
||||
if (body.code) error.code = body.code;
|
||||
} catch {
|
||||
// Ignore JSON parse errors
|
||||
} catch (parseError) {
|
||||
// Log parse errors instead of silently ignoring
|
||||
console.warn('[API] Failed to parse error response:', parseError);
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -104,7 +161,68 @@ async function fetchApi<T>(
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
// Wrap response.json() with try-catch for better error messages
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (parseError) {
|
||||
const message = parseError instanceof Error ? parseError.message : 'Unknown error';
|
||||
throw new Error(`Failed to parse JSON response: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Transformation Helpers ==========
|
||||
|
||||
/**
|
||||
* Transform backend session data to frontend SessionMetadata interface
|
||||
* Maps backend schema (project, status: 'active') to frontend schema (title, description, status: 'in_progress', location)
|
||||
*
|
||||
* @param backendSession - Raw session data from backend
|
||||
* @param location - Whether this session is from active or archived list
|
||||
* @returns Transformed SessionMetadata object
|
||||
*/
|
||||
function transformBackendSession(
|
||||
backendSession: BackendSessionData,
|
||||
location: 'active' | 'archived'
|
||||
): SessionMetadata {
|
||||
// Map backend 'active' status to frontend 'in_progress'
|
||||
// Other statuses remain the same
|
||||
const statusMap: Record<string, SessionMetadata['status']> = {
|
||||
'active': 'in_progress',
|
||||
'completed': 'completed',
|
||||
'archived': 'archived',
|
||||
'planning': 'planning',
|
||||
'paused': 'paused',
|
||||
};
|
||||
|
||||
const transformedStatus = statusMap[backendSession.status] || backendSession.status as SessionMetadata['status'];
|
||||
|
||||
// Extract title and description from project field
|
||||
// Backend sends 'project' as a string, frontend expects 'title' and optional 'description'
|
||||
let title = backendSession.project || backendSession.session_id;
|
||||
let description: string | undefined;
|
||||
|
||||
if (backendSession.project && backendSession.project.includes(':')) {
|
||||
const parts = backendSession.project.split(':');
|
||||
title = parts[0].trim();
|
||||
description = parts.slice(1).join(':').trim();
|
||||
}
|
||||
|
||||
return {
|
||||
session_id: backendSession.session_id,
|
||||
title,
|
||||
description,
|
||||
status: transformedStatus,
|
||||
created_at: backendSession.created_at,
|
||||
updated_at: backendSession.updated_at,
|
||||
location,
|
||||
// Preserve additional fields if they exist
|
||||
has_plan: (backendSession as unknown as { has_plan?: boolean }).has_plan,
|
||||
plan_updated_at: (backendSession as unknown as { plan_updated_at?: string }).plan_updated_at,
|
||||
has_review: (backendSession as unknown as { has_review?: boolean }).has_review,
|
||||
review: (backendSession as unknown as { review?: SessionMetadata['review'] }).review,
|
||||
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
|
||||
tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Dashboard API ==========
|
||||
@@ -113,18 +231,45 @@ async function fetchApi<T>(
|
||||
* Fetch dashboard statistics
|
||||
*/
|
||||
export async function fetchDashboardStats(): Promise<DashboardStats> {
|
||||
const data = await fetchApi<{ statistics?: DashboardStats }>('/api/data');
|
||||
try {
|
||||
const data = await fetchApi<{ statistics?: DashboardStats }>('/api/data');
|
||||
|
||||
// Extract statistics from response, with defaults
|
||||
// Validate response structure
|
||||
if (!data) {
|
||||
console.warn('[API] No data received from /api/data for dashboard stats');
|
||||
return getEmptyDashboardStats();
|
||||
}
|
||||
|
||||
// Extract statistics from response, with defaults
|
||||
return {
|
||||
totalSessions: data.statistics?.totalSessions ?? 0,
|
||||
activeSessions: data.statistics?.activeSessions ?? 0,
|
||||
archivedSessions: data.statistics?.archivedSessions ?? 0,
|
||||
totalTasks: data.statistics?.totalTasks ?? 0,
|
||||
completedTasks: data.statistics?.completedTasks ?? 0,
|
||||
pendingTasks: data.statistics?.pendingTasks ?? 0,
|
||||
failedTasks: data.statistics?.failedTasks ?? 0,
|
||||
todayActivity: data.statistics?.todayActivity ?? 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to fetch dashboard stats:', error);
|
||||
return getEmptyDashboardStats();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get empty dashboard stats with zero values
|
||||
*/
|
||||
function getEmptyDashboardStats(): DashboardStats {
|
||||
return {
|
||||
totalSessions: data.statistics?.totalSessions ?? 0,
|
||||
activeSessions: data.statistics?.activeSessions ?? 0,
|
||||
archivedSessions: data.statistics?.archivedSessions ?? 0,
|
||||
totalTasks: data.statistics?.totalTasks ?? 0,
|
||||
completedTasks: data.statistics?.completedTasks ?? 0,
|
||||
pendingTasks: data.statistics?.pendingTasks ?? 0,
|
||||
failedTasks: data.statistics?.failedTasks ?? 0,
|
||||
todayActivity: data.statistics?.todayActivity ?? 0,
|
||||
totalSessions: 0,
|
||||
activeSessions: 0,
|
||||
archivedSessions: 0,
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
pendingTasks: 0,
|
||||
failedTasks: 0,
|
||||
todayActivity: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,17 +277,61 @@ export async function fetchDashboardStats(): Promise<DashboardStats> {
|
||||
|
||||
/**
|
||||
* Fetch all sessions (active and archived)
|
||||
* Applies transformation layer to map backend data to frontend SessionMetadata interface
|
||||
*/
|
||||
export async function fetchSessions(): Promise<SessionsResponse> {
|
||||
const data = await fetchApi<{
|
||||
activeSessions?: SessionMetadata[];
|
||||
archivedSessions?: SessionMetadata[];
|
||||
}>('/api/data');
|
||||
try {
|
||||
const data = await fetchApi<{
|
||||
activeSessions?: BackendSessionData[];
|
||||
archivedSessions?: BackendSessionData[];
|
||||
}>('/api/data');
|
||||
|
||||
return {
|
||||
activeSessions: data.activeSessions ?? [],
|
||||
archivedSessions: data.archivedSessions ?? [],
|
||||
};
|
||||
// Validate response structure
|
||||
if (!data) {
|
||||
console.warn('[API] No data received from /api/data for sessions');
|
||||
return { activeSessions: [], archivedSessions: [] };
|
||||
}
|
||||
|
||||
// Transform active sessions with location = 'active'
|
||||
const activeSessions = (data.activeSessions ?? []).map((session) => {
|
||||
try {
|
||||
return transformBackendSession(session, 'active');
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to transform active session:', session, error);
|
||||
// Return a minimal valid session to prevent crashes
|
||||
return {
|
||||
session_id: session.session_id,
|
||||
title: session.project || session.session_id,
|
||||
status: 'in_progress' as const,
|
||||
created_at: session.created_at,
|
||||
location: 'active' as const,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Transform archived sessions with location = 'archived'
|
||||
const archivedSessions = (data.archivedSessions ?? []).map((session) => {
|
||||
try {
|
||||
return transformBackendSession(session, 'archived');
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to transform archived session:', session, error);
|
||||
// Return a minimal valid session to prevent crashes
|
||||
return {
|
||||
session_id: session.session_id,
|
||||
title: session.project || session.session_id,
|
||||
status: session.status === 'active' ? 'in_progress' : session.status as SessionMetadata['status'],
|
||||
created_at: session.created_at,
|
||||
location: 'archived' as const,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return { activeSessions, archivedSessions };
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to fetch sessions:', error);
|
||||
// Return empty arrays on error to prevent crashes
|
||||
return { activeSessions: [], archivedSessions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,6 +760,177 @@ export async function deleteMemory(memoryId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Project Overview API ==========
|
||||
|
||||
export interface TechnologyStack {
|
||||
languages: Array<{ name: string; file_count: number; primary?: boolean }>;
|
||||
frameworks: string[];
|
||||
build_tools: string[];
|
||||
test_frameworks?: string[];
|
||||
}
|
||||
|
||||
export interface Architecture {
|
||||
style: string;
|
||||
layers: string[];
|
||||
patterns: string[];
|
||||
}
|
||||
|
||||
export interface KeyComponent {
|
||||
name: string;
|
||||
description?: string;
|
||||
importance: 'high' | 'medium' | 'low';
|
||||
responsibility?: string[];
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface DevelopmentIndexEntry {
|
||||
title: string;
|
||||
description?: string;
|
||||
sessionId?: string;
|
||||
sub_feature?: string;
|
||||
status?: string;
|
||||
tags?: string[];
|
||||
archivedAt?: string;
|
||||
date?: string;
|
||||
implemented_at?: string;
|
||||
}
|
||||
|
||||
export interface GuidelineEntry {
|
||||
rule: string;
|
||||
scope: string;
|
||||
enforced_by?: string;
|
||||
}
|
||||
|
||||
export interface LearningEntry {
|
||||
insight: string;
|
||||
category?: string;
|
||||
session_id?: string;
|
||||
context?: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface ProjectGuidelines {
|
||||
conventions?: Record<string, string[]>;
|
||||
constraints?: Record<string, string[]>;
|
||||
quality_rules?: GuidelineEntry[];
|
||||
learnings?: LearningEntry[];
|
||||
}
|
||||
|
||||
export interface ProjectOverviewMetadata {
|
||||
analysis_mode?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ProjectOverview {
|
||||
projectName: string;
|
||||
description?: string;
|
||||
initializedAt: string;
|
||||
technologyStack: TechnologyStack;
|
||||
architecture: Architecture;
|
||||
keyComponents: KeyComponent[];
|
||||
developmentIndex?: {
|
||||
feature?: DevelopmentIndexEntry[];
|
||||
enhancement?: DevelopmentIndexEntry[];
|
||||
bugfix?: DevelopmentIndexEntry[];
|
||||
refactor?: DevelopmentIndexEntry[];
|
||||
docs?: DevelopmentIndexEntry[];
|
||||
[key: string]: DevelopmentIndexEntry[] | undefined;
|
||||
};
|
||||
guidelines?: ProjectGuidelines;
|
||||
metadata?: ProjectOverviewMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch project overview
|
||||
*/
|
||||
export async function fetchProjectOverview(): Promise<ProjectOverview | null> {
|
||||
const data = await fetchApi<{ projectOverview?: ProjectOverview }>('/api/ccw');
|
||||
return data.projectOverview ?? null;
|
||||
}
|
||||
|
||||
// ========== Session Detail API ==========
|
||||
|
||||
export interface SessionDetailContext {
|
||||
requirements?: string[];
|
||||
focus_paths?: string[];
|
||||
artifacts?: string[];
|
||||
shared_context?: {
|
||||
tech_stack?: string[];
|
||||
conventions?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionDetailResponse {
|
||||
session: SessionMetadata;
|
||||
context?: SessionDetailContext;
|
||||
summary?: string;
|
||||
implPlan?: unknown;
|
||||
conflicts?: unknown[];
|
||||
review?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch session detail
|
||||
*/
|
||||
export async function fetchSessionDetail(sessionId: string): Promise<SessionDetailResponse> {
|
||||
return fetchApi<SessionDetailResponse>(`/api/sessions/${encodeURIComponent(sessionId)}/detail`);
|
||||
}
|
||||
|
||||
// ========== History / CLI Execution API ==========
|
||||
|
||||
export interface CliExecution {
|
||||
id: string;
|
||||
tool: 'gemini' | 'qwen' | 'codex' | string;
|
||||
mode?: string;
|
||||
status: 'success' | 'error' | 'timeout';
|
||||
prompt_preview: string;
|
||||
timestamp: string;
|
||||
duration_ms: number;
|
||||
sourceDir?: string;
|
||||
turn_count?: number;
|
||||
}
|
||||
|
||||
export interface HistoryResponse {
|
||||
executions: CliExecution[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch CLI execution history
|
||||
*/
|
||||
export async function fetchHistory(): Promise<HistoryResponse> {
|
||||
const data = await fetchApi<{ executions?: CliExecution[] }>('/api/cli/history');
|
||||
return {
|
||||
executions: data.executions ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a CLI execution record
|
||||
*/
|
||||
export async function deleteExecution(executionId: string): Promise<void> {
|
||||
await fetchApi<void>(`/api/cli/history/${encodeURIComponent(executionId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete CLI executions by tool
|
||||
*/
|
||||
export async function deleteExecutionsByTool(tool: string): Promise<void> {
|
||||
await fetchApi<void>(`/api/cli/history/tool/${encodeURIComponent(tool)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all CLI execution history
|
||||
*/
|
||||
export async function deleteAllHistory(): Promise<void> {
|
||||
await fetchApi<void>('/api/cli/history', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== CLI Tools Config API ==========
|
||||
|
||||
export interface CliToolsConfigResponse {
|
||||
@@ -602,3 +962,461 @@ export async function updateCliToolsConfig(
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Lite Tasks API ==========
|
||||
|
||||
export interface ImplementationStep {
|
||||
step: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
modification_points?: string[];
|
||||
logic_flow?: string[];
|
||||
depends_on?: number[];
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export interface FlowControl {
|
||||
pre_analysis?: Array<{
|
||||
step: string;
|
||||
action: string;
|
||||
commands?: string[];
|
||||
output_to: string;
|
||||
on_error?: 'fail' | 'continue' | 'skip';
|
||||
}>;
|
||||
implementation_approach?: ImplementationStep[];
|
||||
target_files?: string[];
|
||||
}
|
||||
|
||||
export interface LiteTask {
|
||||
id: string;
|
||||
task_id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'blocked' | 'failed';
|
||||
priority?: string;
|
||||
flow_control?: FlowControl;
|
||||
meta?: {
|
||||
type?: string;
|
||||
scope?: string;
|
||||
};
|
||||
context?: {
|
||||
focus_paths?: string[];
|
||||
acceptance?: string[];
|
||||
depends_on?: string[];
|
||||
};
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface LiteTaskSession {
|
||||
id: string;
|
||||
session_id?: string;
|
||||
type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
|
||||
title?: string;
|
||||
description?: string;
|
||||
tasks?: LiteTask[];
|
||||
metadata?: Record<string, unknown>;
|
||||
latestSynthesis?: {
|
||||
title?: string | { en?: string; zh?: string };
|
||||
status?: string;
|
||||
};
|
||||
roundCount?: number;
|
||||
status?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface LiteTasksResponse {
|
||||
litePlan?: LiteTaskSession[];
|
||||
liteFix?: LiteTaskSession[];
|
||||
multiCliPlan?: LiteTaskSession[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all lite tasks sessions
|
||||
*/
|
||||
export async function fetchLiteTasks(): Promise<LiteTasksResponse> {
|
||||
const data = await fetchApi<{ liteTasks?: LiteTasksResponse }>('/api/data');
|
||||
return data.liteTasks || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single lite task session by ID
|
||||
*/
|
||||
export async function fetchLiteTaskSession(
|
||||
sessionId: string,
|
||||
type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan'
|
||||
): Promise<LiteTaskSession | null> {
|
||||
const data = await fetchLiteTasks();
|
||||
const sessions = type === 'lite-plan' ? (data.litePlan || []) :
|
||||
type === 'lite-fix' ? (data.liteFix || []) :
|
||||
(data.multiCliPlan || []);
|
||||
return sessions.find(s => s.id === sessionId || s.session_id === sessionId) || null;
|
||||
}
|
||||
|
||||
// ========== Review Session API ==========
|
||||
|
||||
export interface ReviewFinding {
|
||||
id?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
category?: string;
|
||||
file?: string;
|
||||
line?: string;
|
||||
code_context?: string;
|
||||
snippet?: string;
|
||||
recommendations?: string[];
|
||||
recommendation?: string;
|
||||
root_cause?: string;
|
||||
impact?: string;
|
||||
references?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
fix_status?: string | null;
|
||||
}
|
||||
|
||||
export interface ReviewDimension {
|
||||
name: string;
|
||||
findings: ReviewFinding[];
|
||||
}
|
||||
|
||||
export interface ReviewSession {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
type: 'review';
|
||||
phase?: string;
|
||||
reviewDimensions?: ReviewDimension[];
|
||||
_isActive?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface ReviewSessionsResponse {
|
||||
reviewSessions?: ReviewSession[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all review sessions
|
||||
*/
|
||||
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
|
||||
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
|
||||
return data.reviewSessions || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single review session by ID
|
||||
*/
|
||||
export async function fetchReviewSession(sessionId: string): Promise<ReviewSession | null> {
|
||||
const sessions = await fetchReviewSessions();
|
||||
return sessions.find(s => s.session_id === sessionId) || null;
|
||||
}
|
||||
|
||||
// ========== MCP API ==========
|
||||
|
||||
export interface McpServer {
|
||||
name: string;
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
enabled: boolean;
|
||||
scope: 'project' | 'global';
|
||||
}
|
||||
|
||||
export interface McpServersResponse {
|
||||
project: McpServer[];
|
||||
global: McpServer[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all MCP servers (project and global scope)
|
||||
*/
|
||||
export async function fetchMcpServers(): Promise<McpServersResponse> {
|
||||
const data = await fetchApi<{ project?: McpServer[]; global?: McpServer[] }>('/api/mcp/servers');
|
||||
return {
|
||||
project: data.project ?? [],
|
||||
global: data.global ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update MCP server configuration
|
||||
*/
|
||||
export async function updateMcpServer(
|
||||
serverName: string,
|
||||
config: Partial<McpServer>
|
||||
): Promise<McpServer> {
|
||||
return fetchApi<McpServer>(`/api/mcp/servers/${encodeURIComponent(serverName)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new MCP server
|
||||
*/
|
||||
export async function createMcpServer(
|
||||
server: Omit<McpServer, 'name'>
|
||||
): Promise<McpServer> {
|
||||
return fetchApi<McpServer>('/api/mcp/servers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(server),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an MCP server
|
||||
*/
|
||||
export async function deleteMcpServer(serverName: string): Promise<void> {
|
||||
await fetchApi<void>(`/api/mcp/servers/${encodeURIComponent(serverName)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle MCP server enabled status
|
||||
*/
|
||||
export async function toggleMcpServer(
|
||||
serverName: string,
|
||||
enabled: boolean
|
||||
): Promise<McpServer> {
|
||||
return fetchApi<McpServer>(`/api/mcp/servers/${encodeURIComponent(serverName)}/toggle`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== CLI Endpoints API ==========
|
||||
|
||||
export interface CliEndpoint {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'litellm' | 'custom' | 'wrapper';
|
||||
enabled: boolean;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CliEndpointsResponse {
|
||||
endpoints: CliEndpoint[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all CLI endpoints
|
||||
*/
|
||||
export async function fetchCliEndpoints(): Promise<CliEndpointsResponse> {
|
||||
const data = await fetchApi<{ endpoints?: CliEndpoint[] }>('/api/cli/endpoints');
|
||||
return {
|
||||
endpoints: data.endpoints ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CLI endpoint configuration
|
||||
*/
|
||||
export async function updateCliEndpoint(
|
||||
endpointId: string,
|
||||
config: Partial<CliEndpoint>
|
||||
): Promise<CliEndpoint> {
|
||||
return fetchApi<CliEndpoint>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new CLI endpoint
|
||||
*/
|
||||
export async function createCliEndpoint(
|
||||
endpoint: Omit<CliEndpoint, 'id'>
|
||||
): Promise<CliEndpoint> {
|
||||
return fetchApi<CliEndpoint>('/api/cli/endpoints', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(endpoint),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a CLI endpoint
|
||||
*/
|
||||
export async function deleteCliEndpoint(endpointId: string): Promise<void> {
|
||||
await fetchApi<void>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle CLI endpoint enabled status
|
||||
*/
|
||||
export async function toggleCliEndpoint(
|
||||
endpointId: string,
|
||||
enabled: boolean
|
||||
): Promise<CliEndpoint> {
|
||||
return fetchApi<CliEndpoint>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}/toggle`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== CLI Installations API ==========
|
||||
|
||||
export interface CliInstallation {
|
||||
name: string;
|
||||
version: string;
|
||||
installed: boolean;
|
||||
path?: string;
|
||||
status: 'active' | 'inactive' | 'error';
|
||||
lastChecked?: string;
|
||||
}
|
||||
|
||||
export interface CliInstallationsResponse {
|
||||
tools: CliInstallation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all CLI tool installations
|
||||
*/
|
||||
export async function fetchCliInstallations(): Promise<CliInstallationsResponse> {
|
||||
const data = await fetchApi<{ tools?: CliInstallation[] }>('/api/cli/installations');
|
||||
return {
|
||||
tools: data.tools ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a CLI tool
|
||||
*/
|
||||
export async function installCliTool(toolName: string): Promise<CliInstallation> {
|
||||
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/install`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall a CLI tool
|
||||
*/
|
||||
export async function uninstallCliTool(toolName: string): Promise<void> {
|
||||
await fetchApi<void>(`/api/cli/installations/${encodeURIComponent(toolName)}/uninstall`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade a CLI tool
|
||||
*/
|
||||
export async function upgradeCliTool(toolName: string): Promise<CliInstallation> {
|
||||
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/upgrade`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check CLI tool installation status
|
||||
*/
|
||||
export async function checkCliToolStatus(toolName: string): Promise<CliInstallation> {
|
||||
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/check`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Hooks API ==========
|
||||
|
||||
export interface Hook {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
script?: string;
|
||||
trigger: 'pre-commit' | 'post-commit' | 'pre-push' | 'custom';
|
||||
}
|
||||
|
||||
export interface HooksResponse {
|
||||
hooks: Hook[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all hooks
|
||||
*/
|
||||
export async function fetchHooks(): Promise<HooksResponse> {
|
||||
const data = await fetchApi<{ hooks?: Hook[] }>('/api/hooks');
|
||||
return {
|
||||
hooks: data.hooks ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update hook configuration
|
||||
*/
|
||||
export async function updateHook(
|
||||
hookName: string,
|
||||
config: Partial<Hook>
|
||||
): Promise<Hook> {
|
||||
return fetchApi<Hook>(`/api/hooks/${encodeURIComponent(hookName)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle hook enabled status
|
||||
*/
|
||||
export async function toggleHook(
|
||||
hookName: string,
|
||||
enabled: boolean
|
||||
): Promise<Hook> {
|
||||
return fetchApi<Hook>(`/api/hooks/${encodeURIComponent(hookName)}/toggle`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Rules API ==========
|
||||
|
||||
export interface Rule {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
category?: string;
|
||||
pattern?: string;
|
||||
severity?: 'error' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
export interface RulesResponse {
|
||||
rules: Rule[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all rules
|
||||
*/
|
||||
export async function fetchRules(): Promise<RulesResponse> {
|
||||
const data = await fetchApi<{ rules?: Rule[] }>('/api/rules');
|
||||
return {
|
||||
rules: data.rules ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rule configuration
|
||||
*/
|
||||
export async function updateRule(
|
||||
ruleId: string,
|
||||
config: Partial<Rule>
|
||||
): Promise<Rule> {
|
||||
return fetchApi<Rule>(`/api/rules/${encodeURIComponent(ruleId)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle rule enabled status
|
||||
*/
|
||||
export async function toggleRule(
|
||||
ruleId: string,
|
||||
enabled: boolean
|
||||
): Promise<Rule> {
|
||||
return fetchApi<Rule>(`/api/rules/${encodeURIComponent(ruleId)}/toggle`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
}
|
||||
|
||||
156
ccw/frontend/src/lib/i18n.ts
Normal file
156
ccw/frontend/src/lib/i18n.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// ========================================
|
||||
// i18n Configuration
|
||||
// ========================================
|
||||
// Internationalization setup with react-intl
|
||||
|
||||
import { createIntl, createIntlCache } from '@formatjs/intl';
|
||||
|
||||
// Supported locales
|
||||
export type Locale = 'en' | 'zh';
|
||||
|
||||
// Available locales with display names
|
||||
export const availableLocales: Record<Locale, string> = {
|
||||
en: 'English',
|
||||
zh: '中文',
|
||||
};
|
||||
|
||||
// Browser language detection
|
||||
function getBrowserLocale(): Locale {
|
||||
if (typeof window === 'undefined') return 'zh';
|
||||
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
if (browserLang.startsWith('zh')) return 'zh';
|
||||
if (browserLang.startsWith('en')) return 'en';
|
||||
|
||||
// Default to Chinese for unsupported languages
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
// Get initial locale from localStorage or browser detection
|
||||
export function getInitialLocale(): Locale {
|
||||
if (typeof window === 'undefined') return 'zh';
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem('ccw-app-store');
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed.state?.locale && (parsed.state.locale === 'en' || parsed.state.locale === 'zh')) {
|
||||
return parsed.state.locale as Locale;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
return getBrowserLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translation messages for a locale
|
||||
* Dynamically imports the consolidated translation file
|
||||
* NOTE: This dynamic import relies on Vite's glob import feature
|
||||
* to bundle the locale index.ts files.
|
||||
*/
|
||||
async function loadMessages(locale: Locale): Promise<Record<string, string>> {
|
||||
try {
|
||||
// Dynamic import with .ts extension for Vite compatibility
|
||||
const messagesModule = await import(`../locales/${locale}/index.ts`);
|
||||
return messagesModule.default || {};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load messages for locale "${locale}":`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Translation messages (will be populated by loading message files)
|
||||
const messages: Record<Locale, Record<string, string>> = {
|
||||
en: {},
|
||||
zh: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize translation messages for all locales
|
||||
* Call this during app initialization
|
||||
*/
|
||||
export async function initMessages(): Promise<void> {
|
||||
// Load messages for both locales in parallel
|
||||
const [enMessages, zhMessages] = await Promise.all([
|
||||
loadMessages('en'),
|
||||
loadMessages('zh'),
|
||||
]);
|
||||
|
||||
messages.en = enMessages;
|
||||
messages.zh = zhMessages;
|
||||
|
||||
// Update current intl instance with loaded messages
|
||||
const currentLocale = getInitialLocale();
|
||||
updateIntl(currentLocale);
|
||||
}
|
||||
|
||||
// Cache for intl instances to avoid recreating on every render
|
||||
const intlCache = createIntlCache();
|
||||
|
||||
// Current intl instance (will be updated when locale changes)
|
||||
let currentIntl = createIntl(
|
||||
{
|
||||
locale: getInitialLocale(),
|
||||
messages: messages[getInitialLocale()],
|
||||
},
|
||||
intlCache
|
||||
);
|
||||
|
||||
/**
|
||||
* Get translation messages for a locale
|
||||
* This will be used to load messages dynamically
|
||||
*/
|
||||
export function getMessages(locale: Locale): Record<string, string> {
|
||||
return messages[locale];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current intl instance with a new locale
|
||||
*/
|
||||
export function updateIntl(locale: Locale): void {
|
||||
currentIntl = createIntl(
|
||||
{
|
||||
locale,
|
||||
messages: messages[locale],
|
||||
},
|
||||
intlCache
|
||||
);
|
||||
|
||||
// Update document lang attribute
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current intl instance
|
||||
*/
|
||||
export function getIntl() {
|
||||
return currentIntl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register messages for a locale
|
||||
* This can be used to dynamically load translation files
|
||||
*/
|
||||
export function registerMessages(locale: Locale, newMessages: Record<string, string>): void {
|
||||
messages[locale] = { ...messages[locale], ...newMessages };
|
||||
|
||||
// Update current intl if this is the active locale
|
||||
if (currentIntl.locale === locale) {
|
||||
updateIntl(locale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a message using the current intl instance
|
||||
*/
|
||||
export function formatMessage(
|
||||
id: string,
|
||||
values?: Record<string, string | number | boolean | Date | null | undefined>
|
||||
): string {
|
||||
return currentIntl.formatMessage({ id }, values);
|
||||
}
|
||||
30
ccw/frontend/src/lib/query-client.ts
Normal file
30
ccw/frontend/src/lib/query-client.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// ========================================
|
||||
// Query Client Configuration
|
||||
// ========================================
|
||||
// TanStack Query client configuration for React
|
||||
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
/**
|
||||
* Query client instance with default configuration
|
||||
*/
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Time in milliseconds that data remains fresh
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
// Time in milliseconds that unused data is cached
|
||||
gcTime: 1000 * 60 * 10, // 10 minutes
|
||||
// Number of times to retry failed queries
|
||||
retry: 1,
|
||||
// Disable refetch on window focus for better UX
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
mutations: {
|
||||
// Number of times to retry failed mutations
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default queryClient;
|
||||
Reference in New Issue
Block a user