---
# System prepended metadata

title: Hive Switch Migration Plan

---

# Hive Switch Migration Plan
## Standalone Next.js Application for Customer Owner Reallocation

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Architecture Overview](#2-architecture-overview)
3. [Repository Structure](#3-repository-structure)
4. [Technology Stack](#4-technology-stack)
5. [Database Integration](#5-database-integration)
6. [Authentication & Security](#6-authentication--security)
7. [Feature Implementation](#7-feature-implementation)
8. [Reallocation Business Logic Options](#8-reallocation-business-logic-options)
9. [UI/UX Migration](#9-uiux-migration)
10. [Deployment Strategy](#10-deployment-strategy)
11. [Testing Strategy](#11-testing-strategy)
12. [Migration Checklist](#12-migration-checklist)
13. [Alternative Architecture: Microservice (Option C)](#13-alternative-architecture-microservice-option-c)
14. [Risk Assessment & Mitigations](#14-risk-assessment--mitigations)
15. [Future Considerations](#15-future-considerations)

---

## 1. Executive Summary

### Objective
Create a standalone Next.js application (`hive-switch`) that provides internal operations teams with the ability to manage customer ownership ("beekeeper") reallocations, while maintaining compatibility with the existing Postgres database shared with `web-app-alpha`.

### Key Deliverables
- Standalone Next.js 14+ application with App Router
- Direct Postgres integration via Sequelize (matching legacy versions)
- Three operational tabs: Account Distributions, Lead Distributions, Reallocations
- VPN-restricted access via Cloudflare WARP Zero Trust
- Tailwind CSS styling system
- Extensible architecture for future hive management features

---

## 2. Architecture Overview

### Option A: Direct Database Connection (Primary Approach)

```
┌─────────────────────────────────────────────────────────────────────────┐
│                        Cloudflare WARP Zero Trust                       │
│                              (VPN Gateway)                              │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         hive-switch (New App)                           │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                      Next.js 14 (App Router)                     │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌────────────────────────┐ │   │
│  │  │  Account     │  │    Lead      │  │    Reallocations       │ │   │
│  │  │ Distributions│  │ Distributions│  │        Tab             │ │   │
│  │  └──────────────┘  └──────────────┘  └────────────────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    Next.js API Routes                            │   │
│  │  ┌──────────────────────┐  ┌──────────────────────────────────┐ │   │
│  │  │  GET /api/workload   │  │  POST /api/reallocate            │ │   │
│  │  └──────────────────────┘  └──────────────────────────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    Data Access Layer                             │   │
│  │  ┌──────────────────────────────────────────────────────────┐   │   │
│  │  │              Sequelize v5.22.5 + pg v8.11.0              │   │   │
│  │  │    (Version-matched with web-app-alpha for compatibility) │   │   │
│  │  └──────────────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         Shared Postgres Database                        │
│  ┌─────────────────────────┐    ┌─────────────────────────────────┐    │
│  │     account table       │    │         lead table              │    │
│  │  - owner_email__c       │    │     - owner_email__c            │    │
│  │  - ownerid              │    │     - ownerid                   │    │
│  │  - account_status__c    │    │     - selected_contribution_... │    │
│  └─────────────────────────┘    └─────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────────┘
                                    ▲
                                    │
┌─────────────────────────────────────────────────────────────────────────┐
│                     web-app-alpha (Existing App)                        │
│                    (Continues normal operations)                        │
└─────────────────────────────────────────────────────────────────────────┘
```

### Data Flow

```
User (VPN) → Cloudflare WARP → hive-switch UI → API Route → Sequelize → Postgres
                                                                ↑
                                                    web-app-alpha (concurrent access)
```

---

## 3. Repository Structure

```
hive-switch/
├── .github/
│   └── workflows/
│       ├── ci.yml                    # Linting, type-checking, tests
│       └── deploy.yml                # Heroku deployment
├── src/
│   ├── app/                          # Next.js App Router
│   │   ├── layout.tsx                # Root layout with providers
│   │   ├── page.tsx                  # Dashboard redirect
│   │   ├── globals.css               # Tailwind imports
│   │   ├── (auth)/                   # Auth-related routes (future)
│   │   │   └── login/
│   │   │       └── page.tsx
│   │   └── (dashboard)/              # Protected dashboard routes
│   │       ├── layout.tsx            # Dashboard layout with nav
│   │       └── operations/
│   │           ├── page.tsx          # Main operations page with tabs
│   │           └── loading.tsx       # Loading state
│   ├── api/                          # API Routes
│   │   ├── workload/
│   │   │   └── route.ts              # GET beekeeper workload
│   │   ├── reallocate/
│   │   │   └── route.ts              # POST reallocation
│   │   └── health/
│   │       └── route.ts              # Health check endpoint
│   ├── components/
│   │   ├── ui/                       # Reusable UI components
│   │   │   ├── button.tsx
│   │   │   ├── select.tsx
│   │   │   ├── table.tsx
│   │   │   ├── tabs.tsx
│   │   │   ├── card.tsx
│   │   │   └── alert.tsx
│   │   ├── operations/               # Feature-specific components
│   │   │   ├── account-distributions-tab.tsx
│   │   │   ├── lead-distributions-tab.tsx
│   │   │   ├── reallocations-tab.tsx
│   │   │   └── workload-summary.tsx
│   │   └── layout/                   # Layout components
│   │       ├── header.tsx
│   │       ├── sidebar.tsx
│   │       └── footer.tsx
│   ├── lib/
│   │   ├── db/                       # Database layer
│   │   │   ├── sequelize.ts          # Sequelize instance
│   │   │   ├── models/
│   │   │   │   ├── index.ts          # Model exports
│   │   │   │   ├── account.model.ts  # Account model (ported)
│   │   │   │   └── lead.model.ts     # Lead model (ported)
│   │   │   └── repositories/
│   │   │       ├── account.repository.ts
│   │   │       └── lead.repository.ts
│   │   ├── services/                 # Business logic
│   │   │   ├── workload.service.ts
│   │   │   └── reallocation.service.ts
│   │   ├── validations/              # Zod schemas
│   │   │   ├── workload.schema.ts
│   │   │   └── reallocation.schema.ts
│   │   └── utils/                    # Utility functions
│   │       ├── date.ts
│   │       └── formatting.ts
│   ├── hooks/                        # React hooks
│   │   ├── use-workload.ts
│   │   └── use-reallocation.ts
│   ├── types/                        # TypeScript types
│   │   ├── api.ts
│   │   ├── models.ts
│   │   └── index.ts
│   └── middleware.ts                 # Next.js middleware (auth/VPN checks)
├── public/
│   └── images/
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── .env.example                      # Environment variables template
├── .env.local                        # Local development (gitignored)
├── .eslintrc.json
├── .prettierrc
├── .gitignore
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.ts
├── tsconfig.json
└── README.md
```

---

## 4. Technology Stack

### Core Dependencies

```json
{
  "name": "hive-switch",
  "version": "1.0.0",
  "private": true,
  "engines": {
    "node": "18",
    "npm": "9"
  },
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit",
    "test": "jest",
    "test:e2e": "playwright test"
  },
  "dependencies": {
    "next": "^14.2.0",
    "react": "^18.3.0",
    "react-dom": "^18.3.0",

    "pg": "^8.11.0",
    "sequelize": "^5.22.5",

    "@tanstack/react-query": "^5.0.0",
    "zod": "^3.23.0",

    "tailwind-merge": "^2.2.0",
    "clsx": "^2.1.0",
    "@radix-ui/react-tabs": "^1.0.0",
    "@radix-ui/react-select": "^2.0.0",
    "@radix-ui/react-alert-dialog": "^1.0.0",

    "date-fns": "^3.6.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0",
    "typescript": "^5.4.0",

    "tailwindcss": "^3.4.0",
    "postcss": "^8.4.0",
    "autoprefixer": "^10.4.0",

    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.0",
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "prettier": "^3.2.0",
    "prettier-plugin-tailwindcss": "^0.5.0",

    "jest": "^29.7.0",
    "@testing-library/react": "^15.0.0",
    "@testing-library/jest-dom": "^6.4.0",
    "@playwright/test": "^1.43.0"
  }
}
```

### Version Compatibility Analysis

| Package | web-app-alpha | hive-switch | Compatibility Notes |
|---------|---------------|-------------|---------------------|
| `pg` | ^8.11.0 | ^8.11.0 | ✅ Exact match recommended |
| `sequelize` | ^5.22.5 | ^5.22.5 | ✅ **Critical**: Must match for schema compatibility |
| `node` | 16 | 18 | ⚠️ Sequelize 5.x works with Node 18, but test thoroughly |
| `zod` | ^3.22.4 | ^3.23.0 | ✅ Minor version, backwards compatible |

### Why Sequelize v5.22.5 Matters

```typescript
// Sequelize v5 vs v6+ has breaking changes in:
// 1. Model definition syntax
// 2. Transaction handling
// 3. Association definitions
// 4. TypeScript types

// v5 syntax (current):
const Account = sequelize.define('account', { /* ... */ }, { /* options */ });

// v6+ syntax (incompatible):
class Account extends Model {
  declare sfid: string;
}
Account.init({ /* ... */ }, { sequelize });

// RECOMMENDATION: Lock to exact versions
"pg": "8.11.0",           // Not ^8.11.0
"sequelize": "5.22.5",    // Not ^5.22.5
```

### Potential Upgrade Path (Future Consideration)

If you need to upgrade Sequelize in the future:

1. **Sequelize v6**: Requires model class syntax changes
2. **Sequelize v7**: TypeScript-first, significant refactor
3. **Alternative**: Consider Drizzle ORM or Prisma for new projects

> **Recommendation**: For this project, maintain Sequelize v5.22.5 for guaranteed compatibility. Plan a coordinated upgrade of both applications in the future.

---

## 5. Database Integration

### Sequelize Configuration

```typescript
// src/lib/db/sequelize.ts
import { Sequelize } from 'sequelize';

function getDatabaseUrl(): string {
  const { DATABASE_URL, CI, PG_USER, PG_PASSWORD } = process.env;

  if (CI) {
    return `postgres://${PG_USER}:${PG_PASSWORD}@127.0.0.1:5433/development`;
  }

  if (!DATABASE_URL) {
    throw new Error('DATABASE_URL environment variable is required');
  }

  return DATABASE_URL;
}

const sequelize = new Sequelize(getDatabaseUrl(), {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false,
    },
  },
  logging: process.env.NODE_ENV === 'development' ? console.log : false,
  pool: {
    max: 5,      // Lower than web-app-alpha to avoid connection exhaustion
    min: 0,
    acquire: 30000,
    idle: 10000,
  },
});

export default sequelize;
```

### Account Model (Ported)

```typescript
// src/lib/db/models/account.model.ts
import { DataTypes, Model, Op } from 'sequelize';
import sequelize from '../sequelize';

interface AccountAttributes {
  id?: number;
  sfid?: string;
  personemail?: string;
  owner_email__c?: string;
  ownerid?: string;
  owner_first_name__c?: string;
  account_status__c?: string;
  firstname?: string;
  lastname?: string;
  // Add other fields as needed
}

interface AccountInstance extends Model<AccountAttributes>, AccountAttributes {}

const Account = sequelize.define<AccountInstance>(
  'account',
  {
    sfid: {
      type: DataTypes.STRING(18),
      unique: true,
    },
    personemail: {
      type: DataTypes.STRING(75),
    },
    owner_email__c: {
      type: DataTypes.STRING(255),
    },
    ownerid: {
      type: DataTypes.STRING(18),
    },
    owner_first_name__c: {
      type: DataTypes.STRING(255),
    },
    account_status__c: {
      type: DataTypes.STRING(255),
    },
    firstname: {
      type: DataTypes.STRING(30),
    },
    lastname: {
      type: DataTypes.STRING(50),
    },
    is_duplicate_account__c: {
      type: DataTypes.BOOLEAN,
    },
    // Add other required fields...
  },
  {
    freezeTableName: true,
    timestamps: false,
  }
);

export default Account;
export { AccountAttributes, AccountInstance };
```

### Account Repository

```typescript
// src/lib/db/repositories/account.repository.ts
import { Op, fn, col, literal } from 'sequelize';
import Account from '../models/account.model';
import sequelize from '../sequelize';

export interface AccountCountByOwner {
  [email: string]: {
    pensionLive: number;
    other: number;
  };
}

export const accountRepository = {
  /**
   * Get distinct beekeeper emails from accounts
   */
  async getBeekeepers(): Promise<string[]> {
    const records = await Account.findAll({
      attributes: [[fn('DISTINCT', col('owner_email__c')), 'owner_email__c']],
      where: {
        ownerid: { [Op.ne]: null },
        owner_email__c: { [Op.ne]: null },
      },
      order: [['owner_email__c', 'ASC']],
    });
    return records.map((r) => r.owner_email__c as string);
  },

  /**
   * Get account counts grouped by owner email
   */
  async getCountByOwnerEmail(): Promise<AccountCountByOwner> {
    const records = await Account.findAll({
      attributes: [
        'owner_email__c',
        [
          fn('COUNT', literal("CASE WHEN account_status__c = 'Pension Live' THEN 1 END")),
          'pensionLiveCount',
        ],
        [
          fn('COUNT', literal("CASE WHEN account_status__c != 'Pension Live' OR account_status__c IS NULL THEN 1 END")),
          'otherCount',
        ],
      ],
      where: {
        owner_email__c: { [Op.ne]: null },
      },
      group: ['owner_email__c'],
      order: [['owner_email__c', 'ASC']],
      raw: true,
    });

    const result: AccountCountByOwner = {};
    records.forEach((record: any) => {
      if (record.owner_email__c) {
        result[record.owner_email__c] = {
          pensionLive: parseInt(record.pensionLiveCount, 10),
          other: parseInt(record.otherCount, 10),
        };
      }
    });
    return result;
  },

  /**
   * Get total account count
   */
  async getTotalCount(): Promise<number> {
    return Account.count();
  },

  /**
   * Update owner for accounts matching criteria
   */
  async updateOwner(
    currentOwnerEmail: string,
    newOwnerEmail: string,
    newOwnerId: string,
    options?: {
      customerType?: 'pensionLive' | 'other';
      limit?: number;
    }
  ): Promise<[number, AccountInstance[]]> {
    const where: any = {
      owner_email__c: currentOwnerEmail,
    };

    if (options?.customerType === 'pensionLive') {
      where.account_status__c = 'Pension Live';
    } else if (options?.customerType === 'other') {
      where.account_status__c = { [Op.or]: [{ [Op.ne]: 'Pension Live' }, null] };
    }

    // Use transaction for data integrity
    return sequelize.transaction(async (transaction) => {
      // If limit is specified, we need to select specific records first
      if (options?.limit) {
        const accountsToUpdate = await Account.findAll({
          where,
          limit: options.limit,
          attributes: ['id'],
          transaction,
        });

        const ids = accountsToUpdate.map((a) => a.id);

        return Account.update(
          {
            owner_email__c: newOwnerEmail,
            ownerid: newOwnerId,
          },
          {
            where: { id: { [Op.in]: ids } },
            transaction,
            returning: true,
          }
        );
      }

      return Account.update(
        {
          owner_email__c: newOwnerEmail,
          ownerid: newOwnerId,
        },
        {
          where,
          transaction,
          returning: true,
        }
      );
    });
  },
};
```

### Connection Pooling Strategy

Since both applications will access the same database:

```
┌─────────────────────────────────────────────────────────────────┐
│                    Heroku Postgres                              │
│                                                                 │
│   Connection Limit: ~500 (varies by plan)                       │
│                                                                 │
│   ┌─────────────────────┐    ┌─────────────────────┐           │
│   │   web-app-alpha     │    │    hive-switch      │           │
│   │   Pool: max 20      │    │    Pool: max 5      │           │
│   │   (production)      │    │    (internal tool)  │           │
│   └─────────────────────┘    └─────────────────────┘           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

> **Recommendation**: Keep `hive-switch` pool small (max 5) since it's an internal tool with limited concurrent users. Monitor connection usage via Heroku metrics.

---

## 6. Authentication & Security

### Cloudflare WARP Zero Trust Integration

```typescript
// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Cloudflare Access headers
  const cfAccessJwt = request.headers.get('cf-access-jwt-assertion');
  const cfAccessAuthenticated = request.headers.get('cf-access-authenticated-user-email');

  // Health check bypass
  if (request.nextUrl.pathname === '/api/health') {
    return NextResponse.next();
  }

  // Development bypass (remove in production)
  if (process.env.NODE_ENV === 'development' && process.env.BYPASS_AUTH === 'true') {
    return NextResponse.next();
  }

  // Verify Cloudflare Access headers
  if (!cfAccessJwt || !cfAccessAuthenticated) {
    console.warn('Access denied: Missing Cloudflare Access headers', {
      path: request.nextUrl.pathname,
      ip: request.ip,
    });

    return new NextResponse(
      JSON.stringify({ error: 'Unauthorized', message: 'VPN access required' }),
      { status: 401, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Optional: Verify JWT signature with Cloudflare's public key
  // This requires fetching keys from: https://<team-domain>.cloudflareaccess.com/cdn-cgi/access/certs

  // Add user email to request headers for downstream use
  const response = NextResponse.next();
  response.headers.set('x-user-email', cfAccessAuthenticated);

  return response;
}

export const config = {
  matcher: [
    // Match all paths except static files and images
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};
```

### Cloudflare Access Setup Requirements

1. **Create Access Application** in Cloudflare Zero Trust dashboard:
   - Application type: Self-hosted
   - Application domain: `hive-switch.herokuapp.com` (or custom domain)
   - Session duration: 24 hours (recommended for internal tools)

2. **Create Access Policy**:
   ```
   Policy Name: PensionBee Internal Team
   Action: Allow
   Include:
     - Emails ending in: @pensionbee.com
     - OR: Specific user emails
   ```

3. **Configure WARP Client**:
   - Users must have WARP installed and connected
   - Application appears in their Access launcher

### Future Role-Based Access Control (RBAC) Consideration

```typescript
// src/types/auth.ts
export type UserRole = 'viewer' | 'operator' | 'admin';

export interface User {
  email: string;
  role: UserRole;
  permissions: Permission[];
}

export type Permission =
  | 'workload:read'
  | 'reallocation:create'
  | 'reallocation:approve'
  | 'settings:manage';

// Future: Store roles in database or integrate with identity provider
export const rolePermissions: Record<UserRole, Permission[]> = {
  viewer: ['workload:read'],
  operator: ['workload:read', 'reallocation:create'],
  admin: ['workload:read', 'reallocation:create', 'reallocation:approve', 'settings:manage'],
};
```

### Audit Logging

```typescript
// src/lib/services/audit.service.ts
import sequelize from '../db/sequelize';

export interface AuditLogEntry {
  action: 'REALLOCATION_CREATED' | 'REALLOCATION_EXECUTED' | 'REALLOCATION_FAILED';
  performedBy: string;
  details: Record<string, any>;
  timestamp: Date;
  ipAddress?: string;
}

export const auditService = {
  async log(entry: AuditLogEntry): Promise<void> {
    // Option 1: Log to database table
    await sequelize.query(
      `INSERT INTO audit_log (action, performed_by, details, timestamp, ip_address)
       VALUES (:action, :performedBy, :details, :timestamp, :ipAddress)`,
      {
        replacements: {
          action: entry.action,
          performedBy: entry.performedBy,
          details: JSON.stringify(entry.details),
          timestamp: entry.timestamp,
          ipAddress: entry.ipAddress || null,
        },
      }
    );

    // Option 2: Also send to external logging service (Datadog, etc.)
    console.info('[AUDIT]', JSON.stringify(entry));
  },
};
```

---

## 7. Feature Implementation

### API Routes

#### GET /api/workload

```typescript
// src/app/api/workload/route.ts
import { NextResponse } from 'next/server';
import { accountRepository } from '@/lib/db/repositories/account.repository';
import { leadRepository } from '@/lib/db/repositories/lead.repository';
import { workloadResponseSchema } from '@/lib/validations/workload.schema';

export async function GET() {
  try {
    const [
      beekeepers,
      accountCount,
      accountCountByOwnerEmail,
      leadCount,
      leadCountByOwnerEmail,
      accountTotalCount,
    ] = await Promise.all([
      accountRepository.getBeekeepers(),
      accountRepository.getTotalCount(),
      accountRepository.getCountByOwnerEmail(),
      leadRepository.getTotalCount(),
      leadRepository.getCountByOwnerEmail(),
      accountRepository.getTotalCount(),
    ]);

    const responseData = {
      beekeepers,
      accountCount,
      accountCountByOwnerEmail,
      leadCount,
      leadCountByOwnerEmail,
      accountTotalCount,
    };

    // Validate response shape
    const validated = workloadResponseSchema.parse(responseData);

    return NextResponse.json(validated);
  } catch (error) {
    console.error('Error fetching workload data:', error);
    return NextResponse.json(
      { error: 'Failed to fetch workload data' },
      { status: 500 }
    );
  }
}
```

#### POST /api/reallocate

```typescript
// src/app/api/reallocate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { reallocationRequestSchema } from '@/lib/validations/reallocation.schema';
import { reallocationService } from '@/lib/services/reallocation.service';
import { auditService } from '@/lib/services/audit.service';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const userEmail = request.headers.get('x-user-email') || 'unknown';

    // Validate request
    const validationResult = reallocationRequestSchema.safeParse(body);
    if (!validationResult.success) {
      return NextResponse.json(
        { error: 'Validation failed', details: validationResult.error.issues },
        { status: 400 }
      );
    }

    const { currentBeekeeper, targetBeekeeperArray, accountOrLead, customerType } = validationResult.data;

    // Execute reallocation
    const result = await reallocationService.executeReallocation({
      currentBeekeeper,
      targetBeekeeperArray,
      accountOrLead,
      customerType,
    });

    // Audit log
    await auditService.log({
      action: 'REALLOCATION_EXECUTED',
      performedBy: userEmail,
      details: {
        currentBeekeeper,
        targetBeekeeperArray,
        accountOrLead,
        customerType,
        affectedCount: result.affectedCount,
      },
      timestamp: new Date(),
      ipAddress: request.ip,
    });

    return NextResponse.json({
      success: true,
      message: `Successfully reallocated ${result.affectedCount} ${accountOrLead}s`,
      details: result,
    });
  } catch (error) {
    console.error('Reallocation error:', error);
    return NextResponse.json(
      { error: 'Reallocation failed', message: (error as Error).message },
      { status: 500 }
    );
  }
}
```

### React Query Hooks

```typescript
// src/hooks/use-workload.ts
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
import { workloadResponseSchema } from '@/lib/validations/workload.schema';

type WorkloadData = z.infer<typeof workloadResponseSchema>;

async function fetchWorkload(): Promise<WorkloadData> {
  const response = await fetch('/api/workload');
  if (!response.ok) {
    throw new Error('Failed to fetch workload data');
  }
  const data = await response.json();
  return workloadResponseSchema.parse(data);
}

export function useWorkload() {
  return useQuery({
    queryKey: ['workload'],
    queryFn: fetchWorkload,
    staleTime: 30 * 1000, // Consider data stale after 30 seconds
    refetchOnWindowFocus: true,
  });
}
```

```typescript
// src/hooks/use-reallocation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { z } from 'zod';
import { reallocationRequestSchema, reallocationResponseSchema } from '@/lib/validations/reallocation.schema';

type ReallocationRequest = z.infer<typeof reallocationRequestSchema>;
type ReallocationResponse = z.infer<typeof reallocationResponseSchema>;

async function executeReallocation(data: ReallocationRequest): Promise<ReallocationResponse> {
  const response = await fetch('/api/reallocate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Reallocation failed');
  }

  return reallocationResponseSchema.parse(await response.json());
}

export function useReallocation() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: executeReallocation,
    onSuccess: () => {
      // Invalidate workload query to refetch updated data
      queryClient.invalidateQueries({ queryKey: ['workload'] });
    },
  });
}
```

---

## 8. Reallocation Business Logic Options

### Option A: Round-Robin Distribution

Distributes customers evenly across target beekeepers in order.

```typescript
// src/lib/services/reallocation.service.ts

interface ReallocationParams {
  currentBeekeeper: string;
  targetBeekeeperArray: string[];
  accountOrLead: 'account' | 'lead';
  customerType: string;
}

interface ReallocationResult {
  affectedCount: number;
  distribution: { [beekeeper: string]: number };
}

async function roundRobinReallocation(params: ReallocationParams): Promise<ReallocationResult> {
  const { currentBeekeeper, targetBeekeeperArray, accountOrLead, customerType } = params;

  return sequelize.transaction(async (transaction) => {
    // Get all customers to reallocate
    const repository = accountOrLead === 'account' ? accountRepository : leadRepository;
    const customers = await repository.findByOwner(currentBeekeeper, customerType, { transaction });

    const distribution: { [beekeeper: string]: number } = {};
    targetBeekeeperArray.forEach(bk => distribution[bk] = 0);

    // Round-robin assignment
    for (let i = 0; i < customers.length; i++) {
      const targetIndex = i % targetBeekeeperArray.length;
      const targetBeekeeper = targetBeekeeperArray[targetIndex];

      await repository.updateOwner(
        customers[i].id,
        targetBeekeeper,
        { transaction }
      );

      distribution[targetBeekeeper]++;
    }

    return {
      affectedCount: customers.length,
      distribution,
    };
  });
}
```

**Pros:**
- Simple and predictable
- Ensures even distribution

**Cons:**
- Doesn't consider existing workload

---

### Option B: Workload-Based Distribution

Distributes to equalize workload across all beekeepers.

```typescript
async function workloadBasedReallocation(params: ReallocationParams): Promise<ReallocationResult> {
  const { currentBeekeeper, targetBeekeeperArray, accountOrLead, customerType } = params;

  return sequelize.transaction(async (transaction) => {
    // Get current workload for all target beekeepers
    const currentWorkloads = await getWorkloadsForBeekeepers(
      targetBeekeeperArray,
      accountOrLead,
      customerType,
      { transaction }
    );

    // Get customers to reallocate
    const repository = accountOrLead === 'account' ? accountRepository : leadRepository;
    const customers = await repository.findByOwner(currentBeekeeper, customerType, { transaction });

    // Calculate target workload (average after redistribution)
    const totalToDistribute = customers.length;
    const currentTotal = Object.values(currentWorkloads).reduce((a, b) => a + b, 0);
    const targetAverage = Math.floor((currentTotal + totalToDistribute) / targetBeekeeperArray.length);

    const distribution: { [beekeeper: string]: number } = {};
    targetBeekeeperArray.forEach(bk => distribution[bk] = 0);

    // Assign to beekeepers below average first
    let customerIndex = 0;

    // Sort by current workload (lowest first)
    const sortedBeekeepers = targetBeekeeperArray.sort(
      (a, b) => currentWorkloads[a] - currentWorkloads[b]
    );

    for (const beekeeper of sortedBeekeepers) {
      const currentLoad = currentWorkloads[beekeeper];
      const toAssign = Math.min(targetAverage - currentLoad, customers.length - customerIndex);

      if (toAssign > 0) {
        for (let i = 0; i < toAssign && customerIndex < customers.length; i++) {
          await repository.updateOwner(
            customers[customerIndex].id,
            beekeeper,
            { transaction }
          );
          distribution[beekeeper]++;
          customerIndex++;
        }
      }
    }

    // Distribute remaining customers round-robin
    while (customerIndex < customers.length) {
      const targetIndex = customerIndex % targetBeekeeperArray.length;
      const targetBeekeeper = targetBeekeeperArray[targetIndex];

      await repository.updateOwner(
        customers[customerIndex].id,
        targetBeekeeper,
        { transaction }
      );
      distribution[targetBeekeeper]++;
      customerIndex++;
    }

    return {
      affectedCount: customers.length,
      distribution,
    };
  });
}
```

**Pros:**
- Balances overall workload
- More equitable distribution

**Cons:**
- More complex logic
- Requires additional queries

---

### Option C: Proportional Distribution

Distributes based on current capacity ratios.

```typescript
async function proportionalReallocation(params: ReallocationParams): Promise<ReallocationResult> {
  // Calculate what percentage of the total workload each beekeeper currently handles
  // Distribute new customers in the same proportions

  const currentWorkloads = await getWorkloadsForBeekeepers(targetBeekeeperArray, ...);
  const totalWorkload = Object.values(currentWorkloads).reduce((a, b) => a + b, 0);

  const proportions = targetBeekeeperArray.map(bk => ({
    beekeeper: bk,
    proportion: totalWorkload > 0 ? currentWorkloads[bk] / totalWorkload : 1 / targetBeekeeperArray.length,
  }));

  // Distribute according to proportions
  // ...
}
```

**Pros:**
- Maintains current workload ratios
- Good for teams with different capacities

**Cons:**
- May perpetuate imbalances
- More complex calculation

---

### Option D: Configurable Strategy

Allow the user to choose the distribution method.

```typescript
// src/lib/services/reallocation.service.ts

type DistributionStrategy = 'round-robin' | 'workload-based' | 'proportional';

interface ReallocationParams {
  currentBeekeeper: string;
  targetBeekeeperArray: string[];
  accountOrLead: 'account' | 'lead';
  customerType: string;
  strategy: DistributionStrategy;  // User-selected
}

export const reallocationService = {
  async executeReallocation(params: ReallocationParams): Promise<ReallocationResult> {
    switch (params.strategy) {
      case 'round-robin':
        return roundRobinReallocation(params);
      case 'workload-based':
        return workloadBasedReallocation(params);
      case 'proportional':
        return proportionalReallocation(params);
      default:
        throw new Error(`Unknown distribution strategy: ${params.strategy}`);
    }
  },
};
```

### Recommendation

Start with **Option A (Round-Robin)** for initial implementation due to simplicity. Add **Option D (Configurable)** as a follow-up enhancement once business rules are finalized.

---

## 9. UI/UX Migration

### Tailwind Configuration

```typescript
// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        // PensionBee brand colors (adjust to match brand)
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#2563eb',  // Main brand blue
          600: '#1d4ed8',
          700: '#1e40af',
          800: '#1e3a8a',
          900: '#1e3a8a',
        },
        honey: {
          50: '#fffbeb',
          100: '#fef3c7',
          200: '#fde68a',
          300: '#fcd34d',
          400: '#fbbf24',  // Accent yellow
          500: '#f59e0b',
          600: '#d97706',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
    },
  },
  plugins: [],
};

export default config;
```

### Component Examples

#### Tabs Component

```tsx
// src/components/ui/tabs.tsx
'use client';

import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';

const Tabs = TabsPrimitive.Root;

const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className={cn(
      'inline-flex h-10 items-center justify-center gap-1 border-b border-gray-200',
      className
    )}
    {...props}
  />
));
TabsList.displayName = TabsPrimitive.List.displayName;

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      'inline-flex items-center justify-center whitespace-nowrap px-4 py-2',
      'text-sm font-medium text-gray-500 transition-all',
      'hover:text-gray-700',
      'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
      'disabled:pointer-events-none disabled:opacity-50',
      'data-[state=active]:border-b-2 data-[state=active]:border-primary-500',
      'data-[state=active]:text-primary-600 data-[state=active]:font-semibold',
      '-mb-px', // Overlap with border
      className
    )}
    {...props}
  />
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;

const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className={cn(
      'mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
      className
    )}
    {...props}
  />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;

export { Tabs, TabsList, TabsTrigger, TabsContent };
```

#### Operations Page

```tsx
// src/app/(dashboard)/operations/page.tsx
'use client';

import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { AccountDistributionsTab } from '@/components/operations/account-distributions-tab';
import { LeadDistributionsTab } from '@/components/operations/lead-distributions-tab';
import { ReallocationsTab } from '@/components/operations/reallocations-tab';
import { useWorkload } from '@/hooks/use-workload';
import { Loader2 } from 'lucide-react';

export default function OperationsPage() {
  const { data, isLoading, error } = useWorkload();

  if (isLoading) {
    return (
      <div className="flex h-64 items-center justify-center">
        <Loader2 className="h-8 w-8 animate-spin text-primary-500" />
      </div>
    );
  }

  if (error) {
    return (
      <div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
        Failed to load workload data. Please try again.
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-bold text-gray-900">Hive Operations</h1>
        <p className="mt-1 text-sm text-gray-500">
          Manage customer ownership and beekeeper workload distribution
        </p>
      </div>

      <Tabs defaultValue="accounts" className="w-full">
        <TabsList>
          <TabsTrigger value="accounts">Account Distributions</TabsTrigger>
          <TabsTrigger value="leads">Lead Distributions</TabsTrigger>
          <TabsTrigger value="reallocations">Reallocations</TabsTrigger>
        </TabsList>

        <TabsContent value="accounts">
          <AccountDistributionsTab data={data} />
        </TabsContent>

        <TabsContent value="leads">
          <LeadDistributionsTab data={data} />
        </TabsContent>

        <TabsContent value="reallocations">
          <ReallocationsTab data={data} />
        </TabsContent>
      </Tabs>
    </div>
  );
}
```

#### Reallocations Tab (Tailwind)

```tsx
// src/components/operations/reallocations-tab.tsx
'use client';

import { useState, useMemo } from 'react';
import { useReallocation } from '@/hooks/use-reallocation';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { WorkloadData } from '@/types';

interface ReallocationsTabProps {
  data: WorkloadData | undefined;
}

export function ReallocationsTab({ data }: ReallocationsTabProps) {
  const [currentOwner, setCurrentOwner] = useState<string>('');
  const [accountOrLead, setAccountOrLead] = useState<'account' | 'lead'>('account');
  const [customerType, setCustomerType] = useState<string>('');
  const [selectedNewOwners, setSelectedNewOwners] = useState<string[]>([]);

  const reallocation = useReallocation();

  const ownerEmails = useMemo(() => {
    return Object.keys(data?.accountCountByOwnerEmail ?? {}).sort();
  }, [data]);

  const customerTypeOptions = useMemo(() => {
    if (accountOrLead === 'account') {
      return [
        { label: 'Pension Live', value: 'pensionLive' },
        { label: 'Other Accounts', value: 'otherAccounts' },
      ];
    }
    return [
      { label: 'Contribution First', value: 'contributionFirst' },
      { label: 'Other Leads', value: 'otherLeads' },
    ];
  }, [accountOrLead]);

  const availableNewOwners = useMemo(() => {
    return ownerEmails.filter((email) => email !== currentOwner);
  }, [ownerEmails, currentOwner]);

  const isFormValid = currentOwner && customerType && selectedNewOwners.length > 0;

  const handleSubmit = async () => {
    if (!isFormValid) return;

    try {
      await reallocation.mutateAsync({
        currentBeekeeper: currentOwner,
        targetBeekeeperArray: selectedNewOwners,
        accountOrLead,
        customerType,
      });

      // Reset form on success
      setCurrentOwner('');
      setCustomerType('');
      setSelectedNewOwners([]);
    } catch (error) {
      // Error handled by mutation
    }
  };

  const toggleOwnerSelection = (email: string) => {
    setSelectedNewOwners((prev) =>
      prev.includes(email)
        ? prev.filter((e) => e !== email)
        : [...prev, email]
    );
  };

  return (
    <div className="rounded-lg border border-gray-200 bg-gray-50 p-6">
      <h3 className="mb-6 text-lg font-semibold text-gray-900">Transfer Ownership</h3>

      <div className="grid gap-6 md:grid-cols-2">
        {/* Current Owner */}
        <div className="space-y-2">
          <label className="block text-sm font-medium text-gray-700">
            Current Owner
          </label>
          <Select value={currentOwner} onValueChange={setCurrentOwner}>
            <SelectTrigger>
              <SelectValue placeholder="Select current owner" />
            </SelectTrigger>
            <SelectContent>
              {ownerEmails.map((email) => (
                <SelectItem key={email} value={email}>
                  {email}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>

        {/* Account or Lead */}
        <div className="space-y-2">
          <label className="block text-sm font-medium text-gray-700">
            Record Type
          </label>
          <div className="flex gap-4">
            <label className="flex cursor-pointer items-center gap-2">
              <input
                type="radio"
                name="accountOrLead"
                value="account"
                checked={accountOrLead === 'account'}
                onChange={() => {
                  setAccountOrLead('account');
                  setCustomerType('');
                }}
                className="h-4 w-4 text-primary-500 focus:ring-primary-500"
              />
              <span className="text-sm text-gray-700">Account</span>
            </label>
            <label className="flex cursor-pointer items-center gap-2">
              <input
                type="radio"
                name="accountOrLead"
                value="lead"
                checked={accountOrLead === 'lead'}
                onChange={() => {
                  setAccountOrLead('lead');
                  setCustomerType('');
                }}
                className="h-4 w-4 text-primary-500 focus:ring-primary-500"
              />
              <span className="text-sm text-gray-700">Lead</span>
            </label>
          </div>
        </div>

        {/* Customer Type */}
        <div className="space-y-2">
          <label className="block text-sm font-medium text-gray-700">
            Customer Type
          </label>
          <Select value={customerType} onValueChange={setCustomerType}>
            <SelectTrigger>
              <SelectValue placeholder="Select customer type" />
            </SelectTrigger>
            <SelectContent>
              {customerTypeOptions.map((opt) => (
                <SelectItem key={opt.value} value={opt.value}>
                  {opt.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>

        {/* New Owners Multi-Select */}
        <div className="space-y-2 md:col-span-2">
          <label className="block text-sm font-medium text-gray-700">
            New Owner(s)
          </label>
          <div className="max-h-40 overflow-y-auto rounded-md border border-gray-300 bg-white p-2">
            {availableNewOwners.map((email) => (
              <label
                key={email}
                className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-gray-50"
              >
                <input
                  type="checkbox"
                  checked={selectedNewOwners.includes(email)}
                  onChange={() => toggleOwnerSelection(email)}
                  className="h-4 w-4 rounded text-primary-500 focus:ring-primary-500"
                />
                <span className="text-sm text-gray-700">{email}</span>
              </label>
            ))}
          </div>
          {selectedNewOwners.length > 0 && (
            <p className="text-xs text-gray-500">
              {selectedNewOwners.length} owner(s) selected
            </p>
          )}
        </div>
      </div>

      {/* Submit */}
      <div className="mt-6">
        <Button
          onClick={handleSubmit}
          disabled={!isFormValid || reallocation.isPending}
          className="w-full md:w-auto"
        >
          {reallocation.isPending ? 'Submitting...' : 'Submit Reallocation'}
        </Button>

        {reallocation.isError && (
          <Alert variant="destructive" className="mt-4">
            <AlertDescription>
              {reallocation.error?.message || 'Failed to submit reallocation request'}
            </AlertDescription>
          </Alert>
        )}

        {reallocation.isSuccess && (
          <Alert className="mt-4 border-green-200 bg-green-50 text-green-700">
            <AlertDescription>
              Reallocation completed successfully
            </AlertDescription>
          </Alert>
        )}
      </div>
    </div>
  );
}
```

---

## 10. Deployment Strategy

### Heroku Configuration

```yaml
# Procfile
web: npm start
```

```json
// package.json scripts
{
  "scripts": {
    "build": "next build",
    "start": "next start -p $PORT"
  }
}
```

### Environment Variables

```bash
# .env.example

# Database
DATABASE_URL=postgres://user:pass@host:5432/dbname

# App
NODE_ENV=production
NEXT_PUBLIC_APP_URL=https://hive-switch.herokuapp.com

# Security
BYPASS_AUTH=false  # Only for development

# Cloudflare Access (optional verification)
CF_ACCESS_TEAM_DOMAIN=pensionbee
CF_ACCESS_AUD=<audience-tag>
```

### Heroku Setup Commands

```bash
# Create Heroku app
heroku create hive-switch --team=pensionbee

# Add buildpack for Node.js
heroku buildpacks:set heroku/nodejs -a hive-switch

# Set environment variables
heroku config:set NODE_ENV=production -a hive-switch
heroku config:set DATABASE_URL=<same-as-web-app-alpha> -a hive-switch

# Deploy
git push heroku main

# View logs
heroku logs --tail -a hive-switch
```

### VPN Access Configuration

#### Option 1: Cloudflare Access (Recommended)

1. **Configure Application in Cloudflare Zero Trust:**
   - Self-hosted application
   - Domain: `hive-switch.herokuapp.com`
   - Authentication: Cloudflare WARP required

2. **No changes needed in Heroku** - Cloudflare acts as reverse proxy

#### Option 2: Heroku Private Spaces + VPN

```
# More expensive but provides network-level isolation
# Requires Heroku Enterprise
```

#### Option 3: IP Allowlisting (Basic)

```typescript
// src/middleware.ts
const ALLOWED_IPS = process.env.ALLOWED_IPS?.split(',') || [];

export function middleware(request: NextRequest) {
  const clientIp = request.ip || request.headers.get('x-forwarded-for');

  if (ALLOWED_IPS.length > 0 && !ALLOWED_IPS.includes(clientIp)) {
    return new NextResponse('Forbidden', { status: 403 });
  }

  return NextResponse.next();
}
```

> **Note**: IP allowlisting is less secure than Cloudflare Access and harder to maintain with dynamic VPN IPs. Recommend pursuing Cloudflare Access integration.

---

## 11. Testing Strategy

### Unit Tests

```typescript
// tests/unit/services/reallocation.service.test.ts
import { reallocationService } from '@/lib/services/reallocation.service';

describe('ReallocationService', () => {
  describe('roundRobinReallocation', () => {
    it('distributes customers evenly across beekeepers', async () => {
      // Mock setup
      const mockCustomers = Array(10).fill(null).map((_, i) => ({ id: i }));
      jest.spyOn(accountRepository, 'findByOwner').mockResolvedValue(mockCustomers);
      jest.spyOn(accountRepository, 'updateOwner').mockResolvedValue(undefined);

      const result = await reallocationService.executeReallocation({
        currentBeekeeper: 'old@example.com',
        targetBeekeeperArray: ['new1@example.com', 'new2@example.com'],
        accountOrLead: 'account',
        customerType: 'pensionLive',
        strategy: 'round-robin',
      });

      expect(result.affectedCount).toBe(10);
      expect(result.distribution['new1@example.com']).toBe(5);
      expect(result.distribution['new2@example.com']).toBe(5);
    });
  });
});
```

### Integration Tests

```typescript
// tests/integration/api/workload.test.ts
import { GET } from '@/app/api/workload/route';

describe('GET /api/workload', () => {
  it('returns workload data', async () => {
    const response = await GET();
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveProperty('beekeepers');
    expect(data).toHaveProperty('accountCountByOwnerEmail');
    expect(data).toHaveProperty('leadCountByOwnerEmail');
  });
});
```

### E2E Tests

```typescript
// tests/e2e/operations.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Operations Page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/operations');
  });

  test('displays all three tabs', async ({ page }) => {
    await expect(page.getByRole('tab', { name: 'Account Distributions' })).toBeVisible();
    await expect(page.getByRole('tab', { name: 'Lead Distributions' })).toBeVisible();
    await expect(page.getByRole('tab', { name: 'Reallocations' })).toBeVisible();
  });

  test('can switch between tabs', async ({ page }) => {
    await page.getByRole('tab', { name: 'Leads' }).click();
    await expect(page.getByText('Contribution First')).toBeVisible();
  });

  test('reallocation form validates required fields', async ({ page }) => {
    await page.getByRole('tab', { name: 'Reallocations' }).click();

    const submitButton = page.getByRole('button', { name: 'Submit Reallocation' });
    await expect(submitButton).toBeDisabled();
  });
});
```

---

## 12. Migration Checklist

### Phase 1: Repository Setup (Day 1-2)
- [ ] Create new Git repository
- [ ] Initialize Next.js 14 project with App Router
- [ ] Configure TypeScript, ESLint, Prettier
- [ ] Set up Tailwind CSS
- [ ] Create initial folder structure
- [ ] Set up environment variables template

### Phase 2: Database Layer (Day 3-4)
- [ ] Install and configure Sequelize v5.22.5
- [ ] Port Account model from `PostgresAccount.js`
- [ ] Port Lead model from `PostgresLead.js`
- [ ] Create repository layer
- [ ] Write unit tests for repositories
- [ ] Test connection to shared database (staging)

### Phase 3: API Routes (Day 5-6)
- [ ] Implement GET `/api/workload` endpoint
- [ ] Implement POST `/api/reallocate` endpoint
- [ ] Implement GET `/api/health` endpoint
- [ ] Add Zod validation schemas
- [ ] Write integration tests

### Phase 4: UI Components (Day 7-9)
- [ ] Create base UI components (Button, Select, Table, Tabs)
- [ ] Port AccountDistributionsTab with Tailwind
- [ ] Port LeadDistributionsTab with Tailwind
- [ ] Port ReallocationsTab with Tailwind
- [ ] Create dashboard layout
- [ ] Add loading and error states

### Phase 5: Authentication & Security (Day 10-11)
- [ ] Implement Next.js middleware
- [ ] Integrate Cloudflare Access headers
- [ ] Add audit logging
- [ ] Test VPN access restriction

### Phase 6: Testing & QA (Day 12-13)
- [ ] Complete unit test coverage
- [ ] Complete integration tests
- [ ] E2E tests with Playwright
- [ ] Manual testing on staging
- [ ] Security review

### Phase 7: Deployment (Day 14)
- [ ] Set up Heroku application
- [ ] Configure environment variables
- [ ] Deploy to staging
- [ ] Configure Cloudflare Access
- [ ] Deploy to production
- [ ] Smoke tests

### Phase 8: Documentation & Handover (Day 15)
- [ ] Update README with setup instructions
- [ ] Document API endpoints
- [ ] Create runbook for common issues
- [ ] Team walkthrough

---

## 13. Alternative Architecture: Microservice (Option C)

### Overview

A shared microservice that both `web-app-alpha` and `hive-switch` can call.

```
┌──────────────────┐     ┌──────────────────┐
│  web-app-alpha   │     │   hive-switch    │
│   (Legacy App)   │     │    (New App)     │
└────────┬─────────┘     └────────┬─────────┘
         │                        │
         │  HTTP/gRPC             │  HTTP/gRPC
         │                        │
         └────────┬───────────────┘
                  ▼
    ┌──────────────────────────────┐
    │   hive-management-service    │
    │     (Shared Microservice)    │
    │                              │
    │  - GET /workload             │
    │  - POST /reallocate          │
    │  - Owns database access      │
    │  - Single source of truth    │
    └──────────────┬───────────────┘
                   │
                   ▼
    ┌──────────────────────────────┐
    │      Postgres Database       │
    └──────────────────────────────┘
```

### Technology Options

| Option | Stack | Pros | Cons |
|--------|-------|------|------|
| Node.js Express | Express + Sequelize | Familiar tech, reuse existing code | Another Node service to maintain |
| Python FastAPI | FastAPI + SQLAlchemy | Clean API design, good docs | Different tech stack |
| Go | Chi/Gin + sqlc | Performance, type safety | Learning curve |

### When to Consider This Approach

1. **Multiple consumers**: If more applications will need hive management data
2. **Data ownership**: Want a single service to own account/lead ownership logic
3. **Scalability**: Need to scale this functionality independently
4. **Team structure**: Dedicated team for hive management domain

### Why Not Recommended Initially

1. **Operational overhead**: Additional service to deploy, monitor, maintain
2. **Latency**: Extra network hop for every request
3. **Complexity**: Distributed system concerns (service discovery, circuit breakers)
4. **Single consumer**: Currently only internal operations team needs this

### Recommendation

Start with **Option A** (direct database connection). Consider migrating to Option C if:
- Multiple applications need this functionality
- Team grows and can dedicate resources to microservice
- Scalability becomes a concern

---

## 14. Risk Assessment & Mitigations

| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| **Database connection conflicts** | Medium | High | Use lower connection pool limits; monitor connections |
| **Sequelize version mismatch** | Low | High | Lock exact versions; comprehensive testing |
| **Data integrity issues** | Medium | Critical | Use transactions; add audit logging; implement soft deletes |
| **VPN bypass** | Low | High | Cloudflare Access provides cryptographic verification |
| **Performance degradation** | Low | Medium | Monitor query performance; add indexes if needed |
| **Accidental bulk reallocation** | Medium | High | Add confirmation dialog; implement undo functionality |

### Data Integrity Safeguards

```typescript
// Pre-reallocation validation
async function validateReallocation(params: ReallocationParams): Promise<ValidationResult> {
  const errors: string[] = [];

  // Check current owner exists
  const currentOwnerExists = await accountRepository.ownerExists(params.currentBeekeeper);
  if (!currentOwnerExists) {
    errors.push('Current owner not found');
  }

  // Check target owners exist
  for (const target of params.targetBeekeeperArray) {
    const exists = await accountRepository.ownerExists(target);
    if (!exists) {
      errors.push(`Target owner ${target} not found`);
    }
  }

  // Check affected count isn't unexpectedly large
  const affectedCount = await accountRepository.countByOwner(
    params.currentBeekeeper,
    params.customerType
  );
  if (affectedCount > 1000) {
    errors.push(`Warning: This will affect ${affectedCount} records. Please confirm.`);
  }

  return { valid: errors.length === 0, errors };
}
```

---

## 15. Future Considerations

### Potential Enhancements

1. **Undo/Rollback Functionality**
   - Store reallocation history
   - Allow reverting recent changes

2. **Scheduled Reallocations**
   - Set reallocations to execute at specific times
   - Useful for off-hours bulk operations

3. **Preview Mode**
   - Show what would happen before executing
   - Dry-run capability

4. **Bulk Import**
   - CSV upload for complex reallocations
   - Mapping validation

5. **Notifications**
   - Slack/email notifications on completion
   - Alert on failures

6. **Analytics Dashboard**
   - Historical reallocation trends
   - Workload balance over time

### Extensibility Architecture

```
src/
├── features/
│   ├── workload/           # Current feature
│   ├── reallocations/      # Current feature
│   ├── bulk-operations/    # Future: CSV imports
│   ├── scheduling/         # Future: Scheduled tasks
│   └── analytics/          # Future: Dashboards
```

---

## Summary

This plan provides a comprehensive roadmap for migrating the hive switch functionality to a standalone Next.js application. Key decisions:

1. **Architecture**: Direct database connection (Option A) for simplicity
2. **Tech Stack**: Next.js 14, Sequelize 5.22.5, Tailwind CSS
3. **Security**: Cloudflare WARP Zero Trust for VPN access
4. **Database**: Shared Postgres with conservative connection pooling
5. **Timeline**: ~15 working days for full implementation

---

*Document generated: December 2025*
*Version: 1.0*

