FieldOps/README.md
Pedro Gomes fdfa936461 MAI CALL - v0.3
O que o supervisor encontra agora:

Na fila de manutenção (:3001), novo botão "Relatório de turno" no header.
Página /maintenance/report com:
Atalhos Manhã / Tarde / Noite / Hoje + seletor de dia + Personalizado (date-time livre)
Label sempre visível com a janela ativa ("Turno da Manhã — 30/05 06:00 → 14:00")
6 cartões de métricas: pedidos, resolvidos, em aberto, tempo médio de resposta, tempo médio de resolução, pior resposta
Tabela por posto e resumo por área
Lista "Em aberto à hora do relatório" (ou "Nada em aberto. ✓")
Botão Imprimir → PDF via browser; CSS @media print limpa botões/nav
Verificações verdes:

report-smoke.ts — 17/17 (totals, responseMs, resolutionMs, byWorkstation, byArea, stillOpen, window edge cases)
E2E MAI CALL happy-path — 1/1 (dados de seed extra não interferem)
TypeScript — limpo nos pacotes tocados (@repo/api, @repo/admin-web)
Seed cria 6 pedidos de exemplo: relatório "Hoje" nunca começa vazio

+

Resumo da revisão do v0.3
Conformidade com o plano: alta. Shape de output exato, ctx.db (tenant-scoped), requireRole, helper de turnos com o caso da noite, seed com 6 pedidos, UI completa + impressão. Tudo no sítio.

Dois defeitos reais que escaparam ao typecheck e ao E2E — corrigidos:

# Problema Correção
🔴 1 Fetch storm no modo "Hoje" (default): computeWindow recalculava to = new Date() a cada render → nova query key → loop de fetch contínuo. useMemo([windowState]) estabiliza a janela em report-view.tsx:101. Reclicar "Hoje" refresca. Também limpei estado morto (customFrom/customTo).
🔴 2 Smoke não cumpria o AC: re-implementava a agregação à mão em vez de chamar a procedure, e não testava to <= from → BAD_REQUEST (exigido pelo AC do Passo 1). Reescrito report-smoke.ts no padrão createCallerFactory — agora exercita a procedure real: agregação, BAD_REQUEST (to≤from e >31d), janela futura vazia, e FORBIDDEN para operador.
Verificações finais (todas verdes):

tsc --noEmit admin-web — limpo
report-smoke.ts — 22/22 (agora contra a procedure real)
E2E MAI CALL — 1 passed
2026-05-30 12:51:14 +01:00

290 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 12 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ã** (0614 h), **Tarde** (1422 h), **Noite** (2206 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
# Run the happy-path test (starts both dev servers automatically)
pnpm test:e2e
```
The Playwright config force-sets `AUTH_DEV_AUTOLOGIN=true` for the child
servers, so the test does not depend on the developer's `.env`.
Expected: **1 passed** in ~30 s.
---
## 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`.
**`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.