quest-vorteq/CLAUDE.md
Lorentz 9fad8af225 feat: add local dev workflow with hot reload via Traefik
Route dev domain through Traefik to host:3001 instead of Docker
container, enabling instant hot reload. Expose PostgreSQL and Redis
ports to localhost for local dev server connectivity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 19:55:26 +00:00

19 KiB

Vorteq Quest Portal - Claude Code Instructions

Project Overview

Rebuilding the Vorteq Quest customer portal from a legacy Laravel 9 / Windows Server / Savvior stack to a modern Next.js / TypeScript / PostgreSQL stack running in Docker on Linux. The full PRD is in docs/Vorteq_Quest_PRD_v1.0.docx.

Tech Stack

  • Runtime: Node.js 22 LTS
  • Framework: Next.js 15 with App Router
  • Language: TypeScript (strict mode)
  • ORM (App DB): Prisma with PostgreSQL 16
  • ORM (Epicor): mssql package (read-only connection to SQL Server)
  • Auth: Better Auth with credentials provider + custom session enrichment
  • UI: shadcn/ui + Tailwind CSS
  • Data Tables: TanStack Table v8
  • State: React Server Components by default, client components only when needed
  • Cache/Queue: Redis 7 via BullMQ
  • Email: Microsoft Graph API (Office 365)
  • PDF: Puppeteer or @react-pdf/renderer
  • Testing: Vitest (unit/integration), Playwright (E2E)
  • Containers: Docker + Docker Compose
  • Reverse Proxy: Traefik v3 with Cloudflare DNS challenge for SSL
  • CI/CD: Forgejo Actions with self-hosted runner
  • Registry: Forgejo built-in OCI container registry

Reference Documentation

All legacy system documentation is in the docs/ directory:

  • vorteqcoil-portal-documentation.md — Complete interface documentation of the legacy portal: navigation, sections, column layouts, business logic. Reference when building UI pages.
  • vorteqcoil-visual-documentation.html — Visual reference guide with color coding system and UI patterns. Reference for design decisions.
  • VorteqDiscovery_20260206_205453.txt — Server infrastructure discovery: IIS config, scheduled tasks, services.
  • VorteqAppDiscovery_20260206_210519.txt — Application code discovery: Laravel routes, controllers, models, views, Blade templates. Reference when implementing features.
  • VorteqEpicorDB_20260210_211850.txt — Epicor database schema: tables, stored procedures, views. Reference when writing Epicor queries.
  • EpicorDatabase.php — Legacy PHP database class with all Epicor query implementations. Primary reference for porting queries to TypeScript.
  • _FILE_LISTING.txt — Complete file listing of the legacy Laravel application.
  • _MODEL_MAP.txt — Legacy Eloquent model-to-table mappings. Reference for Prisma schema design.
  • _SQL_QUERIES_FOUND.txt — All SQL queries extracted from the legacy codebase. Reference for service layer implementation.

Infrastructure

Server

  • Host: expvtcasp01 (Linux/Ubuntu)
  • Netbird IP: 100.89.62.20
  • Stack directory: /opt/stacks/vorteq
  • Runner directory: /opt/stacks/forgejo-runner

Repository

  • URL: forgejo.wulfconsulting.cloud/lorentz/quest-vorteq
  • SSH: ssh://git@forgejo.wulfconsulting.cloud:222/lorentz/quest-vorteq.git (port 222)
  • Registry: forgejo.wulfconsulting.cloud/lorentz/quest-vorteq

Domains

  • Production: quest.vorteq.wulf.cloud
  • Development: dev.quest.vorteq.wulf.cloud
  • Testing: testing.vorteq.wulf.cloud
  • Traefik Dashboard: traefik.vorteq.wulf.cloud

Running Services

  • Traefik: Reverse proxy, auto-SSL via Cloudflare DNS challenge
  • PostgreSQL 16: Three databases — vorteq_dev, vorteq_test, vorteq_prod
  • Redis 7: Three DB numbers — 0 (dev), 1 (test), 2 (prod)
  • Forgejo Runner: Docker-in-Docker, registered as expvtcasp01

CI/CD Pipeline

Push to main → build Docker image → tag as :latest → push to Forgejo registry → deploy to prod Push to development → build Docker image → tag as :dev → push to Forgejo registry → deploy to dev

Project Structure

quest-vorteq/
├── CLAUDE.md                    # This file
├── TASKS.md                     # Task tracking
├── docker-compose.yml           # Infrastructure: traefik, postgres, redis, app containers
├── Dockerfile                   # Multi-stage Next.js build (node:22-alpine, standalone output)
├── .forgejo/
│   └── workflows/
│       └── deploy.yml           # CI/CD pipeline
├── traefik/
│   ├── traefik.yml              # Traefik configuration
│   ├── acme.json                # SSL certificates (gitignored)
│   └── config/
│       └── dev-local.yml        # Routes dev domain → host:3001 for local dev server
├── prisma/
│   ├── schema.prisma            # PostgreSQL schema (auth + quest + ship/alloc/finance/paint/doc/wave/lts domains)
│   ├── migrations/
│   └── seed.ts                  # Seed data / migration scripts
├── src/
│   ├── app/                     # Next.js App Router
│   │   ├── (auth)/              # Auth route group
│   │   │   ├── login/
│   │   │   ├── forgot-password/
│   │   │   └── account-request/
│   │   ├── (portal)/            # Authenticated route group
│   │   │   ├── dashboard/
│   │   │   ├── inventory/       # Summary + 6 category pages + detail drill-down
│   │   │   ├── orders/
│   │   │   └── select-company/
│   │   ├── api/
│   │   │   ├── auth/            # Better Auth catch-all + custom login + set-active-company
│   │   │   ├── health/
│   │   │   └── account-request/
│   │   ├── layout.tsx
│   │   ├── globals.css
│   │   └── page.tsx
│   ├── components/
│   │   ├── ui/                  # shadcn/ui primitives (button, card, input, table, toast, etc.)
│   │   ├── dashboard/           # summary-card, quick-nav-card, recent-orders-table, recent-shipments-table
│   │   ├── inventory/           # inventory-summary-table, inventory-detail-table
│   │   ├── orders/              # orders-table
│   │   ├── layout/              # portal-sidebar, portal-header, breadcrumb
│   │   └── company-selector.tsx
│   ├── hooks/
│   │   └── use-toast.ts
│   ├── lib/
│   │   ├── db.ts                # Prisma client singleton
│   │   ├── epicor.ts            # MSSQL connection pool + query helpers + typed errors
│   │   ├── auth.ts              # Better Auth config (Prisma adapter, credentials provider)
│   │   ├── auth-client.ts       # Better Auth client-side helpers
│   │   ├── session.ts           # Quest-specific session data (cookie-based)
│   │   ├── permissions.ts       # RBAC: getQuestSession, requirePermission, hasPermission, isAdmin, isSubUser, getActiveCompany, getUserCompanies, PermissionRules constants
│   │   └── utils.ts             # General utilities (cn, etc.)
│   ├── services/
│   │   ├── inventory.ts         # Epicor SP calls for 6 inventory categories (summary + detail)
│   │   ├── orders.ts            # portal_Orders view queries, HDC/HDM exception handling
│   │   └── dashboard.ts         # Dashboard data aggregation
│   ├── types/
│   │   ├── epicor.ts            # Epicor query result types
│   │   ├── portal.ts            # Portal-specific types
│   │   └── index.ts
│   └── middleware.ts             # Rate limiting, public route allowlist, auth check (currently disabled in dev)
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── scripts/
│   └── migrate-data.ts          # SQL Server → PostgreSQL migration
└── docs/                        # Legacy system documentation (see Reference Documentation above)

Not Yet Created (Planned)

src/app/(portal)/shipments/
src/app/(portal)/coil-activity/
src/app/(portal)/invoices/
src/app/(portal)/shipment-requests/
src/app/(portal)/allocation-requests/
src/app/(portal)/jobs/
src/app/(portal)/admin/
src/services/shipments.ts        # Shipment data access
src/services/coil-activity.ts    # Coil activity data access
src/services/ship-requests.ts    # Shipment request business logic
src/services/alloc-requests.ts   # Allocation request business logic
src/services/invoices.ts         # Invoice processing
src/lib/email.ts                 # Microsoft Graph email service
src/lib/pdf.ts                   # PDF generation service
src/jobs/                        # BullMQ workers (shipment-nag, order-ack-email, etc.)
src/components/data-table/       # Reusable TanStack Table wrapper
src/components/forms/            # Shared form components

Current Development State

Phase 1 (Foundation): Complete — project scaffold, Docker infra, Prisma schema (all domains), Epicor connection, Better Auth, middleware, permissions, company selector.

Phase 2 (Core Features): In progress

  • Dashboard: implemented (summary cards, quick nav, recent orders/shipments tables)
  • Inventory: implemented (all 6 category summaries + detail drill-down)
  • Orders: implemented (list page with search and CSV export, HDC exception)
  • Remaining: shipments, coil-activity, invoices, shipment-requests, allocation-requests, jobs, admin pages

Dev workflow: Local Next.js dev server with hot reload via PORT=3001 npm run dev. Traefik routes dev.quest.vorteq.wulf.cloud to host:3001 via traefik/config/dev-local.yml. The app-dev Docker container should be stopped during local dev to avoid route conflicts.

Auth status: Better Auth is configured with Prisma adapter. A custom login endpoint exists at /api/auth/login. Auth middleware is temporarily disabled in dev mode (DEV_MODE=true bypasses to a mock admin session). The dev domain (dev.quest.vorteq.wulf.cloud) also bypasses auth in middleware.

Git: Currently only main branch. No development branch yet.

Coding Conventions

General

  • TypeScript strict mode, no any types
  • Use type over interface unless extending
  • Prefer const over let, never var
  • Use named exports, not default exports (except page.tsx and layout.tsx)
  • Error handling: use typed error classes, not string throws
  • All API responses: { data: T } on success, { error: string, code: string } on failure

Naming

  • Files: kebab-case (shipment-requests.ts)
  • Components: PascalCase (ShipmentRequestForm.tsx)
  • Functions/variables: camelCase
  • Database columns (Prisma): snake_case
  • API routes: kebab-case (/api/shipment-requests)
  • Environment variables: SCREAMING_SNAKE_CASE

Formatting (Prettier)

  • Single quotes, semicolons, trailing commas (es5)
  • 2-space indentation, 80 char print width
  • Tailwind class sorting via prettier-plugin-tailwindcss

Database

  • Prisma schema uses snake_case for all fields
  • Map legacy PascalCase column names with @map("LegacyName") where needed
  • Always use parameterized queries for Epicor MSSQL — never string interpolation
  • Epicor queries go in src/services/ with typed return values
  • PostgreSQL queries go through Prisma client exclusively

Component Pattern

// Server component by default
export async function InventorySummary({ category }: { category: string }) {
  const data = await getInventorySummary(category);
  return <DataTable columns={columns} data={data} />;
}

// Client component only when interactivity needed
'use client';
export function ShipmentRequestCart() { ... }

Epicor Connection Patterns

Connection (src/lib/epicor.ts)

  • Singleton connection pool via mssql package
  • Typed error classes: EpicorConnectionError, EpicorQueryError, EpicorTimeoutError
  • execStoredProc<T>(name, params) — execute stored procedure, returns T
  • execQuery<T>(query, params) — execute parameterized SQL, returns T
  • execPortalStoredProc<T>(name, custId, dbName, sub, additionalParams) — convenience wrapper with standard portal params
  • getPortalDbName() — returns [VorteqPortal] (bracketed DB name for cross-DB SP parameter)
  • checkConnection() — health check
  • Graceful shutdown on SIGINT/SIGTERM

Inventory Stored Procedure Parameters

V6 procedures (WIP, Finished Goods, Processed Other):

  • @Customer — the selected company's EpicorCustID
  • @DBNAME — portal database name in brackets, e.g., [VorteqPortal]
  • @SUBUSER — 0 or 1 (sub-user flag)
  • Detail variants also accept @PART, @PLANT, @WAREHOUSE for filtering

Non-V6 procedures (Unprocessed, R&R variants):

  • @CUSTID — the selected company's EpicorCustID
  • @DBNAME — portal database name in brackets
  • No sub-user parameter — sub-users are blocked entirely (return empty array)
  • Detail variants accept @part, @plant, @warehouse (lowercase)

Orders Pattern

  • Uses execQuery with parameterized SQL against Epicor views
  • Standard view: dbo.portal_Orders WHERE CustomerID = @CustID
  • HDC exception: uses dbo.portal_OrdersHDC view and maps CustID HDCHDM

Column Mapping

Epicor SP results use PascalCase column names. Services map them to snake_case:

function mapSummaryRow(raw: Record<string, unknown>): InventorySummaryRow {
  return {
    part_num: String(raw.VorteqPartNum ?? ''),
    description: String(raw.VorteqPartDesc ?? ''),
    on_hand_qty: Number(raw.OnHandQty ?? 0),
    // ...
  };
}

Auth & Session Pattern

Better Auth (src/lib/auth.ts)

  • Prisma adapter connecting to auth_user, auth_session, auth_account tables
  • Email + password enabled, email verification disabled
  • 7-day session expiry

Quest Session (src/lib/permissions.ts)

getQuestSession() enriches the Better Auth session with:

  • questUserId — from quest_user table
  • activeCompanyId — from cookie-based session store (src/lib/session.ts)
  • isSubUser — from quest_user.is_sub_user
  • permissionRules[] — collected from user type → permission groups → permission rules
  • userType — e.g., "Admin", "Super Admin", "Customer User"

In dev mode (DEV_MODE=true), returns a mock admin session with all permissions.

Permission Helpers (src/lib/permissions.ts)

  • requirePermission(rule) — throws if user lacks permission
  • hasPermission(rule) — returns boolean
  • isAdmin() / requireAdmin() — check for Admin or Super Admin user type
  • isSubUser() — check sub-user status
  • requireActiveCompany() — throws if no company selected
  • getActiveCompany() — returns the active quest_company record
  • getUserCompanies() — returns accessible companies (admins see all active)
  • PermissionRules — constant object with all permission rule names

Important Business Logic

Company Context

After login, the user's active company is stored in a cookie (quest_session). All Epicor queries are scoped to that company's EpicorCustID. Admin users can switch companies and see all active companies.

Sub-User Restrictions

Users with is_sub_user=true on their quest_user record:

  • See filtered inventory data (SUBUSER=1 parameter to V6 stored procedures)
  • See filtered ship-to addresses (only those matching '%NB HANDY%')
  • Cannot access Unprocessed inventory or R&R data at all

HDC/HDM Customer Exception

Customer 'HDC' uses a special order view (portal_OrdersHDC) and maps to CustID 'HDM' in queries. Handled in src/services/orders.ts.

VGL Customer Exception

Customer 'VGL' uses a special SQL query for coil receipts instead of the standard view.

Commands

# Development
npm run dev              # Start Next.js dev server
npm run build            # Production build
npm run start            # Start production server
npm run lint             # ESLint
npm run typecheck        # TypeScript type checking (tsc --noEmit)
npm test                 # Vitest
npm run test:e2e         # Playwright E2E tests

# Database
npx prisma migrate dev   # Create + apply migration (dev)
npx prisma migrate deploy # Apply migrations (prod)
npx prisma db push       # Push schema without migration
npx prisma generate      # Regenerate Prisma client
npx prisma studio        # Database GUI
tsx prisma/seed.ts       # Run seed script

# Docker
docker compose up -d     # Start all services
docker compose ps        # Check service status
docker compose logs -f vorteq-dev  # Tail dev container logs

# Pre-commit checks
npm run lint && npm run typecheck && npm test

Local Development (Hot Reload)

# Start local dev (instead of Docker container)
docker compose stop app-dev              # Release the competing Traefik route
PORT=3001 npm run dev                    # Run in a tmux pane — hot reload at https://dev.quest.vorteq.wulf.cloud

# Stop local dev (restore containerized version)
# Ctrl+C the dev server, then:
docker compose start app-dev

Traefik routes dev.quest.vorteq.wulf.cloudhttp://172.17.0.1:3001 via traefik/config/dev-local.yml. CI/CD pipeline is unchanged — pushes to main still build and deploy the production container.

Environment Variables

# Application
NODE_ENV=development|test|production
DEV_MODE=true                    # Bypasses auth, uses mock admin session
BETTER_AUTH_URL=https://dev.quest.vorteq.wulf.cloud
BETTER_AUTH_SECRET=<generated>

# PostgreSQL (per-environment, set in docker-compose.yml)
DATABASE_URL=postgresql://postgres:<password>@postgres:5432/vorteq_dev

# Redis (per-environment, set in docker-compose.yml)
REDIS_URL=redis://redis:6379/0

# Epicor SQL Server (read-only)
MSSQL_HOST=wi-e10test
MSSQL_DATABASE=Epicor10Live
MSSQL_USER=portal
MSSQL_PASSWORD=<from-env>
MSSQL_PORT=1433

# Portal database name (for cross-DB stored procedure parameter)
PORTAL_DB_NAME=VorteqPortal

# Microsoft Graph (Email)
MS_GRAPH_CLIENT_ID=629eca61-d554-4763-b14a-565aee7b7ab8
MS_GRAPH_TENANT_ID=291e541e-003c-463c-8834-863b9e7c1ebe
MS_GRAPH_CLIENT_SECRET=<from-env>
MAIL_FROM=quest@vorteqcoil.com

# reCAPTCHA
RECAPTCHA_SITE_KEY=<from-env>
RECAPTCHA_SECRET_KEY=<from-env>

# Cloudflare (Traefik SSL - set in .env at stack level)
CLOUDFLARE_DNS_API_TOKEN=<from-cloudflare>

Testing Requirements

  • Unit tests for all service functions (inventory, orders, shipments data transformations)
  • Integration tests for API routes with mocked DB connections
  • E2E tests for: login flow, inventory browse + drill-down, shipment request workflow, invoice upload
  • Data validation: compare Epicor query results with known-good outputs from the legacy system

Git Workflow

  • main branch: production-ready code → deploys to quest.vorteq.wulf.cloud
  • development branch: integration branch → deploys to dev.quest.vorteq.wulf.cloud (not yet created)
  • Feature branches: feature/F-001-project-scaffold
  • Commit messages: feat(F-001): scaffold Next.js project with TypeScript + Tailwind
  • Run npm run lint && npm run typecheck && npm test before committing
  • Push triggers Forgejo Actions → Docker build → registry push → deploy

Task Tracking

All tasks are tracked in TASKS.md. Mark tasks as:

  • [ ] Not started
  • [~] In progress
  • [x] Complete
  • [!] Blocked (note blocker in comments)