Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cfe5771
chore(wp1): establish engineering baseline and regression harness
cryptorevel Aug 14, 2026
17c416e
feat(fsm): implement field service platform through notifications pha…
cryptorevel Aug 19, 2026
4408474
test(ui): verify notifications in browser and harden phase 9.5
cryptorevel Aug 19, 2026
2f7e5c1
test(calendar): verify business timezone handling
cryptorevel Aug 19, 2026
d63a955
feat(settings): add global business timezone configuration
cryptorevel Aug 19, 2026
944ff9d
fix(ui): preserve mobile lead form sizing
cryptorevel Aug 19, 2026
752f9dc
feat(maps): establish geocoding foundation
cryptorevel Aug 20, 2026
32bf005
feat(maps): add Google geocoding provider
cryptorevel Aug 20, 2026
4b4c753
feat(maps): add dispatcher map view
cryptorevel Aug 20, 2026
9530ba8
feat(maps): add technician route view
cryptorevel Aug 20, 2026
7ff1287
feat(routes): add travel-time integration
cryptorevel Aug 21, 2026
b7e3479
docs(maps): harden API key restriction guidance
cryptorevel Aug 21, 2026
6ed0244
docs(phase11): audit platform generalization boundaries
cryptorevel Aug 21, 2026
21625bd
docs(phase11): correct integration classification in generalization a…
cryptorevel Aug 21, 2026
5b20035
refactor(phase11): establish core module boundaries
cryptorevel Aug 21, 2026
db335bd
refactor(phase11): centralize job type workflow registry
cryptorevel Aug 21, 2026
0f37def
refactor(phase11): extract BC rebate program ownership
cryptorevel Aug 21, 2026
12d9f68
refactor(phase11): establish tenant organization boundary
cryptorevel Aug 21, 2026
122ebb8
fix(phase11): enforce tenant-safe calendar fanout
cryptorevel Aug 21, 2026
998fb96
feat(phase11): add tenant-safe assets and equipment
cryptorevel Aug 22, 2026
a0a12cb
feat(phase12): add versioned quotes and estimates foundation
cryptorevel Aug 24, 2026
ad10cf0
feat(phase13): add contracts and e-sign foundation
cryptorevel Aug 24, 2026
20c8121
feat(phase13a): complete branded document and signing workflows
cryptorevel Aug 25, 2026
8be8779
feat(phase13b): add invoice delivery, payments, and receipts
cryptorevel Aug 25, 2026
50eeec2
fix(phase13c): harden admin settings access
cryptorevel Aug 25, 2026
d3c12e7
feat(phase13d): add tax and jurisdiction settings
cryptorevel Aug 26, 2026
50ef080
docs(phase14): add answermachine integration audit
cryptorevel Aug 26, 2026
8458c21
feat(phase15): add phone operations foundation
cryptorevel Aug 26, 2026
f6e816b
feat(phase16): integrate phone operations with ofs core
cryptorevel Aug 27, 2026
29eaee5
feat(phase17): add organization pricebook
cryptorevel Aug 27, 2026
f17a01e
feat(phase18): add good better best estimates
cryptorevel Aug 27, 2026
17b99a6
fix(phase19a): complete app-wide responsive acceptance
cryptorevel Aug 27, 2026
45f9fb8
feat(phase19b): add maintenance memberships and agreements
cryptorevel Aug 28, 2026
f182b7d
feat(phase19c): add recurring maintenance renewal automation
cryptorevel Aug 28, 2026
ea7eec7
feat(stack2): add Next.js apps/web application and CI verification job
cryptorevel Sep 10, 2026
3c87cfc
Merge origin/main into feat/stack2-verify-web-ci
cryptorevel Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
50 changes: 50 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copy this file to .dev.vars (gitignored) and fill in real values for local
# development. Never commit .dev.vars or real credentials.
#
# See docs/google-calendar-setup.md for how to create these in Google Cloud.

GOOGLE_CLIENT_SECRET=

# Base64-encoded 32-byte key used to encrypt OAuth tokens at rest.
# Generate one with: openssl rand -base64 32
TOKEN_ENCRYPTION_KEY=

# Phase 9.2 notification delivery — Resend (email) and Twilio (SMS). Leaving
# any of these blank is safe: the dispatcher treats an unconfigured channel
# as a normal, retryable "provider_not_configured" outcome, never a crash.
# RESEND_FROM_ADDRESS is NOT a secret — set it in wrangler.toml's [vars]
# instead, see the comment there.
RESEND_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=

# Phase 10.1 — Google Maps Platform Geocoding API. Only used when
# wrangler.toml's GEOCODING_PROVIDER var is set to "google" (default
# "none" — leave that alone unless you actually want real geocoding).
# Leaving this blank while GEOCODING_PROVIDER=google is a safe, disclosed
# configuration failure (geocode attempts persist as "pending", never a
# crash) — see src/server/google-geocoding.ts. Restrict this key in Google
# Cloud Console to the Geocoding API only — it is server-side only (never
# referrer-restricted), so its API restriction is the sole thing limiting
# what it can do if it ever leaked.
GOOGLE_MAPS_API_KEY=

# Phase 10.4 — Google Routes API (Compute Routes). Only used when
# wrangler.toml's ROUTING_PROVIDER var is set to "google" (default "none"
# — leave that alone unless you actually want real travel-time data).
# Leaving this blank while ROUTING_PROVIDER=google is a safe, disclosed
# configuration failure (every leg reports travel time unavailable, never
# a crash) — see src/server/google-routing.ts. Deliberately a SEPARATE key
# from GOOGLE_MAPS_API_KEY above — restrict this one in Google Cloud
# Console to the Routes API only, never reuse the geocoding key here.
GOOGLE_ROUTES_API_KEY=

# Phase 13B — gates the local/mock online payment flow (Section 34:
# "Online Pay gracefully unavailable" when unset — every other financial
# workflow, including manual/on-site payments, works with this blank).
# Used to HMAC-sign/verify the mock payment provider's webhook events —
# generate one with: openssl rand -base64 32. Not a real payment
# processor credential (no production provider is configured by this
# phase) — see src/server/payment-provider.ts.
MOCK_PAYMENT_WEBHOOK_SECRET=
109 changes: 109 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Quality

on:
pull_request:
push:
branches:
- main
- master

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: corepack pnpm install --frozen-lockfile

- name: Typecheck
run: corepack pnpm run typecheck

- name: Test
run: corepack pnpm run test

- name: Lint
run: corepack pnpm run lint

- name: Build
run: corepack pnpm run build

verify-web:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: open_fieldservice_ci
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/open_fieldservice_ci
AUTH_SECRET: ci-only-non-production-secret-at-least-32-chars
APP_URL: http://localhost:3000
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24

- name: Enable Corepack
run: corepack enable

- name: Install dependencies
run: corepack pnpm install --frozen-lockfile

- name: Typecheck
working-directory: apps/web
run: corepack pnpm run typecheck

- name: Lint
working-directory: apps/web
run: corepack pnpm run lint

- name: Migrate database
working-directory: apps/web
run: corepack pnpm exec drizzle-kit migrate

- name: Unit tests
working-directory: apps/web
run: corepack pnpm run test

- name: PostgreSQL integration tests
working-directory: apps/web
run: corepack pnpm run test:integration

- name: Build
working-directory: apps/web
run: corepack pnpm run build

- name: Drizzle drift check
working-directory: apps/web
run: |
before=$(find src/db/migrations -maxdepth 1 -name '*.sql' | wc -l)
corepack pnpm exec drizzle-kit generate
after=$(find src/db/migrations -maxdepth 1 -name '*.sql' | wc -l)
if [ "$before" != "$after" ]; then
echo "Drizzle schema drift detected: schema.ts changed without a generated/committed migration"
exit 1
fi
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
node_modules/
.next/
*.tsbuildinfo
.env*.local
.postgres-test/
dist/
data.db
data.db-shm
data.db-wal
docs/
.wrangler/
coverage/
.dev.vars
.dev-server.stdout.log
.dev-server.stderr.log
.serena/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.19.0
11 changes: 11 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/open_fieldservice
AUTH_SECRET=replace-with-at-least-32-random-characters
APP_URL=http://localhost:3000
SEED_ADMIN_PASSWORD=development-password-change-me
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=open-fieldservice-media
R2_ENDPOINT=https://replace-me.r2.cloudflarestorage.com
MAINTENANCE_SCHEDULER_SECRET=
PHONE_OPERATIONS_ENCRYPTION_KEY=
9 changes: 9 additions & 0 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions apps/web/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
19 changes: 19 additions & 0 deletions apps/web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Open Fieldservice Next.js application

This is the side-by-side migration target. The legacy Preact/Worker application remains the source of truth until individual modules pass parity gates.

## Local setup

1. Copy `.env.example` to `.env.local` and use development-only values.
2. Create a PostgreSQL database.
3. From this directory, run `pnpm drizzle-kit generate` and `pnpm drizzle-kit migrate`.
4. Run `pnpm dev`.

R2 variables are optional until storage features are exercised, but must be supplied as a complete group.

## Verification

- `pnpm typecheck`, `pnpm lint`, and `pnpm test` run the standard checks.
- `pnpm test:integration` runs the PostgreSQL suite against `DATABASE_URL`.
- On this Windows development host, `powershell -ExecutionPolicy Bypass -File scripts/run-postgres-integration.ps1` creates a disposable PostgreSQL 17 cluster on port 55432, recreates the test database, runs the integration suite, and stops the cluster.
- `pnpm db:seed` adds synthetic demonstration records and requires `SEED_ADMIN_PASSWORD`; it never reads legacy D1 data.
9 changes: 9 additions & 0 deletions apps/web/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": { "css": "src/app/globals.css", "baseColor": "slate", "cssVariables": true },
"aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" },
"iconLibrary": "lucide"
}
14 changes: 14 additions & 0 deletions apps/web/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from "drizzle-kit";

if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is required to run Drizzle commands");
}

export default defineConfig({
dialect: "postgresql",
schema: "./src/db/schema/index.ts",
out: "./src/db/migrations",
dbCredentials: { url: process.env.DATABASE_URL },
strict: true,
verbose: true,
});
9 changes: 9 additions & 0 deletions apps/web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTypeScript from "eslint-config-next/typescript";

export default defineConfig([
...nextVitals,
...nextTypeScript,
globalIgnores([".next/**", "next-env.d.ts"]),
]);
7 changes: 7 additions & 0 deletions apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
11 changes: 11 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { NextConfig } from "next";
import { fileURLToPath } from "node:url";

const nextConfig: NextConfig = {
reactStrictMode: true,
serverExternalPackages: ["@react-pdf/renderer", "bcryptjs", "sharp"],
experimental: { serverActions: { bodySizeLimit: "16mb" } },
turbopack: { root: fileURLToPath(new URL("../..", import.meta.url)) },
};

export default nextConfig;
56 changes: 56 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@open-fieldservice/web",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=24.0.0 <25.0.0",
"pnpm": "11.21.0"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest run",
"test:integration": "vitest run --config vitest.integration.config.ts",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:seed": "tsx --conditions=react-server src/db/seed.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.0.0",
"@aws-sdk/s3-request-presigner": "^3.0.0",
"@base-ui/react": "^1.0.0",
"@react-pdf/renderer": "^4.0.0",
"bcryptjs": "^3.0.0",
"drizzle-orm": "^0.44.0",
"lucide-react": "^0.500.0",
"next": "^16.0.0",
"next-auth": "5.0.0-beta.30",
"next-themes": "^0.4.0",
"postgres": "^3.4.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"server-only": "^0.0.1",
"sharp": "^0.34.0",
"sonner": "^2.0.0",
"tw-animate-css": "^1.0.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/bcryptjs": "^2.4.0",
"@types/node": "^24.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"drizzle-kit": "^0.31.0",
"eslint": "^9.0.0",
"eslint-config-next": "^16.0.0",
"fake-indexeddb": "^6.2.5",
"tailwindcss": "^4.0.0",
"tsx": "^4.20.0",
"typescript": "^5.7.0",
"vitest": "^4.0.0"
}
}
7 changes: 7 additions & 0 deletions apps/web/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const postcssConfig = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default postcssConfig;
31 changes: 31 additions & 0 deletions apps/web/scripts/run-postgres-integration.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
$ErrorActionPreference = "Stop"

$postgresBin = "C:\Program Files\PostgreSQL\17\bin"
$cluster = Join-Path $PSScriptRoot "..\.postgres-test"
$data = Join-Path $cluster "data"
$log = Join-Path $cluster "postgres.log"
$port = 55432

if (-not (Test-Path (Join-Path $postgresBin "initdb.exe"))) {
throw "PostgreSQL 17 tools were not found. Set DATABASE_URL and run pnpm test:integration directly."
}

New-Item -ItemType Directory -Force -Path $cluster | Out-Null
if (-not (Test-Path (Join-Path $data "PG_VERSION"))) {
& (Join-Path $postgresBin "initdb.exe") -D $data -U postgres --auth=trust --encoding=UTF8 --no-locale
if ($LASTEXITCODE -ne 0) { throw "initdb failed" }
}

try {
& (Join-Path $postgresBin "pg_ctl.exe") -D $data -l $log -o "-p $port -h 127.0.0.1" -w start
if ($LASTEXITCODE -ne 0) { throw "PostgreSQL test cluster failed to start" }
& (Join-Path $postgresBin "dropdb.exe") -h 127.0.0.1 -p $port -U postgres --if-exists open_fieldservice_test
& (Join-Path $postgresBin "createdb.exe") -h 127.0.0.1 -p $port -U postgres open_fieldservice_test
$env:DATABASE_URL = "postgresql://postgres@127.0.0.1:$port/open_fieldservice_test"
$env:AUTH_SECRET = "integration-test-secret-at-least-32-characters"
pnpm.cmd test:integration
if ($LASTEXITCODE -ne 0) { throw "PostgreSQL integration tests failed" }
}
finally {
& (Join-Path $postgresBin "pg_ctl.exe") -D $data -m fast -w stop
}
6 changes: 6 additions & 0 deletions apps/web/src/app/(auth)/login/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use server";
import { signIn } from "@/auth/auth";

export async function loginAction(formData: FormData): Promise<void> {
await signIn("credentials", { email: formData.get("email"), password: formData.get("password"), organizationSlug: formData.get("organizationSlug"), redirectTo: "/customers" });
}
Loading