Date: 2025-22-09

Status: accepted

Current Architecture Analysis

The Vega web application is built with Remix (React Router v7) and follows a feature-based routing structure with the following characteristics:

Current Structure

services/remix/
├── app/
│   ├── components/           # Shared components
│   │   ├── ui/              # Design system components (shadcn/ui based)
│   │   ├── chat/            # Chat-related components
│   │   ├── forms/           # Form components
│   │   └── shared/          # Shared business components
│   ├── routes/              # Route-based organization
│   │   ├── alerts+/         # Alerts feature
│   │   ├── dashboards+/     # Dashboards feature
│   │   ├── detections+/     # Detections feature
│   │   ├── assessment+/       # Data assessment feature
│   │   ├── intel+/          # Intelligence feature
│   │   ├── notebooks+/      # Notebooks feature
│   │   ├── query+/          # Query feature
│   │   ├── settings+/       # Settings feature
│   │   └── _auth+/          # Authentication
│   ├── queries/             # GraphQL queries
│   ├── utils/               # Utility functions
│   └── transformations/     # Data transformations

Key Technologies

  • Framework: Remix (React Router v7) with Vite
  • UI Library: shadcn/ui components + Radix UI primitives
  • State Management: Zustand + React Query
  • Styling: Tailwind CSS
  • Data Fetching: GraphQL with custom hooks
  • Testing: Vitest + Playwright

MVC Pattern Migration

What is MVC?

Model-View-Controller (MVC) is an architectural pattern that separates an application into three interconnected components:

  • Model: Manages data and business logic
  • View: Handles user interface and presentation
  • Controller: Manages user input and coordinates between Model and View

MVC Structure for React/Remix

src/
├── models/                  # Data and business logic
│   ├── User.ts             # User model
│   ├── Detection.ts         # Detection model
│   ├── Dashboard.ts        # Dashboard model
│   └── index.ts           # Model exports
├── views/                  # UI components (View layer)
│   ├── components/         # Reusable UI components
│   ├── pages/             # Page components
│   └── layouts/           # Layout components
├── controllers/            # Business logic and state management
│   ├── UserController.ts   # User business logic
│   ├── DetectionController.ts
│   ├── DashboardController.ts
│   └── index.ts
├── services/              # External services and API calls
│   ├── api/               # API services
│   ├── auth/              # Authentication services
│   └── index.ts
└── utils/                 # Utility functions

Migration Strategy

Phase 1: Extract Models

Step 1: User Model
// src/models/User.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: string;
  permissions: string[];
  avatar?: string;
  createdAt: string;
  updatedAt: string;
}

export class UserModel {
  private static instance: UserModel;
  private user: User | null = null;
  private listeners: Array<(user: User | null) => void> = [];

  static getInstance(): UserModel {
    if (!UserModel.instance) {
      UserModel.instance = new UserModel();
    }
    return UserModel.instance;
  }

  setUser(user: User | null): void {
    this.user = user;
    this.notifyListeners();
  }

  getUser(): User | null {
    return this.user;
  }

  isAuthenticated(): boolean {
    return this.user !== null;
  }

  hasPermission(permission: string): boolean {
    return this.user?.permissions.includes(permission) ?? false;
  }

  subscribe(listener: (user: User | null) => void): () => void {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  private notifyListeners(): void {
    this.listeners.forEach(listener => listener(this.user));
  }
}
Step 2: Detection Model
// src/models/Detection.ts
export interface Detection {
  id: string;
  name: string;
  description: string;
  query: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
  status: 'active' | 'inactive' | 'draft';
  mitreTechniques: string[];
  createdAt: string;
  updatedAt: string;
  createdBy: string;
}

export class DetectionModel {
  private static instance: DetectionModel;
  private detections: Detection[] = [];
  private currentDetection: Detection | null = null;
  private listeners: Array<(detections: Detection[]) => void> = [];

  static getInstance(): DetectionModel {
    if (!DetectionModel.instance) {
      DetectionModel.instance = new DetectionModel();
    }
    return DetectionModel.instance;
  }

  setDetections(detections: Detection[]): void {
    this.detections = detections;
    this.notifyListeners();
  }

  getDetections(): Detection[] {
    return this.detections;
  }

  getDetectionById(id: string): Detection | undefined {
    return this.detections.find(d => d.id === id);
  }

  setCurrentDetection(detection: Detection | null): void {
    this.currentDetection = detection;
  }

  getCurrentDetection(): Detection | null {
    return this.currentDetection;
  }

  addDetection(detection: Detection): void {
    this.detections.push(detection);
    this.notifyListeners();
  }

  updateDetection(id: string, updates: Partial<Detection>): void {
    const index = this.detections.findIndex(d => d.id === id);
    if (index !== -1) {
      this.detections[index] = { ...this.detections[index], ...updates };
      this.notifyListeners();
    }
  }

  deleteDetection(id: string): void {
    this.detections = this.detections.filter(d => d.id !== id);
    this.notifyListeners();
  }

  subscribe(listener: (detections: Detection[]) => void): () => void {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  private notifyListeners(): void {
    this.listeners.forEach(listener => listener(this.detections));
  }
}

Phase 2: Extract Controllers

Step 1: User Controller
// src/controllers/UserController.ts
import { UserModel, type User } from '../models/User';
import { authService } from '../services/authService';
import { userService } from '../services/userService';

export class UserController {
  private userModel: UserModel;

  constructor() {
    this.userModel = UserModel.getInstance();
  }

  async login(credentials: { email: string; password: string }): Promise<void> {
    try {
      const { token, user } = await authService.login(credentials);
      localStorage.setItem('token', token);
      this.userModel.setUser(user);
    } catch (error) {
      throw new Error('Login failed');
    }
  }

  async logout(): Promise<void> {
    try {
      await authService.logout();
      localStorage.removeItem('token');
      this.userModel.setUser(null);
    } catch (error) {
      console.error('Logout error:', error);
    }
  }

  async getCurrentUser(): Promise<User | null> {
    try {
      const user = await userService.getCurrentUser();
      this.userModel.setUser(user);
      return user;
    } catch (error) {
      console.error('Get current user error:', error);
      return null;
    }
  }

  async updateProfile(updates: Partial<User>): Promise<void> {
    try {
      const updatedUser = await userService.updateProfile(updates);
      this.userModel.setUser(updatedUser);
    } catch (error) {
      throw new Error('Profile update failed');
    }
  }

  isAuthenticated(): boolean {
    return this.userModel.isAuthenticated();
  }

  hasPermission(permission: string): boolean {
    return this.userModel.hasPermission(permission);
  }

  subscribeToUserChanges(callback: (user: User | null) => void): () => void {
    return this.userModel.subscribe(callback);
  }
}
Step 2: Detection Controller
// src/controllers/DetectionController.ts
import { DetectionModel, type Detection } from '../models/Detection';
import { detectionService } from '../services/detectionService';

export class DetectionController {
  private detectionModel: DetectionModel;

  constructor() {
    this.detectionModel = DetectionModel.getInstance();
  }

  async loadDetections(): Promise<void> {
    try {
      const detections = await detectionService.getDetections();
      this.detectionModel.setDetections(detections);
    } catch (error) {
      throw new Error('Failed to load detections');
    }
  }

  async createDetection(detectionData: Omit<Detection, 'id' | 'createdAt' | 'updatedAt'>): Promise<Detection> {
    try {
      const newDetection = await detectionService.createDetection(detectionData);
      this.detectionModel.addDetection(newDetection);
      return newDetection;
    } catch (error) {
      throw new Error('Failed to create detection');
    }
  }

  async updateDetection(id: string, updates: Partial<Detection>): Promise<void> {
    try {
      await detectionService.updateDetection(id, updates);
      this.detectionModel.updateDetection(id, updates);
    } catch (error) {
      throw new Error('Failed to update detection');
    }
  }

  async deleteDetection(id: string): Promise<void> {
    try {
      await detectionService.deleteDetection(id);
      this.detectionModel.deleteDetection(id);
    } catch (error) {
      throw new Error('Failed to delete detection');
    }
  }

  setCurrentDetection(detection: Detection | null): void {
    this.detectionModel.setCurrentDetection(detection);
  }

  getCurrentDetection(): Detection | null {
    return this.detectionModel.getCurrentDetection();
  }

  getDetections(): Detection[] {
    return this.detectionModel.getDetections();
  }

  getDetectionById(id: string): Detection | undefined {
    return this.detectionModel.getDetectionById(id);
  }

  subscribeToDetections(callback: (detections: Detection[]) => void): () => void {
    return this.detectionModel.subscribe(callback);
  }
}

Phase 3: Extract Views

Step 1: User View Components
// src/views/components/UserProfile.tsx
import React, { useEffect, useState } from 'react';
import { userController } from '../controllers';
import type { User } from '../../models/User';

export function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = userController.subscribeToUserChanges(setUser);
    
    const loadUser = async () => {
      setLoading(true);
      try {
        await userController.getCurrentUser();
      } finally {
        setLoading(false);
      }
    };

    loadUser();

    return unsubscribe;
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <div>No user data</div>;
  }

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>Role: {user.role}</p>
    </div>
  );
}

// src/views/components/LoginForm.tsx
import React, { useState } from 'react';
import { userController } from '../controllers';

export function LoginForm() {
  const [credentials, setCredentials] = useState({ email: '', password: '' });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      await userController.login(credentials);
    } catch (err) {
      setError('Login failed');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={credentials.email}
        onChange={(e) => setCredentials(prev => ({ ...prev, email: e.target.value }))}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={credentials.password}
        onChange={(e) => setCredentials(prev => ({ ...prev, password: e.target.value }))}
        placeholder="Password"
        required
      />
      {error && <div className="error">{error}</div>}
      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}
Step 2: Detection View Components
// src/views/components/DetectionList.tsx
import React, { useEffect, useState } from 'react';
import { detectionController } from '../controllers';
import type { Detection } from '../../models/Detection';

export function DetectionList() {
  const [detections, setDetections] = useState<Detection[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = detectionController.subscribeToDetections(setDetections);
    
    const loadDetections = async () => {
      setLoading(true);
      try {
        await detectionController.loadDetections();
      } finally {
        setLoading(false);
      }
    };

    loadDetections();

    return unsubscribe;
  }, []);

  const handleDelete = async (id: string) => {
    try {
      await detectionController.deleteDetection(id);
    } catch (error) {
      console.error('Delete failed:', error);
    }
  };

  if (loading) {
    return <div>Loading detections...</div>;
  }

  return (
    <div className="detection-list">
      <h2>Detections</h2>
      {detections.map(detection => (
        <div key={detection.id} className="detection-item">
          <h3>{detection.name}</h3>
          <p>{detection.description}</p>
          <span className={`severity ${detection.severity}`}>
            {detection.severity}
          </span>
          <button onClick={() => handleDelete(detection.id)}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

// src/views/components/CreateDetectionForm.tsx
import React, { useState } from 'react';
import { detectionController } from '../controllers';

export function CreateDetectionForm() {
  const [formData, setFormData] = useState({
    name: '',
    description: '',
    query: '',
    severity: 'medium' as const,
  });
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    try {
      await detectionController.createDetection({
        ...formData,
        status: 'draft',
        mitreTechniques: [],
        createdBy: 'current-user-id',
      });
      setFormData({ name: '', description: '', query: '', severity: 'medium' });
    } catch (error) {
      console.error('Create detection failed:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
        placeholder="Detection Name"
        required
      />
      <textarea
        value={formData.description}
        onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
        placeholder="Description"
        required
      />
      <textarea
        value={formData.query}
        onChange={(e) => setFormData(prev => ({ ...prev, query: e.target.value }))}
        placeholder="Query"
        required
      />
      <select
        value={formData.severity}
        onChange={(e) => setFormData(prev => ({ ...prev, severity: e.target.value as any }))}
      >
        <option value="low">Low</option>
        <option value="medium">Medium</option>
        <option value="high">High</option>
        <option value="critical">Critical</option>
      </select>
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create Detection'}
      </button>
    </form>
  );
}

Phase 4: Extract Services

Step 1: Authentication Service
// src/services/authService.ts
import { api } from './api';
import type { User } from '../models/User';

export interface LoginCredentials {
  email: string;
  password: string;
}

export interface LoginResponse {
  token: string;
  user: User;
}

export const authService = {
  async login(credentials: LoginCredentials): Promise<LoginResponse> {
    const response = await api.post('/auth/login', credentials);
    return response.data;
  },

  async logout(): Promise<void> {
    await api.post('/auth/logout');
  },

  async refreshToken(): Promise<string> {
    const response = await api.post('/auth/refresh');
    return response.data.token;
  },

  async validateToken(token: string): Promise<boolean> {
    try {
      await api.get('/auth/validate', {
        headers: { Authorization: `Bearer ${token}` }
      });
      return true;
    } catch {
      return false;
    }
  }
};
Step 2: Detection Service
// src/services/detectionService.ts
import { api } from './api';
import type { Detection } from '../models/Detection';

export const detectionService = {
  async getDetections(): Promise<Detection[]> {
    const response = await api.get('/detections');
    return response.data;
  },

  async getDetectionById(id: string): Promise<Detection> {
    const response = await api.get(`/detections/${id}`);
    return response.data;
  },

  async createDetection(data: Omit<Detection, 'id' | 'createdAt' | 'updatedAt'>): Promise<Detection> {
    const response = await api.post('/detections', data);
    return response.data;
  },

  async updateDetection(id: string, updates: Partial<Detection>): Promise<Detection> {
    const response = await api.put(`/detections/${id}`, updates);
    return response.data;
  },

  async deleteDetection(id: string): Promise<void> {
    await api.delete(`/detections/${id}`);
  }
};

Phase 5: Update Remix Routes

Step 1: Update Route Structure
// app/routes/detections+/_index.tsx
import { type LoaderFunctionArgs } from 'react-router';
import { DetectionList } from '@/views/components/DetectionList';
import { CreateDetectionForm } from '@/views/components/CreateDetectionForm';
import { detectionController } from '@/controllers';

export async function loader({ request, context }: LoaderFunctionArgs) {
  // Load initial data
  await detectionController.loadDetections();
  return {};
}

export default function DetectionsPage() {
  return (
    <div>
      <h1>Detections</h1>
      <CreateDetectionForm />
      <DetectionList />
    </div>
  );
}

MVC Benefits

  1. Separation of Concerns: Clear separation between data, logic, and presentation
  2. Maintainability: Easy to modify individual components without affecting others
  3. Testability: Each layer can be tested independently
  4. Reusability: Models and controllers can be reused across different views
  5. Scalability: Easy to add new features following the MVC pattern

Migration Timeline

Week 1-2: Extract models (User, Detection, Dashboard) Week 3-4: Extract controllers (UserController, DetectionController) Week 5-6: Extract views (components and pages) Week 7-8: Extract services (API services, authentication) Week 9-10: Update Remix routes and integration Week 11-12: Testing, optimization, and documentation

Implementation Considerations

  1. State Management: Use models as single source of truth
  2. Event Handling: Implement observer pattern for model updates
  3. Error Handling: Centralize error handling in controllers
  4. Testing: Test each layer independently
  5. Documentation: Document the MVC structure and responsibilities

Potential Challenges

  1. Complexity: MVC can add complexity for simple applications
  2. Boilerplate: More code required for simple operations
  3. Learning Curve: Team needs to understand MVC principles
  4. Performance: Multiple layers might impact performance
  5. Over-engineering: Risk of over-engineering simple features

Success Metrics

  • Code Organization: Percentage of code properly organized by MVC layers
  • Separation of Concerns: Reduction in tightly coupled code
  • Test Coverage: Test coverage for each MVC layer
  • Development Velocity: Time to implement new features
  • Bug Reduction: Reduction in bugs due to better separation of concerns