sciagent code + Gitea Actions CI/CD
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thinh Lam
2026-06-30 09:38:30 +07:00
commit 688fac73e9
1167 changed files with 158244 additions and 0 deletions
+658
View File
@@ -0,0 +1,658 @@
# Frontend Architecture Redesign
## Overview
This document outlines a production-ready frontend architecture for the ProfytAI Compliance Management Platform, addressing scalability, maintainability, and developer experience.
## Design Principles
1. **Feature-Based Organization**: Group related code by feature/domain
2. **Separation of Concerns**: Clear boundaries between layers
3. **Type Safety**: Full TypeScript coverage
4. **Reusability**: Shared components and utilities
5. **Testability**: Easy to test components and logic
6. **Performance**: Code splitting, lazy loading, optimization
7. **Developer Experience**: Clear structure, good DX
---
## New Directory Structure
```
fe0/
├── src/
│ ├── app/ # App-level configuration
│ │ ├── providers/ # Global providers
│ │ │ ├── QueryProvider.tsx
│ │ │ ├── AuthProvider.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ └── ErrorBoundary.tsx
│ │ ├── router/ # Routing configuration
│ │ │ ├── index.tsx
│ │ │ ├── routes.tsx
│ │ │ └── guards.tsx
│ │ └── store/ # Global state (if needed)
│ │ └── index.ts
│ │
│ ├── features/ # Feature-based modules
│ │ ├── auth/
│ │ │ ├── components/
│ │ │ │ ├── LoginForm.tsx
│ │ │ │ └── SignUpForm.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useAuth.ts
│ │ │ │ └── useLogin.ts
│ │ │ ├── services/
│ │ │ │ └── authService.ts
│ │ │ ├── types/
│ │ │ │ └── auth.types.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── workflows/
│ │ │ ├── components/
│ │ │ │ ├── WorkflowList.tsx
│ │ │ │ ├── WorkflowCard.tsx
│ │ │ │ ├── WorkflowForm.tsx
│ │ │ │ └── WorkflowItem.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useWorkflows.ts
│ │ │ │ ├── useWorkflow.ts
│ │ │ │ └── useCreateWorkflow.ts
│ │ │ ├── services/
│ │ │ │ └── workflowService.ts
│ │ │ ├── types/
│ │ │ │ └── workflow.types.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── documents/
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ └── types/
│ │ │
│ │ ├── compliance/
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ └── types/
│ │ │
│ │ └── dashboard/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── types/
│ │
│ ├── shared/ # Shared code
│ │ ├── api/ # API client
│ │ │ ├── client.ts # Axios/Fetch client
│ │ │ ├── interceptors.ts # Request/Response interceptors
│ │ │ ├── endpoints.ts # API endpoint constants
│ │ │ └── types.ts # API response types
│ │ │
│ │ ├── components/ # Shared UI components
│ │ │ ├── ui/ # shadcn components (existing)
│ │ │ ├── layout/
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ └── Footer.tsx
│ │ │ ├── feedback/
│ │ │ │ ├── LoadingSpinner.tsx
│ │ │ │ ├── ErrorMessage.tsx
│ │ │ │ └── EmptyState.tsx
│ │ │ └── forms/
│ │ │ └── FormField.tsx
│ │ │
│ │ ├── hooks/ # Shared hooks
│ │ │ ├── useApi.ts
│ │ │ ├── useDebounce.ts
│ │ │ ├── useLocalStorage.ts
│ │ │ └── useMediaQuery.ts
│ │ │
│ │ ├── utils/ # Utility functions
│ │ │ ├── format.ts
│ │ │ ├── validation.ts
│ │ │ ├── date.ts
│ │ │ └── constants.ts
│ │ │
│ │ ├── types/ # Shared TypeScript types
│ │ │ ├── api.types.ts
│ │ │ ├── common.types.ts
│ │ │ └── index.ts
│ │ │
│ │ └── config/ # Configuration
│ │ ├── env.ts # Environment variables
│ │ ├── constants.ts # App constants
│ │ └── routes.ts # Route constants
│ │
│ ├── pages/ # Page components
│ │ ├── public/
│ │ │ ├── HomePage.tsx
│ │ │ ├── LoginPage.tsx
│ │ │ └── NotFoundPage.tsx
│ │ └── protected/
│ │ ├── DashboardPage.tsx
│ │ └── WorkflowsPage.tsx
│ │
│ ├── layouts/ # Layout components
│ │ ├── PublicLayout.tsx
│ │ ├── DashboardLayout.tsx
│ │ └── AuthLayout.tsx
│ │
│ ├── assets/ # Static assets
│ │ ├── images/
│ │ ├── icons/
│ │ └── fonts/
│ │
│ ├── styles/ # Global styles
│ │ ├── globals.css
│ │ └── themes.css
│ │
│ ├── App.tsx # Root component
│ └── main.tsx # Entry point
├── public/ # Public assets
├── tests/ # Test files
│ ├── unit/
│ ├── integration/
│ ├── e2e/
│ └── setup.ts
├── .env.example
├── .env.development
├── .env.production
├── vite.config.ts
├── tsconfig.json
└── package.json
```
---
## Key Architectural Components
### 1. API Client Layer
**Purpose**: Centralized HTTP client with interceptors, error handling, and type safety.
**Features**:
- Request/Response interceptors
- Automatic token refresh
- Error handling
- Type-safe endpoints
- Request cancellation
- Retry logic
### 2. Service Layer
**Purpose**: Business logic and API communication abstraction.
**Features**:
- Type-safe API calls
- Data transformation
- Error handling
- Caching strategies
### 3. Feature Modules
**Purpose**: Self-contained feature modules with all related code.
**Structure**:
- Components (UI)
- Hooks (React hooks)
- Services (API calls)
- Types (TypeScript types)
- Utils (Feature-specific utilities)
### 4. Shared Layer
**Purpose**: Reusable code across features.
**Includes**:
- UI components
- Utilities
- Hooks
- Types
- Configuration
### 5. Routing
**Purpose**: Centralized route configuration with guards.
**Features**:
- Route guards (auth, permissions)
- Lazy loading
- Code splitting
- Route constants
---
## Implementation Examples
### Example 1: API Client
```typescript
// shared/api/client.ts
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
import { env } from '@/shared/config/env';
import { getAccessToken, clearAccessToken, setAccessToken } from '@/shared/utils/token';
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: env.API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
// Request interceptor
this.client.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Handle 401 - Unauthorized
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newToken = await this.refreshToken();
if (newToken && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return this.client(originalRequest);
}
} catch (refreshError) {
clearAccessToken();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Handle other errors
return Promise.reject(this.handleError(error));
}
);
}
private async refreshToken(): Promise<string | null> {
try {
const response = await axios.post(`${env.API_URL}/auth/refresh`, {}, {
withCredentials: true,
});
const { accessToken } = response.data;
setAccessToken(accessToken);
return accessToken;
} catch (error) {
return null;
}
}
private handleError(error: AxiosError): ApiError {
if (error.response) {
// Server responded with error
return {
message: error.response.data?.message || 'An error occurred',
status: error.response.status,
data: error.response.data,
};
} else if (error.request) {
// Request made but no response
return {
message: 'Network error. Please check your connection.',
status: 0,
};
} else {
// Error setting up request
return {
message: error.message || 'An unexpected error occurred',
status: 0,
};
}
}
public get<T = any>(url: string, config?: any): Promise<T> {
return this.client.get<T>(url, config).then((res) => res.data);
}
public post<T = any>(url: string, data?: any, config?: any): Promise<T> {
return this.client.post<T>(url, data, config).then((res) => res.data);
}
public put<T = any>(url: string, data?: any, config?: any): Promise<T> {
return this.client.put<T>(url, data, config).then((res) => res.data);
}
public delete<T = any>(url: string, config?: any): Promise<T> {
return this.client.delete<T>(url, config).then((res) => res.data);
}
public patch<T = any>(url: string, data?: any, config?: any): Promise<T> {
return this.client.patch<T>(url, data, config).then((res) => res.data);
}
}
export const apiClient = new ApiClient();
export type ApiError = {
message: string;
status: number;
data?: any;
};
```
### Example 2: Service Layer
```typescript
// features/workflows/services/workflowService.ts
import { apiClient } from '@/shared/api/client';
import { Workflow, CreateWorkflowDto, UpdateWorkflowItemDto } from '../types/workflow.types';
import { ApiResponse, PaginatedResponse } from '@/shared/types/api.types';
export const workflowService = {
async getAll(params?: {
skip?: number;
limit?: number;
projectName?: string;
}): Promise<PaginatedResponse<Workflow>> {
return apiClient.get<PaginatedResponse<Workflow>>('/api/v1/workflows', { params });
},
async getById(id: string): Promise<Workflow> {
return apiClient.get<Workflow>(`/api/v1/workflows/${id}`);
},
async create(data: CreateWorkflowDto): Promise<Workflow> {
return apiClient.post<Workflow>('/api/v1/workflows', data);
},
async updateItem(
workflowId: string,
data: UpdateWorkflowItemDto
): Promise<Workflow> {
return apiClient.put<Workflow>(
`/api/v1/workflows/${workflowId}/items`,
data
);
},
async advancePhase(workflowId: string): Promise<Workflow> {
return apiClient.post<Workflow>(
`/api/v1/workflows/${workflowId}/advance`
);
},
async delete(id: string): Promise<void> {
return apiClient.delete(`/api/v1/workflows/${id}`);
},
};
```
### Example 3: Custom Hooks
```typescript
// features/workflows/hooks/useWorkflows.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { workflowService } from '../services/workflowService';
import { CreateWorkflowDto, UpdateWorkflowItemDto } from '../types/workflow.types';
import { toast } from 'sonner';
export const useWorkflows = (params?: {
skip?: number;
limit?: number;
projectName?: string;
}) => {
return useQuery({
queryKey: ['workflows', params],
queryFn: () => workflowService.getAll(params),
});
};
export const useWorkflow = (id: string) => {
return useQuery({
queryKey: ['workflow', id],
queryFn: () => workflowService.getById(id),
enabled: !!id,
});
};
export const useCreateWorkflow = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateWorkflowDto) => workflowService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] });
toast.success('Workflow created successfully');
},
onError: (error: any) => {
toast.error(error.message || 'Failed to create workflow');
},
});
};
export const useUpdateWorkflowItem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ workflowId, data }: { workflowId: string; data: UpdateWorkflowItemDto }) =>
workflowService.updateItem(workflowId, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['workflow', variables.workflowId] });
queryClient.invalidateQueries({ queryKey: ['workflows'] });
toast.success('Workflow item updated');
},
onError: (error: any) => {
toast.error(error.message || 'Failed to update workflow item');
},
});
};
```
### Example 4: Environment Configuration
```typescript
// shared/config/env.ts
interface EnvConfig {
API_URL: string;
WS_URL?: string;
ENVIRONMENT: 'development' | 'staging' | 'production';
ENABLE_DEV_TOOLS: boolean;
}
const getEnvVar = (key: string, defaultValue?: string): string => {
const value = import.meta.env[key];
if (!value && !defaultValue) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value || defaultValue || '';
};
export const env: EnvConfig = {
API_URL: getEnvVar('VITE_API_URL', 'http://localhost:4402'),
WS_URL: getEnvVar('VITE_WS_URL'),
ENVIRONMENT: (getEnvVar('VITE_ENV', 'development') as EnvConfig['ENVIRONMENT']),
ENABLE_DEV_TOOLS: getEnvVar('VITE_ENABLE_DEV_TOOLS', 'false') === 'true',
};
export const isDevelopment = env.ENVIRONMENT === 'development';
export const isProduction = env.ENVIRONMENT === 'production';
```
### Example 5: Route Configuration
```typescript
// app/router/routes.tsx
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';
import { PublicLayout } from '@/layouts/PublicLayout';
import { DashboardLayout } from '@/layouts/DashboardLayout';
import { ProtectedRoute } from '@/app/router/guards';
// Lazy load pages for code splitting
const HomePage = lazy(() => import('@/pages/public/HomePage'));
const LoginPage = lazy(() => import('@/pages/public/LoginPage'));
const DashboardPage = lazy(() => import('@/pages/protected/DashboardPage'));
const WorkflowsPage = lazy(() => import('@/pages/protected/WorkflowsPage'));
const NotFoundPage = lazy(() => import('@/pages/public/NotFoundPage'));
export const routes: RouteObject[] = [
{
path: '/',
element: <PublicLayout />,
children: [
{ index: true, element: <HomePage /> },
{ path: 'login', element: <LoginPage /> },
{ path: 'about', element: <div>About</div> },
],
},
{
path: '/dashboard',
element: (
<ProtectedRoute requiredPermission="dashboard.access">
<DashboardLayout />
</ProtectedRoute>
),
children: [
{ index: true, element: <DashboardPage /> },
{
path: 'workflows',
element: (
<ProtectedRoute requiredPermission="workflows.view">
<WorkflowsPage />
</ProtectedRoute>
),
},
],
},
{ path: '*', element: <NotFoundPage /> },
];
```
### Example 6: Error Boundary
```typescript
// app/providers/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Button } from '@/shared/components/ui/button';
import { AlertTriangle } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
// Log to error reporting service
}
private handleReset = () => {
this.setState({ hasError: false, error: null });
};
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="text-center">
<AlertTriangle className="mx-auto h-12 w-12 text-destructive" />
<h1 className="mt-4 text-2xl font-bold">Something went wrong</h1>
<p className="mt-2 text-muted-foreground">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<Button onClick={this.handleReset} className="mt-4">
Try again
</Button>
</div>
</div>
);
}
return this.props.children;
}
}
```
---
## Benefits
1. **Scalability**: Easy to add new features
2. **Maintainability**: Clear structure, easy to find code
3. **Type Safety**: Full TypeScript coverage
4. **Testability**: Easy to test components and logic
5. **Performance**: Code splitting, lazy loading
6. **Developer Experience**: Clear patterns, good DX
7. **Reusability**: Shared components and utilities
---
## Migration Strategy
### Phase 1: Foundation (Week 1)
1. Set up new directory structure
2. Create API client
3. Set up environment configuration
4. Create shared utilities
### Phase 2: Features (Week 2-3)
1. Migrate auth feature
2. Migrate workflows feature
3. Migrate documents feature
4. Migrate compliance feature
### Phase 3: Refinement (Week 4)
1. Add error boundaries
2. Improve error handling
3. Add loading states
4. Optimize performance
### Phase 4: Testing & Polish (Week 5)
1. Write tests
2. Add documentation
3. Performance optimization
4. Final review
---
## Next Steps
1. Review and approve architecture
2. Create detailed implementation plan
3. Set up project structure
4. Begin Phase 1 implementation