MEGAINFRA-WEBAPP/docs/operations.md
DEls e83a0d302c fix: overview counts, wrong-unit fuel limits, meter guard, en-ZA numbers, vehicle merge, docs
- Overview/Plant tiles and pages share one vehicle bucket rule (blank unit_type = fleet)
- Plant landing equipment figures were always 0; hour diffs now reject 0 baselines and >24h/day jumps
- km/L limits on plant (and L/h on vehicles) report unit-mismatch instead of false alerts
- formatPercent/formatDecimal replace display toFixed (en-ZA)
- POST /api/vehicles/[id]/merge + Merge duplicate dialog (dry run first)
- Attention links go to equipment vs vehicles by unit type; PO upload view link fixed
- Conditional hook fixed in DN document view; ESLint flat config (next lint removed in 16)
- Docs: current modules/routes/env vars; login + Ask reports plans

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:04:08 +02:00

16 KiB

Operations

Environment variables, scripts, tests, feature flags, and the PocketBase API rules in effect in production.

This is the "how do I run it, how do I ship it, where do things live" document. It deliberately does not document the application logic — see architecture.md, data-model.md, data-flows.md, and modules.md.


1. Environment variables

The full set lives in .env.local.example. The Coolify-friendly subset (plus PocketBase host env) lives in docs/cooler-env.template. They should agree at all times.

Variable Client-exposed? Required? Default Purpose
NEXT_PUBLIC_APP_PIN yes yes 2468 Login PIN. The lock screen validates it locally. Must be changed in production.
APP_PIN no yes (clock corrections) unset Server-side copy of the shared PIN. POST /api/clock/events/[id]/correct (the On-site Needs attention missed scan-out form) compares the x-app-pin header against it and returns 503 "Manual correction is not configured" when unset. Keep it equal to NEXT_PUBLIC_APP_PIN.
RECEIPT_CORRECTION_SECRET no yes (receipt edits) unset Server-only code required by POST /api/dn-batches/[id]/notes/[noteId]/correct-receipt to edit a posted receipt. At least 12 chars; must not reuse the PIN. openssl rand -hex 16.
MEGA_CUSTOMER_VAT_NO no yes (DN pipeline) unset MEGA's VAT number. The DN pipeline checks the customer identity block printed on each slip against it; a mismatch marks the note needs_attention.
NEXT_PUBLIC_APP_NAME yes no MEGA INFRASTRUCTURE Brand on the lock screen.
NEXT_PUBLIC_DEFAULT_THEME yes no light light or dark — pre-hydration fallback.
NEXT_PUBLIC_POCKETBASE_URL yes yes (PB-dependent) http://127.0.0.1:8090 Browser-reachable PB URL. Also used by pbAdmin() server-side. The runtime joins file URLs off this same base.
NEXT_PUBLIC_API_BASE_URL yes no unset Legacy. Documented but unused in current code.
POCKETBASE_ADMIN_EMAIL no yes (server) unset Superuser email used by pbAdmin().
POCKETBASE_ADMIN_PASSWORD no yes (server) unset Superuser password.
POCKETBASE_ADMIN_TOKEN no optional unset If set, used in place of email + password — saves a re-auth on every cold start. Generate via PocketBase admin UI → "Generate token".
N8N_PO_INGEST_URL no yes (PO upload) unset Webhook URL the Next.js server forwards PO uploads to. Server-only.
N8N_INGEST_SECRET no yes (n8n callbacks) unset Shared secret n8n sends in x-ingest-secret on the ingest/process-batch/ocr-result/slip routes. Generate with openssl rand -hex 32.
N8N_MCP_URL no optional unset Public URL of the n8n MCP server (for the local Codex MCP config).
ENABLE_CHAT no NO (production) unset Master switch for /api/chat and /dashboard/chat. The .env.local.example warning is explicit: must stay unset in production until real authentication exists.
CHAT_PROVIDER no no anthropic anthropic or minimax (MiniMax OpenAI-compatible endpoint).
ANTHROPIC_API_KEY no conditional unset Anthropic API key. Leave unset to use an ant auth login profile instead.
MINIMAX_API_KEY no conditional unset MiniMax Open Platform API key (NOT a Token Plan subscription key).
MINIMAX_BASE_URL no no https://api.minimax.io/v1 MiniMax OpenAI-compatible base URL.
MINIMAX_MODEL no no MiniMax-M3 The only model in the M-series that supports BOTH vision AND tool calling (verified 2026-08-30). M2 / M2.7 accept tools but reject images.
NODE_ENV no yes (prod) production Standard.
PORT no no 3000 Next.js standalone server port.
HOSTNAME no no 0.0.0.0 Next.js standalone server bind.
NEXT_TELEMETRY_DISABLED no yes (build) unset Set to 1 in the Dockerfile.

Anything prefixed NEXT_PUBLIC_ is inlined into the client bundle at build time. Changing NEXT_PUBLIC_APP_PIN requires a rebuild — Coolify will not pick up the change otherwise. (Source: COOLIFY.md:55-58.)


2. Scripts in scripts/

2.1 PocketBase schema and rules

Script Purpose Idempotent?
pb-bootstrap-schema.py Canonical schema definition for the 13 collections. Run after any schema change. No — running twice may fail because the collections already exist.
pb-relax-rules.py Writes the production API ruleset to every collection (list/view/create = any authed, update/delete = superuser). Yes — re-running just rewrites the same rules.
migrate-delivery-notes-ocr.py One-off migration that adds the OCR processing_status / ocr_* fields and seeds status on existing DNs. No — running twice is a no-op on most steps.
migrate-plant-hours-backfill-vehicle.py Backfills plant_hours_log.vehicle relations from vehicle_qr for pre-relation rows. Idempotent.

2.2 Build-time

Script Trigger Output
build-icons.mjs npm run build (pre-step) Regenerates the PWA icon set under public/icons/
build-og-image.mjs npm run build (pre-step) Regenerates public/og-image.png for OG previews
patch-next-dev-blocker.cjs npm install (postinstall hook) Patches Next 16's dev-only cross-site guard so next dev accepts any host
generate-plant-hours-template.ts Manual Used by scripts/build-*.mjs for the plant-hours template

The postinstall patch is only loaded by next dev — never by production. The Dockerfile runs npm ci --ignore-scripts in the deps stage to skip the hook entirely (Dockerfile:32).

2.3 Migrations directory

scripts/migrations/ holds one-off patches that the schema script doesn't cover. Examples (without full enumeration — there are several):

  • Aug 2026: latest_consumption field added to fuel_logs.
  • Sep 2026: file_hash column added to data_import_batches for PO dedup.

Always add a new entry here when running a one-off schema change. The canonical pattern is one file per concern, named migrate-<collection>-<concern>.py.


3. PocketBase API rules

scripts/pb-relax-rules.py writes:

  • listRule, viewRule, createRule = @request.auth.id != '' (any authenticated user)
  • updateRule, deleteRule = null (superuser only)

Applied to all 13 collections:

DATA_IMPORT_BATCHES, SITES, EMPLOYEES, PURCHASE_ORDERS,
PURCHASE_ORDER_ITEMS, FIXED_COSTS, PLANT, VEHICLES, TIMESHEETS,
FUEL_LOGS, DELIVERY_NOTES, RECEIVED_ITEMS

What this means in practice:

  • The only client that talks to PB in production is pbAdmin() — and it's the superuser, so the rules don't constrain it.
  • The browser-side pb client (currently unused) would be constrained by the rules if it were active.
  • The /api/delivery-notes/[id]/slip proxy uses the admin token in its Authorization header — also not constrained by these rules.

Verifying in production. Run the script again and compare the output. If the rules match, every line should print ✓ ... updated. If anything prints FAILED, investigate before trusting the API.


4. Running the test suite

npm run typecheck   # tsc --noEmit
npm run test:run    # vitest run
npm run lint        # eslint . (flat config in eslint.config.mjs)

4.1 Current state (as of 2026-09-25)

Typecheck:  passes cleanly
Tests:      1397 passed (122 files)
Build:      next build --webpack passes
Lint:       131 problems (53 errors, 78 warnings) — not yet a gate

next lint was removed in Next 16, so npm run lint now calls the ESLint CLI directly. The remaining errors are mostly the React Compiler rules that eslint-config-next 16 turned on (react-hooks/set-state-in-effect, react-hooks/refs, react-hooks/purity) plus no-explicit-any. They are not crashes, but should be worked down before lint is added to CI.

4.2 Test inventory

The src/lib/metrics/*.test.ts files are the most important — they pin every computation the dashboard exposes. Anything that touches a number should be cross-checked against these.

The src/lib/chat/*.test.ts files pin the tool-schemas, the think-filter, and the chat loop's tool-call limits.

The src/app/api/**/*.test.ts files mock pbAdmin and exercise the business logic of each route (file dedup, batch state machine, OCR tier decision, etc.).


5. Running the app

5.1 Local dev

cp .env.local.example .env.local
# edit NEXT_PUBLIC_POCKETBASE_URL, POCKETBASE_ADMIN_*, N8N_*
npm install
npm run dev    # next dev -H 0.0.0.0 -p 6004 --webpack

The dev server runs on port 6004 (not 3000 — the user runs other apps on 3000; do not change this back to 3000). The --webpack flag is required because @ducanh2912/next-pwa is webpack-only.

Append ?demo=1 to any URL to skip the PIN gate.

5.2 Production build

npm run build   # icons + og-image + next build --webpack
npm run start   # next start -H 0.0.0.0 -p 3000

5.3 Docker / Coolify

The Dockerfile and docker-compose.yml are Coolify v4-ready. See COOLIFY.md for the full setup walkthrough. Key points:

  • The standalone server in the final image is non-root, listens on ${PORT:-3000} on 0.0.0.0.
  • The Docker healthcheck is GET /api/health every 30s with a 40s start period (Dockerfile:86-87).
  • Traefik routes traffic to the container on port 3000 — there is no internal port mapping in the docker-compose.
  • The .dockerignore excludes pre-built service-worker files so the build always ships the right hashes for the new release.

5.4 MCP server

npm run mcp     # tsx --env-file=.env.local src/mcp/server.ts

Runs over stdio using the PocketBase credentials in .env.local. Opens no network listener. Exposes the same tools array the chat uses, so an external agent (Codex, etc.) gets identical answers to the in-app chat.

5.5 Deploy logs

The repo tracks one historical deploy log (deployment-vptid1uo0jnz2q4t1mcbqjj9-all-logs-2026-08-19-00-23-06.txt) as evidence of how the Coolify deploy surface looks when something breaks. This is left in-tree for context, not as a reference.


6. Feature flags

Flag Default Effect when on Effect when off
ENABLE_CHAT unset (= false) /api/chat and /dashboard/chat work Both return 404 / notFound()
?demo=1 query string n/a Skips the PIN gate on the current URL Normal auth flow
NEXT_PUBLIC_DEFAULT_THEME light Initial theme before user preference loads "
CHAT_PROVIDER anthropic anthropic if ANTHROPIC_API_KEY is set (or ant auth login profile); minimax / openai if MINIMAX_API_KEY is set

There is no other feature-flag mechanism. Anything else that needed to be toggleable in the past has been either hardcoded (variance tolerances, RATE_PER_LITRE) or set via env (RATE_PER_LITRE is in src/lib/metrics/constants.ts — not env-driven; change there).


7. Smoke tests after a deploy

The two routes that should always return 200 in production:

Probe URL Expects
Liveness GET /api/health 200 with { status: "ok", uptime, timestamp }
Auth gate GET /login 200 HTML (the lock screen)

curl -sS -o /dev/null -w "%{http_code}\n" https://<host>/api/health should print 200. If it doesn't, the container isn't ready (start period is 40s).

8. Staged delivery-note ingestion

The staged pipeline is deliberately off until a representative site has been observed. It is additive: original source files and the legacy runner remain available, and financial rows are created only by the posting action.

8.1 Migration

Preview the schema first. This command never writes and does not require admin credentials:

set -a; source .env.local; set +a
python3 scripts/pb-add-dn-staged-ingestion.py --dry-run

Apply only after reviewing the preview and confirming the exact PocketBase host:

set -a; source .env.local; set +a
python3 scripts/pb-add-dn-staged-ingestion.py --apply \
  --confirm-host "$NEXT_PUBLIC_POCKETBASE_URL"

The script adds the staged collections, fields, and indexes. Do not remove collections or edit financial rows directly.

8.2 Required staged environment

The server needs the existing PocketBase admin values plus:

Variable Required value Purpose
NEXT_PUBLIC_POCKETBASE_URL exact browser/server PB host File reads and migration host guard.
POCKETBASE_ADMIN_EMAIL + POCKETBASE_ADMIN_PASSWORD or POCKETBASE_ADMIN_TOKEN valid superuser credentials Server reads and fenced writes.
N8N_INGEST_SECRET high-entropy shared secret Authenticates the tick callback.
MINIMAX_API_KEY vision-capable API key Reads decoded page images.
MINIMAX_BASE_URL usually https://api.minimax.io/v1 Vision endpoint.
MINIMAX_MODEL MiniMax-M3 or approved vision model Extraction model.
DN_STAGED_INGESTION_ENABLED exactly false until rollout, then exactly true Selects staged vs legacy tick runner.
DN_PAGE_PROXY_SECRET high-entropy secret (or documented n8n fallback) Signs review page URLs.

Keep these values server-only except for NEXT_PUBLIC_POCKETBASE_URL.

8.3 Tick and lock recovery

POST /api/dn-batches/tick is the n8n scheduler entry point and requires x-ingest-secret. The staged runner acquires a PocketBase lease per batch and fences business mutations in the same transaction. A worker that loses its lease must not retry writes. Expired dn_pipeline_locks rows are reclaimed by the next tick; do not delete a live lock by hand. If a tick is interrupted, inspect the batch stage, last_error, page statuses, and candidate stages, then let the next tick resume idempotently. Repeated expiry indicates a timeout or network/vision problem; fix that before increasing DN_TICK_BATCH_SIZE.

8.4 Shadow rollout and observation

  1. Apply the migration and leave DN_STAGED_INGESTION_ENABLED=false.

  2. Run the read-only comparison:

    npx tsx --env-file=.env.local scripts/compare-dn-pipelines.mts <batch-id>
    npx tsx --env-file=.env.local scripts/compare-dn-pipelines.mts --all
    

    It reports page coverage, candidate count, PO/item links, unit compatibility, quantities, computed value, and writes: false; it never promotes or changes PocketBase.

  3. Observe one site's page-read failures, open pages, needs-review rate, exact-PO and item-link rates, duplicates, lock reclaims, promotion failures, and incorrect automatic financial matches. The last metric must remain zero.

  4. Enable staged processing for that site by setting the flag to true and restarting the server. Expand site by site only after the metrics remain acceptable.

8.5 Rollback

Set DN_STAGED_INGESTION_ENABLED=false and restart to return to the legacy runner. Do not delete staged rows: they are the audit trail and can be retried or compared later. Do not edit delivery_notes, delivery_note_items, or supplier ledger rows directly. Use the correction/audit workflow and investigate the candidate, page, lock, and promotion event first. Posted candidates are immutable unless an explicit correction process is approved.


9. What this document does NOT cover