# Headroom - Architecture & Design

**Version:** 1.0  
**Date:** February 17, 2026  
**Status:** Approved for Implementation  

---

## Table of Contents

1. [System Architecture](#system-architecture)
2. [Data Model](#data-model)
3. [API Architecture](#api-architecture)
4. [Authentication Flow](#authentication-flow)
5. [Deployment Architecture](#deployment-architecture)
6. [Technology Stack Details](#technology-stack-details)
7. [Quality Standards](#quality-standards)
8. [Development Workflow](#development-workflow)

---

## System Architecture

### High-Level Component Diagram

```mermaid
graph TB
    subgraph "Client Layer"
        Browser[Web Browser]
    end
    
    subgraph "Frontend Container :5173"
        SvelteKit[SvelteKit App]
        Store[Svelte Stores]
        Forms[Superforms + Zod]
        Charts[Recharts]
        Tables[TanStack Table]
    end
    
    subgraph "Backend Container :3000"
        API[Laravel API]
        Resources[API Resources]
        Auth[JWT Auth]
        Queue[Queue System]
        Scribe[API Docs - Scribe]
    end
    
    subgraph "Data Layer"
        Postgres[(PostgreSQL)]
        Redis[(Redis Cache)]
    end
    
    subgraph "Reverse Proxy"
        NPM[Nginx Proxy Manager]
    end
    
    Browser -->|HTTPS| NPM
    NPM -->|/api/*| API
    NPM -->|/*| SvelteKit
    
    SvelteKit -->|REST API| API
    SvelteKit --> Store
    SvelteKit --> Forms
    SvelteKit --> Charts
    SvelteKit --> Tables
    
    API --> Resources
    API --> Auth
    API --> Queue
    API --> Scribe
    
    API -->|Read/Write| Postgres
    API -->|Cache| Redis
    Queue -->|Job Storage| Redis
    
    style SvelteKit fill:#ff3e00
    style API fill:#ff2d20
    style Postgres fill:#336791
    style Redis fill:#dc382d
```

### Request Flow

```mermaid
sequenceDiagram
    participant User
    participant Browser
    participant NPM as Nginx Proxy Manager
    participant SvelteKit
    participant Laravel as Laravel API
    participant Redis
    participant DB as PostgreSQL
    
    User->>Browser: Access dashboard
    Browser->>NPM: GET /
    NPM->>SvelteKit: Forward request
    SvelteKit-->>Browser: Return HTML + JS
    
    Browser->>NPM: GET /api/allocations?month=2026-02
    NPM->>Laravel: Forward API request
    Laravel->>Redis: Check cache
    
    alt Cache Hit
        Redis-->>Laravel: Return cached data
    else Cache Miss
        Laravel->>DB: Query allocations
        DB-->>Laravel: Return data
        Laravel->>Redis: Store in cache
    end
    
    Laravel->>Laravel: Transform via API Resources
    Laravel-->>Browser: JSON response
    Browser->>Browser: Render with TanStack Table
```

---

## Data Model

### Entity Relationship Diagram

```mermaid
erDiagram
    TEAM_MEMBER ||--o{ ALLOCATION : "receives"
    TEAM_MEMBER ||--o{ ACTUAL : "logs"
    TEAM_MEMBER ||--o{ PTO : "requests"
    TEAM_MEMBER }o--|| ROLE : "has"
    
    PROJECT ||--o{ ALLOCATION : "requires"
    PROJECT ||--o{ ACTUAL : "tracks"
    PROJECT }o--|| PROJECT_STATUS : "in"
    PROJECT }o--|| PROJECT_TYPE : "categorized_as"
    
    ALLOCATION }o--|| MONTH : "for"
    ACTUAL }o--|| MONTH : "in"
    
    HOLIDAY }o--|| MONTH : "occurs_in"
    PTO }o--|| MONTH : "occurs_in"
    
    TEAM_MEMBER {
        uuid id PK
        string name
        uuid role_id FK
        decimal hourly_rate
        boolean active
        timestamp created_at
        timestamp updated_at
    }
    
    ROLE {
        uuid id PK
        string name
        string description
        timestamp created_at
    }
    
    PROJECT {
        uuid id PK
        string project_code UK
        string title
        uuid status_id FK
        uuid type_id FK
        decimal approved_estimate
        json forecasted_effort
        date start_date
        date end_date
        timestamp created_at
        timestamp updated_at
    }
    
    PROJECT_STATUS {
        uuid id PK
        string name
        int order
        boolean is_active
        boolean is_billable
    }
    
    PROJECT_TYPE {
        uuid id PK
        string name
        string description
    }
    
    ALLOCATION {
        uuid id PK
        uuid project_id FK
        uuid team_member_id FK
        string month
        decimal allocated_hours
        text notes
        timestamp created_at
        timestamp updated_at
    }
    
    ACTUAL {
        uuid id PK
        uuid project_id FK
        uuid team_member_id FK
        string month
        decimal hours_logged
        text notes
        timestamp created_at
        timestamp updated_at
    }
    
    MONTH {
        string id PK
        int year
        int month
        int working_days
    }
    
    HOLIDAY {
        uuid id PK
        date date
        string name
        string description
    }
    
    PTO {
        uuid id PK
        uuid team_member_id FK
        date start_date
        date end_date
        string reason
        string status
        timestamp created_at
    }
```

### Database Schema Details

**Key Design Decisions:**

1. **UUIDs for Primary Keys**
   - Prevents ID enumeration attacks
   - Easier distributed system support (future)
   - No auto-increment exposure

2. **Normalized Master Data**
   - Roles, Statuses, Types in separate tables
   - Allows dynamic configuration without code changes

3. **Month as String (YYYY-MM)**
   - Simplifies queries and grouping
   - Index-friendly
   - Human-readable

4. **JSON for Forecasted Effort**
   - Flexible structure: `{"2026-02": 40, "2026-03": 60}`
   - Easy to extend without schema changes
   - PostgreSQL JSON operators for querying

5. **Soft Deletes**
   - Team members: use `active` boolean (preserve historical allocations)
   - Projects: `deleted_at` timestamp (audit trail)

---

## API Architecture

### REST Endpoint Structure

```mermaid
graph LR
    subgraph "API Routes"
        Auth[/api/auth]
        TeamMembers[/api/team-members]
        Projects[/api/projects]
        Allocations[/api/allocations]
        Actuals[/api/actuals]
        Reports[/api/reports]
        MasterData[/api/master-data]
    end
    
    Auth --> Login[POST /login]
    Auth --> Logout[POST /logout]
    Auth --> Refresh[POST /refresh]
    
    TeamMembers --> TMList[GET /]
    TeamMembers --> TMCreate[POST /]
    TeamMembers --> TMShow[GET /:id]
    TeamMembers --> TMUpdate[PUT /:id]
    TeamMembers --> TMDelete[DELETE /:id]
    
    Projects --> PList[GET /]
    Projects --> PCreate[POST /]
    Projects --> PShow[GET /:id]
    Projects --> PUpdate[PUT /:id]
    Projects --> PDelete[DELETE /:id]
    
    Allocations --> AList[GET /?month=YYYY-MM]
    Allocations --> ACreate[POST /]
    Allocations --> ABulk[POST /bulk]
    Allocations --> AUpdate[PUT /:id]
    Allocations --> ADelete[DELETE /:id]
    
    Actuals --> ACList[GET /?month=YYYY-MM]
    Actuals --> ACCreate[POST /]
    Actuals --> ACBulk[POST /bulk]
    Actuals --> ACUpdate[PUT /:id]
    
    Reports --> RForecast[GET /forecast]
    Reports --> RUtilization[GET /utilization]
    Reports --> RCost[GET /cost]
    Reports --> RAllocation[GET /allocation]
    Reports --> RVariance[GET /variance]
    
    MasterData --> MDRoles[GET /roles]
    MasterData --> MDStatuses[GET /statuses]
    MasterData --> MDTypes[GET /types]
```

### API Resource Transformations

**Laravel API Resources ensure consistent JSON structure:**

```json
// Team Member Resource
{
  "data": {
    "id": "uuid",
    "name": "John Doe",
    "role": {
      "id": "uuid",
      "name": "Backend Developer"
    },
    "hourly_rate": 150.00,
    "active": true,
    "created_at": "2026-02-01T10:00:00Z"
  }
}

// Allocation Resource
{
  "data": {
    "id": "uuid",
    "project": {
      "id": "uuid",
      "code": "PROJ-001",
      "title": "Client Dashboard Redesign"
    },
    "team_member": {
      "id": "uuid",
      "name": "Jane Smith"
    },
    "month": "2026-02",
    "allocated_hours": 40.0,
    "notes": "Focus on frontend work",
    "created_at": "2026-02-01T10:00:00Z"
  }
}

// Report Resource (Forecast)
{
  "data": {
    "period": {
      "from": "2026-02",
      "to": "2026-05"
    },
    "projects": [
      {
        "id": "uuid",
        "code": "PROJ-001",
        "title": "Client Dashboard Redesign",
        "approved_estimate": 120.0,
        "forecast_by_month": {
          "2026-02": 40.0,
          "2026-03": 60.0,
          "2026-04": 20.0
        },
        "allocated_by_month": {
          "2026-02": 38.0,
          "2026-03": 62.0,
          "2026-04": 20.0
        },
        "variance": {
          "2026-02": -2.0,
          "2026-03": 2.0,
          "2026-04": 0
        },
        "status": "under_allocated"
      }
    ],
    "summary": {
      "total_approved": 120.0,
      "total_allocated": 120.0,
      "variance": 0
    }
  }
}
```

### Validation & Error Responses

**Laravel Form Requests handle validation:**

```php
// Example: AllocationRequest
public function rules(): array
{
    return [
        'project_id' => 'required|exists:projects,id',
        'team_member_id' => 'required|exists:team_members,id',
        'month' => 'required|date_format:Y-m',
        'allocated_hours' => 'required|numeric|min:0|max:9999.99',
        'notes' => 'nullable|string|max:1000',
    ];
}
```

**Error Response Format:**

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "allocated_hours": [
      "The allocated hours must be at least 0."
    ],
    "month": [
      "The month field must be in YYYY-MM format."
    ]
  }
}
```

### Caching Strategy

```mermaid
graph TD
    Request[API Request] --> CheckCache{Cache Exists?}
    
    CheckCache -->|Yes| ReturnCache[Return Cached Response]
    CheckCache -->|No| QueryDB[Query Database]
    
    QueryDB --> Transform[Transform via API Resource]
    Transform --> StoreCache[Store in Redis]
    StoreCache --> ReturnFresh[Return Fresh Response]
    
    Mutation[Create/Update/Delete] --> InvalidateCache[Invalidate Related Caches]
    InvalidateCache --> ExecuteMutation[Execute Mutation]
    
    style CheckCache fill:#ffd700
    style StoreCache fill:#dc382d
    style InvalidateCache fill:#dc382d
```

**Cache Keys Pattern:**

```
allocations:month:{YYYY-MM}
allocations:project:{project_id}
allocations:member:{member_id}
reports:forecast:{from}:{to}:{filters_hash}
reports:utilization:{month}:{team_id}
capacity:month:{YYYY-MM}
```

**Cache Invalidation Rules:**

| Mutation | Invalidate Keys |
|----------|-----------------|
| Allocation created/updated | `allocations:month:{month}`, `allocations:project:{project_id}`, `allocations:member:{member_id}`, `reports:*` |
| Actual logged | `actuals:month:{month}`, `reports:utilization:*`, `reports:variance:*` |
| Team member updated | `allocations:member:{id}`, `capacity:*`, `reports:*` |
| Project updated | `allocations:project:{id}`, `reports:*` |

**TTL (Time to Live):**
- Allocations: 1 hour
- Reports: 15 minutes
- Master data: 24 hours

---

## Authentication Flow

### JWT Authentication Sequence

```mermaid
sequenceDiagram
    participant User
    participant SvelteKit
    participant Laravel
    participant Redis
    participant DB
    
    User->>SvelteKit: Enter credentials
    SvelteKit->>Laravel: POST /api/auth/login
    Laravel->>DB: Validate credentials
    DB-->>Laravel: User found
    
    Laravel->>Laravel: Generate JWT token
    Laravel->>Redis: Store refresh token
    Laravel-->>SvelteKit: {access_token, refresh_token, user}
    
    SvelteKit->>SvelteKit: Store tokens in localStorage
    SvelteKit-->>User: Redirect to dashboard
    
    Note over SvelteKit,Laravel: Subsequent requests
    
    User->>SvelteKit: Access protected resource
    SvelteKit->>Laravel: GET /api/allocations<br/>Header: Authorization: Bearer {token}
    Laravel->>Laravel: Validate JWT
    
    alt Token Valid
        Laravel->>DB: Fetch data
        DB-->>Laravel: Return data
        Laravel-->>SvelteKit: JSON response
    else Token Expired
        Laravel-->>SvelteKit: 401 Unauthorized
        SvelteKit->>Laravel: POST /api/auth/refresh<br/>{refresh_token}
        Laravel->>Redis: Validate refresh token
        Redis-->>Laravel: Token valid
        Laravel->>Laravel: Generate new access token
        Laravel-->>SvelteKit: {access_token}
        SvelteKit->>Laravel: Retry original request
    end
```

### Token Structure

**Access Token (JWT):**
```json
{
  "sub": "user-uuid",
  "role": "manager",
  "permissions": ["allocate_resources", "view_reports"],
  "iat": 1708171200,
  "exp": 1708174800,
  "jti": "token-uuid"
}
```

**Refresh Token:**
- Stored in Redis with 7-day TTL
- One-time use (rotated on refresh)
- Revocable (logout invalidates)

### Authorization Middleware

```mermaid
graph TD
    Request[Incoming Request] --> ExtractToken[Extract JWT from Header]
    ExtractToken --> ValidateToken{Token Valid?}
    
    ValidateToken -->|No| Return401[Return 401 Unauthorized]
    ValidateToken -->|Yes| LoadUser[Load User from Token]
    
    LoadUser --> CheckPermission{Has Permission?}
    CheckPermission -->|No| Return403[Return 403 Forbidden]
    CheckPermission -->|Yes| ExecuteController[Execute Controller]
    
    ExecuteController --> CheckPolicy{Model Policy?}
    CheckPolicy -->|Fail| Return403
    CheckPolicy -->|Pass| ReturnResponse[Return Response]
    
    style ValidateToken fill:#ffd700
    style CheckPermission fill:#ffd700
    style CheckPolicy fill:#ffd700
```

**Permission Matrix:**

| Role | Permissions |
|------|-------------|
| **Superuser** | `*` (all permissions) |
| **Manager** | `view_all_projects`, `create_project`, `edit_own_project`, `allocate_own_team`, `view_reports` |
| **Developer** | `view_own_allocations`, `log_hours`, `view_assigned_projects` |
| **Top Brass** | `view_all_reports` (read-only) |

---

## Deployment Architecture

### Docker Compose Structure

```mermaid
graph TB
    subgraph "Docker Compose Environment"
        subgraph "Frontend Container"
            SK[SvelteKit<br/>Node:latest<br/>Port 5173]
            SKVol[/app mounted<br/>from ./frontend]
        end
        
        subgraph "Backend Container"
            LAR[Laravel<br/>PHP 8.4-FPM<br/>Port 3000]
            LARVol[/var/www mounted<br/>from ./backend]
        end
        
        subgraph "Database Container"
            PG[PostgreSQL:latest<br/>Port 5432]
            PGVol[/var/lib/postgresql/data<br/>mounted from ./data/postgres]
        end
        
        subgraph "Cache Container"
            RD[Redis:latest<br/>Port 6379]
            RDVol[/data mounted<br/>from ./data/redis]
        end
    end
    
    subgraph "Host Machine"
        NPM[Nginx Proxy Manager<br/>Port 80/443]
        FrontendCode[./frontend/<br/>SvelteKit source]
        BackendCode[./backend/<br/>Laravel source]
    end
    
    NPM -->|Proxy /api/*| LAR
    NPM -->|Proxy /*| SK
    
    SK -->|API calls| LAR
    LAR -->|DB queries| PG
    LAR -->|Cache| RD
    
    SK -.->|Code mount| FrontendCode
    LAR -.->|Code mount| BackendCode
    
    style SK fill:#ff3e00
    style LAR fill:#ff2d20
    style PG fill:#336791
    style RD fill:#dc382d
```

### docker-compose.yml

```yaml
version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    container_name: headroom_frontend
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      - VITE_API_URL=http://backend:3000/api
    depends_on:
      - backend
    networks:
      - headroom_network
    restart: unless-stopped

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    container_name: headroom_backend
    ports:
      - "3000:3000"
    volumes:
      - ./backend:/var/www
      - /var/www/vendor
    environment:
      - APP_ENV=local
      - APP_DEBUG=true
      - DB_CONNECTION=pgsql
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_DATABASE=headroom
      - DB_USERNAME=headroom_user
      - DB_PASSWORD=${DB_PASSWORD}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      - postgres
      - redis
    networks:
      - headroom_network
    restart: unless-stopped

  postgres:
    image: postgres:latest
    container_name: headroom_postgres
    ports:
      - "5432:5432"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=headroom
      - POSTGRES_USER=headroom_user
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    networks:
      - headroom_network
    restart: unless-stopped

  redis:
    image: redis:latest
    container_name: headroom_redis
    ports:
      - "6379:6379"
    volumes:
      - ./data/redis:/data
    networks:
      - headroom_network
    restart: unless-stopped

networks:
  headroom_network:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
```

### Environment Variables (.env)

**Backend (.env):**
```ini
APP_NAME=Headroom
APP_ENV=local
APP_KEY=base64:...
APP_DEBUG=true
APP_URL=http://localhost:3000

DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=headroom
DB_USERNAME=headroom_user
DB_PASSWORD=secure_password_here

REDIS_HOST=redis
REDIS_PORT=6379

JWT_SECRET=your_jwt_secret_here
JWT_TTL=60
JWT_REFRESH_TTL=10080

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
```

**Frontend (.env):**
```ini
VITE_API_URL=http://localhost:3000/api
VITE_APP_NAME=Headroom
```

### Reverse Proxy Configuration (Nginx Proxy Manager)

**Proxy Host 1: Frontend**
- Domain: `headroom.local` (or your domain)
- Forward to: `http://headroom_frontend:5173`
- WebSocket support: Enabled

**Proxy Host 2: API**
- Domain: `headroom.local/api/*` (or your domain)
- Forward to: `http://headroom_backend:3000/api`
- Custom headers: 
  ```
  X-Forwarded-For: $remote_addr
  X-Forwarded-Proto: $scheme
  ```

---

## Technology Stack Details

### Backend: Laravel

**Project Structure:**
```
backend/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── AuthController.php
│   │   │   ├── TeamMemberController.php
│   │   │   ├── ProjectController.php
│   │   │   ├── AllocationController.php
│   │   │   ├── ActualController.php
│   │   │   └── ReportController.php
│   │   ├── Requests/
│   │   │   ├── AllocationRequest.php
│   │   │   ├── ProjectRequest.php
│   │   │   └── ...
│   │   ├── Resources/
│   │   │   ├── TeamMemberResource.php
│   │   │   ├── ProjectResource.php
│   │   │   ├── AllocationResource.php
│   │   │   └── ...
│   │   └── Middleware/
│   │       └── JwtMiddleware.php
│   ├── Models/
│   │   ├── TeamMember.php
│   │   ├── Project.php
│   │   ├── Allocation.php
│   │   ├── Actual.php
│   │   └── ...
│   ├── Policies/
│   │   ├── ProjectPolicy.php
│   │   ├── AllocationPolicy.php
│   │   └── ...
│   └── Services/
│       ├── CapacityService.php
│       ├── AllocationService.php
│       └── ReportService.php
├── database/
│   ├── migrations/
│   └── seeders/
├── routes/
│   └── api.php
├── tests/
│   ├── Unit/
│   └── Feature/
└── composer.json
```

**Key Packages:**
```json
{
  "require": {
    "php": "^8.3",
    "laravel/framework": "^11.0",
    "tymon/jwt-auth": "^2.0",
    "predis/predis": "^2.0",
    "knuckleswtf/scribe": "^4.0"
  },
  "require-dev": {
    "pestphp/pest": "^2.0",
    "phpunit/phpunit": "^10.0",
    "laravel/pint": "^1.0"
  }
}
```

### Frontend: SvelteKit

**Project Structure:**
```
frontend/
├── src/
│   ├── lib/
│   │   ├── components/
│   │   │   ├── AllocationTable.svelte
│   │   │   ├── CapacityCalendar.svelte
│   │   │   ├── ForecastChart.svelte
│   │   │   └── ...
│   │   ├── stores/
│   │   │   ├── auth.ts
│   │   │   ├── filters.ts
│   │   │   └── ui.ts
│   │   ├── api/
│   │   │   ├── client.ts
│   │   │   ├── allocations.ts
│   │   │   ├── projects.ts
│   │   │   └── ...
│   │   └── schemas/
│   │       ├── allocation.ts (Zod schemas)
│   │       ├── project.ts
│   │       └── ...
│   ├── routes/
│   │   ├── +layout.svelte
│   │   ├── +page.svelte (dashboard)
│   │   ├── login/
│   │   ├── capacity/
│   │   ├── projects/
│   │   ├── allocations/
│   │   ├── actuals/
│   │   └── reports/
│   └── app.html
├── static/
├── tests/
│   ├── unit/
│   └── e2e/
└── package.json
```

**Key Packages:**
```json
{
  "dependencies": {
    "@sveltejs/kit": "^2.0.0",
    "svelte": "^5.0.0",
    "tailwindcss": "^3.4.0",
    "daisyui": "^4.0.0",
    "recharts": "^2.10.0",
    "@tanstack/svelte-table": "^8.0.0",
    "sveltekit-superforms": "^2.0.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "vitest": "^1.0.0",
    "@playwright/test": "^1.40.0",
    "prettier": "^3.1.0",
    "eslint": "^8.55.0"
  }
}
```

---

## Quality Standards

### Testing Strategy

```mermaid
graph TB
    Code[Code Change] --> UnitTests[Unit Tests]
    UnitTests --> FeatureTests[Feature Tests]
    FeatureTests --> E2ETests[E2E Tests]
    
    UnitTests -->|PHPUnit/Vitest| UnitPass{Pass?}
    FeatureTests -->|PHPUnit/Pest| FeaturePass{Pass?}
    E2ETests -->|Playwright| E2EPass{Pass?}
    
    UnitPass -->|No| FailBuild[❌ Build Failed]
    FeaturePass -->|No| FailBuild
    E2EPass -->|No| FailBuild
    
    E2EPass -->|Yes| Coverage[Check Coverage]
    Coverage -->|< 70%| FailBuild
    Coverage -->|>= 70%| CodeReview[Code Review]
    
    CodeReview --> StyleCheck{Style OK?}
    StyleCheck -->|No| FailBuild
    StyleCheck -->|Yes| SecurityCheck{Security OK?}
    SecurityCheck -->|No| FailBuild
    SecurityCheck -->|Yes| Merge[✅ Merge]
    
    style FailBuild fill:#ff0000,color:#fff
    style Merge fill:#00ff00,color:#000
```

### Test Coverage Targets

| Layer | Target | Tools |
|-------|--------|-------|
| **Backend Unit** | >80% | PHPUnit |
| **Backend Feature** | >70% | Pest |
| **Frontend Unit** | >70% | Vitest |
| **E2E** | Critical paths | Playwright |
| **Overall** | >70% | Combined |

### Code Quality Tools

**Backend (Laravel):**
- **Linting:** Laravel Pint (PSR-12)
- **Static Analysis:** PHPStan (level 5+)
- **Security:** Laravel Security Checker

**Frontend (SvelteKit):**
- **Linting:** ESLint + Svelte plugin
- **Formatting:** Prettier
- **Type Checking:** TypeScript strict mode

### Pre-Commit Hooks

```bash
# .git/hooks/pre-commit

#!/bin/bash

echo "Running pre-commit checks..."

# Backend checks
cd backend
./vendor/bin/pint --test || exit 1
./vendor/bin/phpstan analyse || exit 1
php artisan test || exit 1

# Frontend checks
cd ../frontend
npm run lint || exit 1
npm run type-check || exit 1
npm run test:unit || exit 1

echo "✅ All checks passed!"
```

---

## Development Workflow

### OpenSpec Integration

```mermaid
graph TD
    Idea[New Feature/Fix] --> Explore[/opsx-explore<br/>Think through problem]
    
    Explore --> NewChange[/opsx-new headroom-feature<br/>Create change]
    
    NewChange --> Proposal[Write Proposal<br/>Why + What]
    Proposal --> Specs[Write Specs<br/>Requirements + Scenarios]
    Specs --> Design[Write Design<br/>How + Decisions]
    Design --> Tasks[Write Tasks<br/>Implementation checklist]
    
    Tasks --> Apply[/opsx-apply<br/>Implement tasks]
    
    Apply --> WriteTests[Write Tests<br/>Unit + E2E]
    WriteTests --> RunTests[Run Tests]
    RunTests --> TestPass{Pass?}
    TestPass -->|No| FixCode[Fix Code]
    FixCode --> RunTests
    
    TestPass -->|Yes| Verify[/opsx-verify<br/>Check coverage + standards]
    Verify --> VerifyPass{Pass?}
    VerifyPass -->|No| FixCode
    
    VerifyPass -->|Yes| Commit[Commit Changes<br/>Granular commits]
    Commit --> Archive[/opsx-archive<br/>Archive change]
    
    Archive --> Done[✅ Feature Complete]
    
    style Explore fill:#ffd700
    style Apply fill:#00bfff
    style Verify fill:#ff8c00
    style Done fill:#00ff00
```

### Branching Strategy

**Branch Types:**
- `main` - Production-ready code
- `develop` - Integration branch (not needed for solo dev initially)
- `feature/opsx-<change-name>` - Feature branches (one per OpenSpec change)
- `hotfix/description` - Emergency fixes

**Workflow:**
```bash
# Start new change
git checkout -b feature/opsx-capacity-planning

# Implement (from /opsx-apply)
# ... make changes, commit granularly ...

# Before merge
/opsx-verify capacity-planning

# Merge
git checkout main
git merge feature/opsx-capacity-planning
git push origin main
```

### Commit Message Format

```
[Type] Brief description (50 chars max)

Detailed explanation (optional, 72 char wrap)

Refs: openspec/changes/<change-name>
```

**Types:**
- `feat`: New feature
- `fix`: Bug fix
- `refactor`: Code restructuring
- `test`: Add/update tests
- `docs`: Documentation only
- `chore`: Tooling, config, etc.

**Examples:**
```
[feat] Add allocation validation rules

Implemented over/under allocation detection with RED flag indicators.
Validation checks approved estimate vs allocated hours per project.

Refs: openspec/changes/allocation-validation
```

---

## Appendix: Data Flow Examples

### Capacity Planning Flow

```mermaid
sequenceDiagram
    participant Manager
    participant UI as SvelteKit UI
    participant API as Laravel API
    participant DB as PostgreSQL
    participant Cache as Redis
    
    Manager->>UI: Navigate to Capacity Planning
    UI->>API: GET /api/capacity?month=2026-02
    API->>Cache: Check cache
    Cache-->>API: Cache miss
    
    API->>DB: Query team members
    API->>DB: Query holidays
    API->>DB: Query PTOs
    API->>API: Calculate capacity
    
    API->>Cache: Store result (TTL: 1h)
    API-->>UI: Return capacity data
    UI->>UI: Render calendar + summary
    
    Manager->>UI: Mark PTO for Team Member A (2026-02-10 to 2026-02-12)
    UI->>API: POST /api/pto
    API->>DB: Insert PTO record
    API->>Cache: Invalidate capacity:month:2026-02
    API-->>UI: Success
    
    UI->>API: GET /api/capacity?month=2026-02
    API->>DB: Recalculate (cache invalidated)
    API-->>UI: Updated capacity
    UI->>UI: Re-render with reduced capacity
```

### Allocation Flow with Validation

```mermaid
sequenceDiagram
    participant Manager
    participant UI as SvelteKit UI
    participant API as Laravel API
    participant DB as PostgreSQL
    
    Manager->>UI: Open allocation matrix (Feb 2026)
    UI->>API: GET /api/allocations?month=2026-02
    API->>DB: Query allocations
    API-->>UI: Return allocations
    
    Manager->>UI: Allocate Dev A: 50h to Project X
    UI->>UI: Superforms validation (Zod)
    UI->>API: POST /api/allocations
    
    API->>API: Validate request (Laravel)
    API->>DB: Check project approved estimate
    DB-->>API: Approved: 100h, Currently allocated: 55h
    
    API->>API: Calculate: 55 + 50 = 105h > 100h
    API->>API: Over-allocation detected!
    API-->>UI: 422 Unprocessable Entity<br/>{error: "Over-allocated by 5h"}
    
    UI->>UI: Show RED flag indicator
    UI->>Manager: "⚠️ Project X will be over-allocated by 5h"
    
    Manager->>UI: Reduce allocation to 45h
    UI->>API: POST /api/allocations (45h)
    API->>API: Validate: 55 + 45 = 100h ✅
    API->>DB: Insert allocation
    API->>Cache: Invalidate related caches
    API-->>UI: Success
    
    UI->>UI: Show GREEN indicator (100% allocated)
```

---

## Frontend Layout Architecture

**Added:** February 18, 2026  
**Status:** Approved for Implementation

### Layout System Overview

The Headroom frontend uses a **sidebar + content** layout pattern optimized for data-dense resource planning workflows.

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         HEADROOM LAYOUT ARCHITECTURE                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────┬──────────────────────────────────────────────────────────┐   │
│  │          │  ┌────────────────────────────────────────────────────┐  │   │
│  │          │  │ 🔍 Search...    [Feb 2026 ▼]  [+ Add]  👤 User ▼  │  │   │
│  │          │  ├────────────────────────────────────────────────────┤  │   │
│  │  SIDEBAR │  │  Breadcrumbs: Dashboard > Overview                 │  │   │
│  │  240px   │  ├────────────────────────────────────────────────────┤  │   │
│  │          │  │                                                    │  │   │
│  │  ◀ ▶     │  │                                                    │  │   │
│  │          │  │                                                    │  │   │
│  │  ─────── │  │         MAIN CONTENT AREA                          │  │   │
│  │  PLANNING│  │                                                    │  │   │
│  │  ─────── │  │    (Tables, Grids, Charts, Forms)                  │  │   │
│  │  📊 Dash │  │                                                    │  │   │
│  │  👥 Team │  │    Full width, minimal padding                     │  │   │
│  │  📁 Projs│  │                                                    │  │   │
│  │  📅 Alloc│  │                                                    │  │   │
│  │  ✅ Actu │  │                                                    │  │   │
│  │          │  │                                                    │  │   │
│  │  ─────── │  │                                                    │  │   │
│  │  REPORTS │  │                                                    │  │   │
│  │  ─────── │  │                                                    │  │   │
│  │  📈 Forecast  │                                                    │  │   │
│  │  📉 Util     │                                                    │  │   │
│  │  💰 Costs    │                                                    │  │   │
│  │  📋 Variance │                                                    │  │   │
│  │          │  │                                                    │  │   │
│  │  ─────── │  │                                                    │  │   │
│  │  ADMIN*  │  │                                                    │  │   │
│  │  ─────── │  │                                                    │  │   │
│  │  ⚙️ Set  │  │                                                    │  │   │
│  │          │  │                                                    │  │   │
│  │  ─────── │  │                                                    │  │   │
│  │  🌙/☀️   │  │                                                    │  │   │
│  └──────────┴──────────────────────────────────────────────────────────┘   │
│                                                                             │
│  * Admin section visible only to superuser                                 │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Component Hierarchy

```mermaid
graph TB
    subgraph "Route Layer"
        Layout["+layout.svelte"]
    end
    
    subgraph "Layout Layer"
        AppLayout["AppLayout.svelte"]
        Sidebar["Sidebar.svelte"]
        TopBar["TopBar.svelte"]
        Breadcrumbs["Breadcrumbs.svelte"]
    end
    
    subgraph "State Layer"
        LayoutStore["layout.ts store"]
        PeriodStore["period.ts store"]
        AuthStore["auth.ts store"]
    end
    
    subgraph "Page Layer"
        PageContent["<slot /> Page Content"]
        PageHeader["PageHeader.svelte"]
    end
    
    Layout --> AppLayout
    AppLayout --> Sidebar
    AppLayout --> TopBar
    AppLayout --> Breadcrumbs
    AppLayout --> PageContent
    
    Sidebar --> LayoutStore
    Sidebar --> AuthStore
    TopBar --> PeriodStore
    TopBar --> AuthStore
    
    PageContent --> PageHeader
    
    style AppLayout fill:#ff3e00
    style LayoutStore fill:#336791
    style PeriodStore fill:#336791
```

### Sidebar Component

**File:** `src/lib/components/layout/Sidebar.svelte`

**States:**
- `expanded` (240px) — Full navigation with labels
- `collapsed` (64px) — Icons only
- `hidden` (0px) — Completely hidden (mobile drawer)

**Features:**
- Toggle button in header
- Section headers: PLANNING, REPORTS, ADMIN
- Role-based visibility (ADMIN section for superuser only)
- Active route highlighting
- Dark mode toggle at bottom
- Keyboard shortcut: `Cmd/Ctrl + \` to toggle

**Responsive Behavior:**

| Breakpoint | Default State | Toggle Method |
|------------|---------------|---------------|
| ≥1280px | expanded | Manual |
| 1024-1279px | collapsed | Manual |
| <1024px | hidden | Hamburger menu (drawer overlay) |

### TopBar Component

**File:** `src/lib/components/layout/TopBar.svelte`

**Elements:**
- Left: Hamburger menu (mobile only)
- Center: Breadcrumbs
- Right: Month selector, User menu

**Global Month Selector:**
```typescript
// src/lib/stores/period.ts
import { writable } from 'svelte/store';

export const selectedMonth = writable<string>(getCurrentMonth());

function getCurrentMonth(): string {
  const now = new Date();
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
}

export function setMonth(month: string): void {
  selectedMonth.set(month);
}
```

### Layout Store

**File:** `src/lib/stores/layout.ts`

```typescript
// src/lib/stores/layout.ts
import { writable } from 'svelte/store';
import { browser } from '$app/environment';

export type SidebarState = 'expanded' | 'collapsed' | 'hidden';
export type Theme = 'light' | 'dark';

function createLayoutStore() {
  const defaultSidebar: SidebarState = browser && window.innerWidth >= 1280 
    ? 'expanded' 
    : browser && window.innerWidth >= 1024 
      ? 'collapsed' 
      : 'hidden';
  
  const storedSidebar = browser 
    ? (localStorage.getItem('headroom_sidebar_state') as SidebarState) || defaultSidebar 
    : defaultSidebar;
  
  const storedTheme = browser 
    ? (localStorage.getItem('headroom_theme') as Theme) || 'light' 
    : 'light';
  
  const sidebarState = writable<SidebarState>(storedSidebar);
  const theme = writable<Theme>(storedTheme);
  
  return {
    sidebarState: { subscribe: sidebarState.subscribe },
    theme: { subscribe: theme.subscribe },
    toggleSidebar: () => {
      sidebarState.update(current => {
        const next = current === 'expanded' ? 'collapsed' : 
                     current === 'collapsed' ? 'hidden' : 'expanded';
        if (browser) localStorage.setItem('headroom_sidebar_state', next);
        return next;
      });
    },
    setTheme: (newTheme: Theme) => {
      theme.set(newTheme);
      if (browser) localStorage.setItem('headroom_theme', newTheme);
    }
  };
}

export const layoutStore = createLayoutStore();
```

### Navigation Configuration

**File:** `src/lib/config/navigation.ts`

```typescript
// src/lib/config/navigation.ts
import type { NavSection } from '$lib/types/layout';

export const navigationSections: NavSection[] = [
  {
    title: 'PLANNING',
    items: [
      { label: 'Dashboard', href: '/dashboard', icon: 'layout-dashboard' },
      { label: 'Team Members', href: '/team-members', icon: 'users' },
      { label: 'Projects', href: '/projects', icon: 'folder' },
      { label: 'Allocations', href: '/allocations', icon: 'calendar' },
      { label: 'Actuals', href: '/actuals', icon: 'check-circle' },
    ]
  },
  {
    title: 'REPORTS',
    items: [
      { label: 'Forecast', href: '/reports/forecast', icon: 'trending-up' },
      { label: 'Utilization', href: '/reports/utilization', icon: 'bar-chart-2' },
      { label: 'Costs', href: '/reports/costs', icon: 'dollar-sign' },
      { label: 'Variance', href: '/reports/variance', icon: 'alert-circle' },
      { label: 'Allocation Matrix', href: '/reports/allocation', icon: 'grid-3x3' },
    ]
  },
  {
    title: 'ADMIN',
    roles: ['superuser'],
    items: [
      { label: 'Settings', href: '/settings', icon: 'settings' },
      { label: 'Master Data', href: '/master-data', icon: 'database' },
    ]
  },
];
```

### Theme System

**Implementation:**
- DaisyUI theme switching via `data-theme` attribute on `<html>` element
- Light theme: `light` (DaisyUI default)
- Dark theme: `dark` or `business`
- Persisted to localStorage
- Respects system preference on first visit

```typescript
// Theme toggle effect
$effect(() => {
  if (browser) {
    document.documentElement.setAttribute('data-theme', $theme);
  }
});
```

### Route Layout Integration

**File:** `src/routes/+layout.svelte`

```svelte
<script lang="ts">
  import AppLayout from '$lib/components/layout/AppLayout.svelte';
  import { page } from '$app/stores';
  
  // Pages without AppLayout
  const publicPages = ['/login', '/auth'];
  $: isPublicPage = publicPages.some(p => $page.url.pathname.startsWith(p));
</script>

{#if isPublicPage}
  <slot />
{:else}
  <AppLayout>
    <slot />
  </AppLayout>
{/if}
```

### Data Density Patterns

**Table Configurations (DaisyUI):**

| View | Classes | Purpose |
|------|---------|---------|
| Allocation Matrix | `table table-compact table-pin-rows table-pin-cols` | Max density, pinned header/first column |
| Projects List | `table table-zebra table-pin-rows` | Readability, pinned header |
| Team Members | `table table-zebra` | Standard readability |
| Reports | `table table-compact table-pin-rows` | Dense data, pinned header |

### Accessibility Requirements

1. **Keyboard Navigation:**
   - `Tab` through sidebar items
   - `Enter/Space` to activate
   - `Escape` to close mobile drawer
   - `Cmd/Ctrl + \` to toggle sidebar

2. **ARIA Attributes:**
   - `aria-expanded` on sidebar toggle
   - `aria-current="page"` on active nav item
   - `role="navigation"` on sidebar
   - `role="main"` on content area

3. **Focus Management:**
   - Focus trap in mobile drawer
   - Focus restored on drawer close

---

**Document Control:**
- **Owner:** Santhosh J
- **Approver:** Santhosh J
- **Next Review:** Post-MVP implementation
- **Change History:**
  - v1.0 (2026-02-17): Initial architecture approved
  - v1.1 (2026-02-18): Added Frontend Layout Architecture section

---

*End of Architecture Document*
