Files
sciagent/fe0/FRONTEND_ARCHITECTURE.md
T
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

19 KiB

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

// 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

// 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

// 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

// 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

// 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

// 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