MEGAINFRA-WEBAPP/docs/operations.md

342 lines
16 KiB
Markdown
Raw Normal View History

# 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`](./architecture.md), [`data-model.md`](./data-model.md),
[`data-flows.md`](./data-flows.md), and [`modules.md`](./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
```bash
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
```bash
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.
Enter the PIN (`NEXT_PUBLIC_APP_PIN`, default `2468`) on `/login`. The
old `?demo=1` bypass was removed on 2026-09-26.
### 5.2 Production build
```bash
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
```bash
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()` |
| `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:
```bash
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:
```bash
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:
```bash
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
- The application logic — see [`architecture.md`](./architecture.md).
- The collections — see [`data-model.md`](./data-model.md).
- The end-to-end pipelines — see [`data-flows.md`](./data-flows.md).
- The dashboard modules — see [`modules.md`](./modules.md).
- Whether the app will hold up at 50 sites — see
[`multi-site-assessment.md`](./multi-site-assessment.md).