Updated the operations dashboard to include a rain outlook feature, providing users with a 7-day forecast based on site coordinates. Added latitude and longitude fields to the site form for better weather data integration. Enhanced the fuel log and site management functionalities to utilize dynamic fuel pricing based on historical data. Improved overall data handling and user experience across various dashboard components.
12 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. |
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_consumptionfield added tofuel_logs. - Sep 2026:
file_hashcolumn added todata_import_batchesfor 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
pbclient (currently unused) would be constrained by the rules if it were active. - The
/api/delivery-notes/[id]/slipproxy uses the admin token in itsAuthorizationheader — 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
4.1 Current state (as of 2026-09-06)
Typecheck: passes cleanly
Tests: 453 passed, 1 failed (1 of 40 files)
The single failing test is in
src/app/api/purchase-orders/process-batch/route.test.ts:
returns 404 when the batch does not exist
expected 500 to be 404
The test mocks pb.collection(...).getOne to throw a plain Error
without a status field. The production code at
src/app/api/purchase-orders/process-batch/route.ts:137 checks
(err as { status?: number }).status === 404. The mock's plain Error
has no status field, so the production code falls through to the 500
branch. The real PB SDK error does have status: 404 for missing
records, so this only affects the unit test, not the live behaviour —
but the test is wrong (mocked the SDK shape badly) and the production
code's reliance on status is fragile to that.
Recommended fix. Either change the test to throw
Object.assign(new Error("..."), { status: 404 }) to match the SDK
shape, or add err.status ?? parseInt(err.message.match(/404/)?.[0] ?? "0")
to the production code. Tier 3 in
multi-site-assessment.md.
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}on0.0.0.0. - The Docker healthcheck is
GET /api/healthevery 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
.dockerignoreexcludes 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. What this document does NOT cover
- The application logic — see
architecture.md. - The collections — see
data-model.md. - The end-to-end pipelines — see
data-flows.md. - The dashboard modules — see
modules.md. - Whether the app will hold up at 50 sites — see
multi-site-assessment.md.