# FieldOps — MAI CALL v0.3 Modular industrial SaaS monorepo. **MAI CALL v0.3** is the latest shipped feature: a full maintenance-request loop (offline-first, operator PIN + admin password auth) with an **end-of-shift report** for supervisors. ## What's here ``` apps/ operator-pwa/ Next.js 15 — operator mobile console (port 3000) admin-web/ Next.js 15 — maintenance queue for admin/supervisor (port 3001) packages/ db/ Prisma 6 schema + multi-tenant extension api/ tRPC v11 routers (maintenanceRequest, workstation, user, storage) storage/ ObjectStorage abstraction + MinIO/S3 implementation ui/ Shared shadcn-style components domain/ Pure domain logic (reserved for later modules) config/ TypeScript / ESLint / Tailwind presets e2e/ Playwright happy-path test ``` ## Prerequisites | Tool | Version | Notes | | --- | --- | --- | | Node.js | 22 LTS | Required by Next.js 15 | | pnpm | 11+ | `npm i -g pnpm@11` or `corepack enable pnpm` | | Docker Desktop | any recent | Provides Postgres 16 + MinIO | | Git | any | — | --- ## Quick-start (< 15 min) Run these commands from the **repo root** in order. ```sh # 1. Copy and configure the environment file cp .env.example .env # The defaults work out of the box for local dev. # AUTH_DEV_AUTOLOGIN is already "true" in .env — leave it. # 2. Install dependencies pnpm install # 3. Start Postgres + MinIO (creates the fieldops bucket automatically) docker compose up -d # 4. Apply the database schema pnpm db:migrate # 5. Seed the demo tenant, 3 operators, and 3 workstations pnpm db:seed # 6. Start both apps pnpm --filter @repo/operator-pwa dev & pnpm --filter @repo/admin-web dev ``` | URL | What it is | | --- | --- | | http://localhost:3000 | Operator PWA | | http://localhost:3001 | Admin / Manutenção queue | | http://localhost:9001 | MinIO console (see credentials below) | --- ## Demo flow ### As an operator (port 3000) 1. Open http://localhost:3000. With `AUTH_DEV_AUTOLOGIN=true` you land on the home page as `admin@demo.local`. To simulate a real operator log-in, navigate to http://localhost:3000/select-operator, tap **op1@demo.local**, then enter PIN **1111** on the keypad. (op2 = **2222**, op3 = **3333**) 2. Tap **Pedir manutenção**. 3. Select a workstation, optionally attach a photo, write a description, and tap **Enviar pedido**. 4. The page shows **"Pedido enviado"** once the sync completes (usually within 1–2 seconds when online). **Offline test:** Chrome DevTools → Network → Offline → create 3 requests → Network → Online. The requests sync automatically within ~10 s; "Tudo sincronizado" appears. > ⚠️ The offline navigation test only works against a **production build** of > `operator-pwa`. In `pnpm dev` mode, Next.js generates page chunks on demand, > so the service worker has nothing to precache and navigation to > `/maintenance/new` fails with `Failed to fetch`. See the > [Troubleshooting](#troubleshooting) section for the workaround. ### As admin / maintenance supervisor (port 3001) 1. Open http://localhost:3001. With `AUTH_DEV_AUTOLOGIN=true` you land on the maintenance queue automatically. Without it, you see a login form — use **admin@demo.local** / **admin1234**. 2. The queue refreshes every 5 s; new requests appear automatically. 3. Click **Aceitar** to claim a request (status: Em curso). 4. Click **Marcar resolvido**, optionally add a note, click **Confirmar** (status: Resolvido). 5. The document tab title shows `(N) FieldOps — Manutenção` when there are open requests. ### Shift report (admin-web only) 1. In the maintenance queue, click **Relatório de turno** (top-right header). 2. Choose a window — **Manhã** (06–14 h), **Tarde** (14–22 h), **Noite** (22–06 h), **Hoje** (midnight → now), or **Personalizado** (free date-time range). 3. The report shows: total/resolved/open counts, avg & max response time, avg resolution time, breakdown by workstation and area, and a list of requests still open at report time. 4. Click **Imprimir** to print / save as PDF via the browser. After `pnpm db:seed`, the "Hoje" window already has 6 sample requests (3 resolved, 1 claimed, 2 open) so the report is never empty on first boot. --- ## MinIO (photo storage) | Setting | Value | | --- | --- | | API endpoint | http://localhost:9000 | | Web console | http://localhost:9001 | | Root user | `fieldops` | | Root password | `fieldops123` | | Bucket | `fieldops` | Photos are stored as presigned-URL objects under `tenants/{tenantId}/maintenance/{uuid}.jpg`. To back up locally: ```sh # Install mc (MinIO client) then: mc alias set local http://localhost:9000 fieldops fieldops123 mc mirror local/fieldops ./backup-photos ``` --- ## Running the E2E tests ```sh # One-time browser install (downloads ~170 MB of Chromium) pnpm --filter @repo/e2e install-browsers ``` ### `pnpm test:e2e` — happy-path + shift report (autologin ON) Starts both dev servers with `AUTH_DEV_AUTOLOGIN=true`. Safe to run at any time — reuses servers already running on 3000/3001 if available. Expected: **3 passed** (~45 s): - MAI CALL happy path: create → claim → resolve - Shift report: renders with seed data and reacts to window selection - Shift report: accessible from the maintenance queue link ### `pnpm test:e2e:auth` — real login (autologin OFF) Tests that the login UI actually works — operator PIN keypad and admin email/password — without the dev back door. **Precondition: no servers running on ports 3000 or 3001.** This command starts its own servers with `AUTH_DEV_AUTOLOGIN=false`. If ports are busy: ```sh # Windows Get-Process node | Stop-Process -Force ``` Also requires `pnpm db:seed` to have been run. Expected: **4 passed** (~60 s): - Operator: wrong PIN shows error, correct PIN enters the app - Operator: unauthenticated root redirects to picker - Admin: protected route redirects to /login without session - Admin: wrong password shows error, correct password enters the queue --- ### Manual smoke checklist (5 min — covers print + offline) Automated tests don't cover the print PDF or offline sync. Run this when you want end-to-end confidence before a demo: ``` 1. docker compose up -d && pnpm db:seed 2. Operator (real login): pnpm --filter @repo/operator-pwa dev → http://localhost:3000/select-operator Try PIN 9999 → see error. Then op1 → PIN 1111 → enters app. 3. Admin (real login): pnpm --filter @repo/admin-web dev → http://localhost:3001/login admin@demo.local / admin1234 → maintenance queue. 4. Shift report: Click "Relatório de turno" in header → click Manhã/Tarde/Noite/Hoje → numbers update each time. Click "Imprimir" → confirm PDF is clean (no buttons/selector/nav). 5. Offline (requires production build of operator-pwa — see Troubleshooting): DevTools → Network → Offline → create 2 requests → Online → they sync. ``` --- ## Known limitations — v0.1 (demo only) | Limitation | Detail | Target | | --- | --- | --- | | **No real authentication in dev** | `AUTH_DEV_AUTOLOGIN=true` lets anyone in as admin (dev/test only — ignored when `NODE_ENV=production`). Real auth is implemented: admin uses email + password, operators use list + PIN. | — | | **Operator picker + PIN, not TAG/card** | Operator identity is chosen from a list and confirmed with a PIN, rather than read from an RFID badge. | MY QUALITY module | | **No multi-tenant onboarding UI** | Tenants are created via `pnpm db:seed` / SQL only. | when 2nd customer onboards | | **No scheduled reports** | The shift report is on-demand (open the page, print). Auto-email at shift end requires a scheduler + email service. | v0.4 or post-pilot | | **Single photo per request** | No video, audio, or multiple photos. | when pilot asks | | **Safari / iOS Background Sync** | Background Sync API is not supported on Safari; sync falls back to main-thread polling every 10 s when the tab is open. | acceptable for pilot | | **No push notifications** | Polling at 5 s on the admin-web tab is the notification mechanism. | if pilot requires it | | **Dev-only storage** | MinIO runs in Docker. No backup cron, no lifecycle policy, no cloud migration yet. | before pilot | | **No i18n** | Hardcoded Portuguese (Mangualde plant). | v0.2 with pilot | | **No observability** | Structured logs via Pino; no trace/metric pipeline. | when pilot requires it | > ⚠️ **NEVER deploy with `AUTH_DEV_AUTOLOGIN=true`.** That flag is a > back door for local dev and CI only. It is **ignored at the code level** > when `NODE_ENV=production`, so a misconfigured `.env` in production won't > open a hole. The chokepoints are `apps/*/lib/auth.ts → resolveUser()` and > `apps/*/middleware.ts`. --- ## Common commands | Command | What it does | | --- | --- | | `pnpm install` | Install all workspace deps | | `docker compose up -d` | Start Postgres + MinIO (detached) | | `docker compose down` | Stop services (data persists in volumes) | | `docker compose down -v` | Stop and **delete all data** | | `pnpm --filter @repo/operator-pwa dev` | Operator PWA only (port 3000) | | `pnpm --filter @repo/admin-web dev` | Admin web only (port 3001) | | `pnpm db:migrate` | Apply pending Prisma migrations | | `pnpm db:seed` | Re-seed demo data (idempotent, safe to re-run) | | `pnpm db:reset` | Drop, recreate, migrate, seed | | `pnpm db:studio` | Open Prisma Studio at http://localhost:5555 | | `pnpm typecheck` | Typecheck every package | | `pnpm test:e2e` | Run the happy-path Playwright test | | `pnpm format` | Prettier write across the workspace | | `pnpm tsx scripts/storage-smoke.ts` | Verify MinIO presigned upload/download | | `pnpm tsx scripts/maintenance-smoke.ts` | Verify the full create→claim→resolve cycle | | `pnpm tsx scripts/auth-smoke.ts` | Verify hashing, PIN/password login, and lockout | | `pnpm tsx scripts/report-smoke.ts` | Verify shift-report aggregation against seeded data | --- ## Architecture notes ### Multi-tenancy Every Prisma model except `Tenant` carries a `tenantId` column. Tenant scoping is enforced at the Prisma layer via an extension in `packages/db/src/tenant-extension.ts`. Application code always goes through `ctx.db.*` (scoped) — the unscoped `ctx.prisma` is a code-review red flag. **Note on `$transaction`:** The interactive-callback form of `$transaction` receives a Prisma client that does NOT support `$extends`. Tenant scoping inside transactions is done by manually injecting `tenantId` into each `where`/`data` clause. See `packages/api/src/routers/maintenance-request.ts` for the pattern. ### Offline sync The operator PWA stores pending requests in IndexedDB (Dexie 4). The sync loop (`lib/queue/sync.ts`) runs in the main thread — triggered by the `online` event, `visibilitychange`, and a 10 s polling interval. The Background Sync API is registered opportunistically (works in Chrome, ignored elsewhere). Communication between the sync loop and UI is via `BroadcastChannel('mai-call-sync')`. ### Storage Photos are uploaded directly from the browser to MinIO via S3 presigned PUT URLs. The tRPC layer signs the URL; the browser performs the PUT. The object key is returned and stored in the `MaintenanceRequest` row. `packages/storage` implements the `ObjectStorage` interface with MinIO (via AWS SDK v3 S3 protocol) and is portable to AWS S3, Cloudflare R2, or Wasabi by changing the endpoint env var. --- ## Troubleshooting **`UNAUTHORIZED` on every page** — `AUTH_DEV_AUTOLOGIN` is `false` in your `.env`. Set it to `true` for local dev, or sign in via the operator picker. **`Tenant not found`** — the seed was wiped. Run `pnpm db:seed`. **Admin login redirects to `:3000` / the operator picker** — the shared `.env` sets `AUTH_URL=http://localhost:3000`, which is correct for the operator-pwa but wrong for the admin-web (:3001). With `AUTH_DEV_AUTOLOGIN=true` it never bites (the middleware doesn't redirect). With autologin OFF, give the admin-web its own `AUTH_URL=http://localhost:3001` (the E2E auth config does this). In production, each app must have its own `AUTH_URL`. **`DATABASE_URL not found`** — `.env` is missing or Docker Postgres is not running. Run `docker compose up -d` then retry. **`ERR_PNPM_IGNORED_BUILDS` after adding a package** — pnpm 11 blocks postinstall scripts by default. Add the package to `pnpm-workspace.yaml` `allowBuilds:`. **Playwright: port already in use** — kill leftover dev servers: ```sh # Windows Get-Process node | Stop-Process -Force # macOS / Linux pkill -f 'next dev' ``` **MinIO photos not loading in admin-web** — verify MinIO is running (`docker compose ps`) and that the `fieldops` bucket exists (`docker logs fieldops-minio-init-1`). **Offline navigation fails with `Failed to fetch` / `NetworkOnly`** — the PWA service worker can only serve pages it has precached, and `pnpm dev` builds pages on demand (no precache). To test the full offline flow, run the operator PWA in production mode: ```sh # Stop the running `pnpm dev` for operator-pwa first, then: pnpm --filter @repo/operator-pwa build pnpm --filter @repo/operator-pwa start ``` Load http://localhost:3000 once while online (so the SW precaches the shell), reload the tab to activate the new SW, then switch DevTools to Offline. The admin-web can stay in `dev` mode — only the operator PWA needs the production build for offline navigation. The IndexedDB sync queue itself works in both modes; only the page-navigation layer requires precache. --- ## License Internal. All rights reserved.