1162 lines
27 KiB
Markdown
1162 lines
27 KiB
Markdown
# 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)
|
|
```
|
|
|
|
---
|
|
|
|
**Document Control:**
|
|
- **Owner:** Santhosh J
|
|
- **Approver:** Santhosh J
|
|
- **Next Review:** Post-MVP implementation
|
|
- **Change History:**
|
|
- v1.0 (2026-02-17): Initial architecture approved
|
|
|
|
---
|
|
|
|
*End of Architecture Document*
|