Skip to content

Latest commit

 

History

History
1494 lines (1170 loc) · 34.3 KB

File metadata and controls

1494 lines (1170 loc) · 34.3 KB

Virtual Office - Developer Guide

Version: 1.0 Last Updated: February 7, 2026

This guide provides comprehensive technical information for developers who want to contribute to Virtual Office, deploy it, or integrate it into their systems.


Table of Contents

  1. Architecture Overview
  2. Development Setup
  3. Project Structure
  4. Technology Stack
  5. Core Components
  6. API Reference
  7. Testing
  8. Build & Deployment
  9. Contributing
  10. Troubleshooting

Architecture Overview

High-Level Architecture

Virtual Office is a full-stack application with real-time communication between AIOS agents and the frontend UI.

┌─────────────────────────────────────────────────────────┐
│                    Virtual Office UI                    │
│                   (React 18 + Vite)                     │
│                                                         │
│  ┌─────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │   Canvas    │  │  Agent       │  │  Activity    │  │
│  │  Renderer   │  │  Status      │  │  Detail      │  │
│  │  (2D Iso)   │  │  Grid        │  │  Panel       │  │
│  └─────────────┘  └──────────────┘  └──────────────┘  │
│                                                         │
│  ┌─────────────────────────────────────────────────┐  │
│  │         State Management (Zustand)              │  │
│  └─────────────────────────────────────────────────┘  │
└────────────────────┬────────────────────────────────────┘
                     │ WebSocket (Socket.io Client)
                     │
┌────────────────────┴────────────────────────────────────┐
│                  Backend API Server                     │
│                  (Node.js + Express)                    │
│                                                         │
│  ┌─────────────────────────────────────────────────┐  │
│  │         Socket.io Server                        │  │
│  │   (Rooms per project, Broadcasting)             │  │
│  └──────────────┬──────────────────────────────────┘  │
│                 │                                       │
│  ┌──────────────┴──────────────┐  ┌─────────────────┐ │
│  │    AIOS Connector           │  │   SQLite DB     │ │
│  │ (IPC/WebSocket to AIOS)     │  │  (Projects,     │ │
│  │                             │  │   Activities)   │ │
│  └──────────────┬──────────────┘  └─────────────────┘ │
└─────────────────┴─────────────────────────────────────┘
                  │
        ┌─────────┴─────────┐
        │   AIOS Framework  │
        │  (Local Instance) │
        └───────────────────┘

Key Architectural Decisions

See Technical Architecture for detailed ADRs.

Component Technology Rationale
Frontend React 18 + Vite Mature ecosystem, concurrent features for real-time
Rendering Canvas 2D Isometric MVP velocity (3-5x faster than 3D)
State Zustand Simple, performant, real-time friendly
WebSocket Socket.io Auto-reconnect, rooms, broadcasting
Database SQLite (Drizzle ORM) Zero config, portable, easy migration
Testing Vitest + Playwright Fast, Vite-native, multi-browser

Data Flow

AIOS Agent Action
      ↓
AIOS Connector (Backend)
      ↓
Store in SQLite
      ↓
Broadcast via Socket.io
      ↓
Zustand Store (Frontend)
      ↓
React Components Re-render
      ↓
Canvas Updates (Visual)

Development Setup

Prerequisites

  • Node.js: 18.0.0 or higher
  • npm: 9.0.0 or higher
  • Git: Latest version
  • AIOS Framework: 2.0+ (for testing integration)
  • Code Editor: VS Code recommended

Recommended VS Code Extensions

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss",
    "ms-vscode.vscode-typescript-next"
  ]
}

Initial Setup

  1. Clone the repository
git clone https://github.com/synkra/virtual-office.git
cd virtual-office
  1. Install dependencies
# Install all workspace dependencies
npm install

This installs dependencies for:

  • Root workspace
  • Frontend package
  • Backend package
  • Shared types package
  1. Configure environment variables
# Backend configuration
cp backend/.env.example backend/.env

Edit backend/.env:

# Server Configuration
PORT=3001
NODE_ENV=development

# Frontend URL (for CORS)
FRONTEND_URL=http://localhost:3000

# AIOS Connection
AIOS_WS_URL=ws://localhost:8080

# Database
DB_PATH=./data/virtual-office.db

# License (for testing Enterprise features)
LICENSE_SECRET_KEY=your-secret-key-here

# Optional: Monitoring
SENTRY_DSN=
POSTHOG_KEY=
  1. Initialize database
cd backend
npm run db:generate  # Generate Drizzle schema
npm run db:push      # Apply to SQLite
  1. Start development servers
# From root directory
npm run dev

This starts:

  1. Verify setup

Open http://localhost:3000 in your browser. You should see the Virtual Office UI.

Development Workflow

# Start both frontend + backend in watch mode
npm run dev

# Run tests (all workspaces)
npm test

# Run tests in watch mode
npm run test:watch

# Lint code
npm run lint

# Format code
npm run format

# Type checking
npm run typecheck

# Build for production
npm run build

Git Hooks (Husky)

Pre-configured hooks:

  • pre-commit: Runs npm run lint to catch style issues
  • pre-push: Runs npm test to prevent broken code

To skip hooks (not recommended):

git commit --no-verify

Project Structure

Monorepo Layout

virtual-office/
├── .github/              # GitHub Actions workflows
│   └── workflows/
│       ├── ci.yml        # Continuous integration
│       └── deploy.yml    # Deployment pipeline
│
├── frontend/             # React application
│   ├── public/           # Static assets
│   ├── src/
│   │   ├── components/   # React components (Atomic Design)
│   │   │   ├── ui/       # Atoms (buttons, inputs)
│   │   │   ├── AgentWorkspace.tsx
│   │   │   ├── AgentDetailPanel.tsx
│   │   │   └── OfficeView.tsx
│   │   ├── stores/       # Zustand state stores
│   │   │   ├── agentStore.ts
│   │   │   ├── projectStore.ts
│   │   │   └── uiStore.ts
│   │   ├── lib/          # Utilities and helpers
│   │   │   ├── socket.ts
│   │   │   └── utils.ts
│   │   ├── hooks/        # Custom React hooks
│   │   ├── types/        # TypeScript types
│   │   └── App.tsx
│   ├── index.html
│   ├── vite.config.ts    # Vite configuration
│   ├── tailwind.config.ts
│   └── package.json
│
├── backend/              # Node.js server
│   ├── src/
│   │   ├── routes/       # Express routes
│   │   │   ├── projects.ts
│   │   │   └── activities.ts
│   │   ├── services/     # Business logic
│   │   │   ├── aiosConnector.ts
│   │   │   ├── activityService.ts
│   │   │   └── projectService.ts
│   │   ├── db/           # Database layer
│   │   │   ├── schema.ts    # Drizzle schema
│   │   │   ├── migrations/
│   │   │   └── index.ts
│   │   ├── websocket/    # Socket.io handlers
│   │   │   └── handlers.ts
│   │   └── index.ts      # Server entry point
│   ├── .env.example
│   ├── drizzle.config.ts
│   └── package.json
│
├── shared/               # Shared TypeScript types
│   ├── src/
│   │   └── types/
│   │       ├── agent.ts
│   │       ├── project.ts
│   │       └── activity.ts
│   └── package.json
│
├── docs/                 # Documentation
│   ├── architecture/     # Architecture documents and ADRs
│   ├── stories/          # User stories
│   ├── prd.md           # Product requirements
│   ├── USER_GUIDE.md
│   └── DEVELOPER_GUIDE.md
│
├── package.json          # Root package (workspace manager)
├── tsconfig.json         # TypeScript configuration
└── README.md

Frontend Structure (Atomic Design)

frontend/src/components/
├── ui/                   # Atoms - Basic building blocks
│   ├── button.tsx
│   ├── input.tsx
│   ├── dialog.tsx
│   └── dropdown-menu.tsx
│
├── AgentStatus.tsx       # Molecules - Simple components
├── AgentWorkspace.tsx
├── ActivityItem.tsx
│
├── AgentDetailPanel.tsx  # Organisms - Complex components
├── AgentGrid.tsx
├── ProjectSwitcher.tsx
│
├── OfficeView.tsx        # Templates - Page layouts
└── SettingsView.tsx

Technology Stack

Frontend

Package Version Purpose
React 18.2.0 UI framework
TypeScript 5.3.3 Type safety
Vite 5.0.12 Build tool & dev server
Zustand 4.5.7 State management
React Query 5.17.19 Server state & caching
Socket.io Client 4.8.3 WebSocket client
Tailwind CSS 3.4.1 Styling
Radix UI Various Accessible components
Lucide React 0.314.0 Icons

Backend

Package Version Purpose
Node.js 18+ Runtime
Express 4.18.2 HTTP server
Socket.io 4.6.1 WebSocket server
SQLite via better-sqlite3 Database
Drizzle ORM 0.29.3 Type-safe ORM
TypeScript 5.3.3 Type safety
dotenv 16.4.0 Environment config

Testing

Package Version Purpose
Vitest 1.2.1 Unit & integration tests
Playwright Latest E2E tests
Testing Library 14.1.2 React component tests
jsdom 24.0.0 DOM simulation

Development Tools

  • ESLint: Code linting
  • Prettier: Code formatting
  • Husky: Git hooks
  • tsx: TypeScript execution (backend)
  • Drizzle Kit: Database migrations

Core Components

Frontend Components

1. OfficeView (Main Container)

Purpose: Root component that renders the virtual office

Location: frontend/src/components/OfficeView.tsx

Key Features:

  • Renders agent grid
  • Manages WebSocket connection
  • Handles project switching
  • Opens/closes detail panel

Props:

interface OfficeViewProps {
  projectId: string;
}

State:

const agents = useAgentStore((state) => state.agents);
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);

2. AgentWorkspace

Purpose: Visual representation of a single agent

Location: frontend/src/components/AgentWorkspace.tsx

Props:

interface AgentWorkspaceProps {
  agent: Agent;
  onClick: (agentId: string) => void;
}

Visual States:

  • Online: Green glow, seated agent, active monitor
  • Offline: Gray appearance, empty chair, inactive monitor

3. AgentDetailPanel

Purpose: Sidebar panel showing detailed activity log

Location: frontend/src/components/AgentDetailPanel.tsx

Props:

interface AgentDetailPanelProps {
  agentId: string;
  isOpen: boolean;
  onClose: () => void;
}

Features:

  • Infinite scroll for activity history
  • Real-time activity updates
  • Copy to clipboard
  • Export to JSON/CSV
  • Filtering and search

Implementation Details:

// Fetch activities
const { data: activities } = useQuery({
  queryKey: ['activities', agentId],
  queryFn: () => fetchActivities(agentId)
});

// Real-time updates
useEffect(() => {
  socket.on('agent_update', (update) => {
    if (update.agentId === agentId) {
      // Update activity list
    }
  });
}, [agentId]);

Backend Services

1. AIOS Connector

Purpose: Bridge between AIOS and Virtual Office

Location: backend/src/services/aiosConnector.ts

Key Methods:

class AIOSConnector {
  private ws: WebSocket | null = null;

  // Connect to AIOS instance
  async connect(url: string): Promise<void> {
    this.ws = new WebSocket(url);
    this.ws.on('message', this.handleMessage.bind(this));
    this.ws.on('error', this.handleError.bind(this));
  }

  // Handle incoming AIOS events
  private handleMessage(data: Buffer): void {
    const event: AIOSEvent = JSON.parse(data.toString());
    this.emit('aios_event', event);
  }

  // Disconnect from AIOS
  disconnect(): void {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

Event Types:

interface AIOSEvent {
  type: 'agent_activated' | 'agent_deactivated' | 'task_started'
        | 'task_completed' | 'activity';
  projectId: string;
  agentId: string;
  timestamp: string;
  payload?: {
    activityType?: 'code' | 'command' | 'file_op' | 'tool_call';
    content?: string;
    filePath?: string;
  };
}

2. Activity Service

Purpose: Manage activity log storage and retrieval

Location: backend/src/services/activityService.ts

Key Methods:

class ActivityService {
  // Store new activity
  async createActivity(activity: NewActivity): Promise<Activity> {
    const [inserted] = await db
      .insert(activities)
      .values(activity)
      .returning();
    return inserted;
  }

  // Get activities for agent
  async getActivitiesByAgent(
    agentId: string,
    limit = 50
  ): Promise<Activity[]> {
    return db
      .select()
      .from(activities)
      .where(eq(activities.agentId, agentId))
      .orderBy(desc(activities.timestamp))
      .limit(limit);
  }

  // Filter activities
  async filterActivities(filters: ActivityFilters): Promise<Activity[]> {
    let query = db.select().from(activities);

    if (filters.type) {
      query = query.where(eq(activities.type, filters.type));
    }

    if (filters.startDate) {
      query = query.where(gte(activities.timestamp, filters.startDate));
    }

    return query.orderBy(desc(activities.timestamp)).limit(filters.limit || 50);
  }

  // Cleanup old activities
  async cleanupOldActivities(daysToKeep = 30): Promise<number> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);

    const result = await db
      .delete(activities)
      .where(lt(activities.timestamp, cutoffDate));

    return result.rowsAffected;
  }
}

3. WebSocket Handlers

Purpose: Handle Socket.io connections and broadcasts

Location: backend/src/websocket/handlers.ts

Implementation:

import { Server as SocketIOServer } from 'socket.io';

export function setupWebSocketHandlers(io: SocketIOServer) {
  io.on('connection', (socket) => {
    console.log('Client connected:', socket.id);

    // Subscribe to project
    socket.on('subscribe_project', async ({ projectId }) => {
      socket.join(`project:${projectId}`);

      // Send initial state
      const agents = await getProjectAgents(projectId);
      const activities = await getRecentActivities(projectId);

      socket.emit('initial_state', { agents, activities });
    });

    // Unsubscribe from project
    socket.on('unsubscribe_project', ({ projectId }) => {
      socket.leave(`project:${projectId}`);
    });

    // Handle disconnection
    socket.on('disconnect', () => {
      console.log('Client disconnected:', socket.id);
    });
  });

  // Broadcast agent updates
  aiosConnector.on('aios_event', async (event: AIOSEvent) => {
    // Store in database
    await activityService.createActivity({
      projectId: event.projectId,
      agentId: event.agentId,
      type: event.payload?.activityType || 'unknown',
      content: event.payload?.content,
      filePath: event.payload?.filePath,
      timestamp: new Date(event.timestamp)
    });

    // Broadcast to clients
    io.to(`project:${event.projectId}`).emit('agent_update', event);
  });
}

State Management (Zustand)

Agent Store

Location: frontend/src/stores/agentStore.ts

interface AgentState {
  agents: Record<string, Agent>;
  updateAgent: (id: string, update: Partial<Agent>) => void;
  setAgents: (agents: Agent[]) => void;
  getAgent: (id: string) => Agent | undefined;
}

export const useAgentStore = create<AgentState>((set, get) => ({
  agents: {},

  updateAgent: (id, update) =>
    set((state) => ({
      agents: {
        ...state.agents,
        [id]: { ...state.agents[id], ...update }
      }
    })),

  setAgents: (agents) =>
    set({
      agents: Object.fromEntries(agents.map(a => [a.id, a]))
    }),

  getAgent: (id) => get().agents[id]
}));

Project Store

Location: frontend/src/stores/projectStore.ts

interface ProjectState {
  projects: Project[];
  activeProjectId: string | null;
  setProjects: (projects: Project[]) => void;
  setActiveProject: (id: string) => void;
  addProject: (project: Project) => void;
  removeProject: (id: string) => void;
}

export const useProjectStore = create<ProjectState>()(
  persist(
    (set) => ({
      projects: [],
      activeProjectId: null,

      setProjects: (projects) => set({ projects }),

      setActiveProject: (id) => set({ activeProjectId: id }),

      addProject: (project) =>
        set((state) => ({
          projects: [...state.projects, project]
        })),

      removeProject: (id) =>
        set((state) => ({
          projects: state.projects.filter(p => p.id !== id),
          activeProjectId: state.activeProjectId === id
            ? null
            : state.activeProjectId
        }))
    }),
    {
      name: 'virtual-office-projects' // localStorage key
    }
  )
);

API Reference

REST API Endpoints

Base URL: http://localhost:3001

Projects

GET /api/projects

// Get all projects
Response: {
  projects: Project[]
}

POST /api/projects

// Add new project
Request: {
  name: string;
  path: string;
}
Response: {
  project: Project
}

DELETE /api/projects/:id

// Remove project
Response: {
  success: boolean
}

Activities

GET /api/activities/:projectId

// Get activities for project
Query params:
  - agentId?: string
  - type?: 'code' | 'command' | 'file_op' | 'tool_call'
  - limit?: number (default: 50)
  - offset?: number

Response: {
  activities: Activity[];
  total: number;
}

GET /api/activities/agent/:agentId

// Get activities for specific agent
Response: {
  activities: Activity[]
}

WebSocket Events

Client → Server

subscribe_project

socket.emit('subscribe_project', {
  projectId: string
});

unsubscribe_project

socket.emit('unsubscribe_project', {
  projectId: string
});

Server → Client

initial_state

socket.on('initial_state', (data: {
  agents: Agent[];
  activities: Activity[];
}) => {
  // Handle initial state
});

agent_update

socket.on('agent_update', (event: AIOSEvent) => {
  // Handle agent status or activity update
});

connection_status

socket.on('connection_status', (status: {
  aiosConnected: boolean;
  message?: string;
}) => {
  // Handle AIOS connection status
});

Testing

Running Tests

# All tests
npm test

# Watch mode
npm run test:watch

# Coverage report
npm run test:coverage

# Frontend only
npm test --workspace=frontend

# Backend only
npm test --workspace=backend

Test Structure

frontend/src/
├── components/
│   ├── AgentWorkspace.tsx
│   └── AgentWorkspace.test.tsx
├── stores/
│   ├── agentStore.ts
│   └── agentStore.test.ts
└── lib/
    ├── utils.ts
    └── utils.test.ts

backend/src/
├── services/
│   ├── activityService.ts
│   └── activityService.test.ts
└── websocket/
    ├── handlers.ts
    └── handlers.test.ts

Writing Unit Tests (Vitest)

Frontend Component Test:

import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { AgentWorkspace } from './AgentWorkspace';

describe('AgentWorkspace', () => {
  it('renders agent name', () => {
    const agent = {
      id: 'dev-1',
      name: 'Dev Agent',
      role: 'dev',
      status: 'online'
    };

    render(<AgentWorkspace agent={agent} onClick={() => {}} />);

    expect(screen.getByText('Dev Agent')).toBeInTheDocument();
  });

  it('shows online status', () => {
    const agent = {
      id: 'dev-1',
      name: 'Dev Agent',
      role: 'dev',
      status: 'online'
    };

    render(<AgentWorkspace agent={agent} onClick={() => {}} />);

    const workspace = screen.getByRole('button');
    expect(workspace).toHaveClass('online');
  });
});

Backend Service Test:

import { describe, it, expect, beforeEach } from 'vitest';
import { ActivityService } from './activityService';

describe('ActivityService', () => {
  let service: ActivityService;

  beforeEach(() => {
    // Use in-memory database for tests
    service = new ActivityService(':memory:');
  });

  it('creates activity', async () => {
    const activity = await service.createActivity({
      projectId: 'proj-1',
      agentId: 'dev-1',
      type: 'code',
      content: 'console.log("test")',
      timestamp: new Date()
    });

    expect(activity.id).toBeDefined();
    expect(activity.type).toBe('code');
  });

  it('filters activities by type', async () => {
    // Create test activities
    await service.createActivity({
      projectId: 'proj-1',
      agentId: 'dev-1',
      type: 'code',
      content: 'test code',
      timestamp: new Date()
    });

    await service.createActivity({
      projectId: 'proj-1',
      agentId: 'dev-1',
      type: 'command',
      content: 'npm test',
      timestamp: new Date()
    });

    // Filter
    const codeActivities = await service.filterActivities({
      type: 'code'
    });

    expect(codeActivities).toHaveLength(1);
    expect(codeActivities[0].type).toBe('code');
  });
});

Writing E2E Tests (Playwright)

Location: tests/e2e/

import { test, expect } from '@playwright/test';

test.describe('Virtual Office', () => {
  test('connects to project and monitors agents', async ({ page }) => {
    // Navigate to app
    await page.goto('http://localhost:3000');

    // Add project
    await page.click('[data-testid="add-project-button"]');
    await page.fill('[data-testid="project-path"]', '/path/to/test-project');
    await page.click('[data-testid="connect-button"]');

    // Verify connection
    await expect(page.locator('[data-testid="status-indicator"]'))
      .toHaveText('Connected');

    // Verify agent grid visible
    await expect(page.locator('[data-testid="agent-grid"]')).toBeVisible();

    // Check agent appears online
    const devAgent = page.locator('[data-agent="dev"]');
    await expect(devAgent).toHaveClass(/online/);

    // Click agent to open detail panel
    await devAgent.click();

    // Verify detail panel opens
    await expect(page.locator('[data-testid="agent-detail-panel"]'))
      .toBeVisible();

    // Verify activity log visible
    await expect(page.locator('[data-testid="activity-log"]'))
      .toBeVisible();
  });

  test('filters activity by type', async ({ page }) => {
    await page.goto('http://localhost:3000');

    // Open agent detail panel
    await page.click('[data-agent="dev"]');

    // Select filter type
    await page.click('[data-testid="type-filter"]');
    await page.click('[data-testid="filter-code"]');

    // Verify only code activities shown
    const activities = await page.locator('[data-testid="activity-item"]').all();
    for (const activity of activities) {
      await expect(activity.locator('[data-testid="activity-type"]'))
        .toHaveText('code');
    }
  });
});

Test Coverage Goals

Area Target Coverage
Backend Services 80%+
Frontend Components 60%+
State Stores 90%+
Utilities 95%+

Build & Deployment

Production Build

# Build all packages
npm run build

# Output:
# frontend/dist/     - Static files for deployment
# backend/dist/      - Compiled JavaScript

Environment Variables (Production)

Backend (.env.production):

NODE_ENV=production
PORT=3001
FRONTEND_URL=https://virtual-office.synkra.com
AIOS_WS_URL=wss://aios-server.synkra.com
DB_PATH=/var/data/virtual-office.db
LICENSE_SECRET_KEY=<production-secret>
SENTRY_DSN=<sentry-dsn>

Frontend (build-time):

VITE_API_URL=https://api.virtual-office.synkra.com
VITE_WS_URL=wss://api.virtual-office.synkra.com
VITE_SENTRY_DSN=<sentry-dsn>
VITE_POSTHOG_KEY=<posthog-key>

Deployment Options

Option 1: Web App (Recommended)

Frontend: Deploy to Vercel/Netlify

# Install Vercel CLI
npm i -g vercel

# Deploy frontend
cd frontend
vercel --prod

Backend: Deploy to Railway/Fly.io

# Install Railway CLI
npm i -g @railway/cli

# Deploy backend
cd backend
railway up

Option 2: Docker Containers

Dockerfile (backend):

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
COPY backend/package*.json ./backend/

RUN npm ci --workspace=backend

COPY backend/ ./backend/
COPY shared/ ./shared/

RUN npm run build --workspace=backend

EXPOSE 3001

CMD ["node", "backend/dist/index.js"]

docker-compose.yml:

version: '3.8'

services:
  frontend:
    build:
      context: .
      dockerfile: frontend/Dockerfile
    ports:
      - "3000:80"
    environment:
      - VITE_API_URL=http://backend:3001

  backend:
    build:
      context: .
      dockerfile: backend/Dockerfile
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=production
      - DB_PATH=/data/virtual-office.db
    volumes:
      - vo-data:/data

volumes:
  vo-data:

Run with:

docker-compose up -d

Option 3: Self-Hosted

Requirements:

  • Linux server (Ubuntu 22.04 recommended)
  • Node.js 18+
  • Nginx (for reverse proxy)
  • PM2 (for process management)

Setup:

# Install PM2
npm install -g pm2

# Clone and build
git clone https://github.com/synkra/virtual-office.git
cd virtual-office
npm install
npm run build

# Start backend with PM2
cd backend
pm2 start npm --name "virtual-office-backend" -- start

# Serve frontend with Nginx
sudo cp -r frontend/dist/* /var/www/virtual-office/

Nginx Configuration:

server {
    listen 80;
    server_name virtual-office.example.com;

    location / {
        root /var/www/virtual-office;
        try_files $uri /index.html;
    }

    location /api {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
    }

    location /socket.io {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
    }
}

CI/CD Pipeline

GitHub Actions (.github/workflows/ci.yml):

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run typecheck

      - name: Unit tests
        run: npm test

      - name: E2E tests
        run: npm run test:e2e

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

  build:
    needs: test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: |
            frontend/dist/
            backend/dist/

Contributing

Getting Started

  1. Fork the repository
  2. Clone your fork
  3. Create a feature branch: git checkout -b feature/your-feature
  4. Make your changes
  5. Test thoroughly: npm test && npm run lint
  6. Commit: git commit -m "feat: add your feature"
  7. Push: git push origin feature/your-feature
  8. Open a Pull Request

Commit Convention

We follow Conventional Commits:

<type>(<scope>): <subject>

<body>

<footer>

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation only
  • style: Code style (formatting, no logic change)
  • refactor: Code refactoring
  • perf: Performance improvement
  • test: Adding or updating tests
  • chore: Maintenance tasks

Examples:

feat(frontend): add activity export to CSV

Implements CSV export functionality for agent activity logs.
Users can now export filtered activities for external analysis.

Closes #123

Code Style

  • TypeScript: Use strict mode, avoid any
  • React: Functional components with hooks
  • Naming:
    • Components: PascalCase (AgentWorkspace.tsx)
    • Functions: camelCase (getAgentStatus())
    • Constants: UPPER_SNAKE_CASE (MAX_ACTIVITIES)
  • Formatting: Prettier (runs on pre-commit)
  • Imports: Group by external → internal → relative

Pull Request Checklist

  • Tests added/updated and passing
  • Linting passes (npm run lint)
  • Type checking passes (npm run typecheck)
  • Documentation updated (if needed)
  • Commit messages follow convention
  • PR description explains changes
  • No merge conflicts

Review Process

  1. Automated checks run (CI pipeline)
  2. Code review by maintainers
  3. Requested changes addressed
  4. Approval from 1+ maintainers
  5. Merge to main branch

Troubleshooting for Developers

Build Issues

Error: Cannot find module '@virtual-office/shared'

Solution:

# Rebuild workspace links
npm install

# Verify workspaces
npm list --workspaces

Error: Type error: Cannot find type definitions

Solution:

# Regenerate tsconfig paths
npm run typecheck

Database Issues

Error: Database locked

Solution:

# Close all connections and restart backend
pm2 restart virtual-office-backend

# Or reset database (development only)
rm backend/data/virtual-office.db
npm run db:push --workspace=backend

Error: Migration failed

Solution:

# Generate new migration
npm run db:generate --workspace=backend

# Apply manually
npm run db:push --workspace=backend

WebSocket Issues

Error: WebSocket connection failed

Solution:

  1. Check backend is running: curl http://localhost:3001/health
  2. Verify Socket.io endpoint: curl http://localhost:3001/socket.io/
  3. Check CORS settings in backend/src/index.ts
  4. Inspect browser console for errors

Error: AIOS connector timeout

Solution:

  1. Verify AIOS is running: ps aux | grep aios
  2. Check AIOS WebSocket URL in .env
  3. Test connection manually:
    wscat -c ws://localhost:8080

Performance Issues

Problem: High CPU usage during development

Solution:

# Disable hot reload for backend
cd backend
NODE_ENV=development node dist/index.js

# Or adjust Vite HMR
# In vite.config.ts:
server: {
  hmr: {
    overlay: false
  }
}

Problem: Memory leak in long-running tests

Solution:

// Add cleanup in tests
afterEach(() => {
  // Close WebSocket connections
  socket?.disconnect();

  // Clear stores
  useAgentStore.getState().setAgents([]);
});

Additional Resources

Documentation

External Links

Support


Virtual Office Developer Guide v1.0 © 2026 Synkra AIOS Team. Licensed under MIT License.