FieldOps — MAI CALL + MY QUALITY
Modular industrial SaaS monorepo. Two modules are shipped:
- MAI CALL — a full maintenance-request loop (offline-first, operator PIN + admin password auth) with an end-of-shift report for supervisors.
- MY QUALITY — operators badge in to a workstation (operator↔posto session); the Quality team (QCP) raises defects against a workstation, which are routed in real time to the operator bound there to acknowledge and correct. State machine OPEN → ACKNOWLEDGED → CORRECTED.
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.
# 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)
- Open http://localhost:3000.
WithAUTH_DEV_AUTOLOGIN=trueyou land on the home page asadmin@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) - Badge in: pick the workstation you are at. This starts your operator↔posto session (the future RFID badge-in). The home shows your current posto with a Sair do posto (badge-out) button.
- Tap Pedir manutenção.
- The posto is taken automatically from your session (no dropdown). Optionally attach a photo, write a description, and tap Enviar pedido.
- 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. Inpnpm devmode, Next.js generates page chunks on demand, so the service worker has nothing to precache and navigation to/maintenance/newfails withFailed to fetch. See the Troubleshooting section for the workaround.
As admin / maintenance supervisor (port 3001)
- Open http://localhost:3001.
WithAUTH_DEV_AUTOLOGIN=trueyou land on the maintenance queue automatically. Without it, you see a login form — use admin@demo.local / admin1234 (or the QCP user qcp@demo.local / qcp1234, who lands on the quality console). - The queue refreshes every 5 s; new requests appear automatically.
- Click Aceitar to claim a request (status: Em curso).
- Click Marcar resolvido, optionally add a note, click Confirmar (status: Resolvido).
- The document tab title shows
(N) FieldOps — Manutençãowhen there are open requests.
Shift report (admin-web only)
- In the maintenance queue, click Relatório de turno (top-right header).
- Choose a window — Manhã (06–14 h), Tarde (14–22 h), Noite (22–06 h), Hoje (midnight → now), or Personalizado (free date-time range).
- 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.
- 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.
MY QUALITY — quality defects
The Quality controller (QCP) raises defects; the operator at the targeted workstation handles them.
As QCP (port 3001): sign in with qcp@demo.local / qcp1234 — you
land on the Defeitos de qualidade console (QCP users are redirected there
from /maintenance). Fill the Novo defeito form (posto, tipo, localização,
RFS, descrição, foto opcional) and click Lançar defeito. The queue below
polls every 5 s and shows each defect's lifecycle (who raised / acknowledged /
corrected it). Admins can reach the console from the Qualidade link in the
maintenance-queue header.
As operator (port 3000): once badged in (see above), open Defeitos de qualidade from the home. Defects raised at your posto appear here (polled, with an optional sound alert). Tap Tomei conhecimento (→ ACKNOWLEDGED), then Marcar corrigido with an optional note (→ CORRECTED).
After pnpm db:seed, op1 is already badged in at CTR04 with 3 sample
defects (1 open, 1 acknowledged, 1 corrected).
Smoke test (no browser): pnpm tsx scripts/quality-smoke.ts (18 assertions —
the full QCP→operator loop, role guards, and state-machine conflicts).
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:
# Install mc (MinIO client) then:
mc alias set local http://localhost:9000 fieldops fieldops123
mc mirror local/fieldops ./backup-photos
Running the E2E tests
# 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:
# 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 whenNODE_ENV=production, so a misconfigured.envin production won't open a hole. The chokepoints areapps/*/lib/auth.ts → resolveUser()andapps/*/middleware.ts.
Languages (i18n)
Both apps support Portuguese (PT, default) and English (EN). The language is
stored in a NEXT_LOCALE cookie and selected via the PT | EN switcher in the
app header.
Changing language
Click the PT | EN pill in the header of either app. The preference is saved in a cookie (1-year expiry) — no account change required.
Adding a new language
- Create
apps/<app>/messages/<locale>.json(copyen.jsonas a starting point). - Add the locale to
LOCALESinapps/<app>/i18n/locales.ts. - That's it — the switcher picks it up automatically.
See docs/i18n.md for the full guide, including the key-parity and ICU validation scripts to run before shipping a new language.
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 |
pnpm tsx scripts/quality-smoke.ts |
Verify the MY QUALITY loop (badge-in, defect create→acknowledge→correct, roles) |
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 admin-web needs
its own AUTH_URL=http://localhost:3001 (the shared .env points at the operator
on :3000). This is handled automatically: the admin dev script loads
apps/admin-web/.env.admin with precedence. If you deleted or edited that file
and hit this, restore AUTH_URL="http://localhost:3001" there. In production,
each app gets its own AUTH_URL from the deploy environment.
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:
# 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:
# 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.