Commit Graph

8 Commits

Author SHA1 Message Date
davidherran ba002ea9e6 fix: auto-chown mounted volumes + metadataBase warning
Deploy to VPS / deploy (push) Has been cancelled
THREE FIXES IN ONE SHOT.

1. UPLOAD EACCES (the crashing one)
The /app/public/branding upload was failing with EACCES because the
folder on the host was created by `debian` (uid 1000) but the container
runs as nextjs (uid 1001). Docker bind mounts preserve host ownership,
so the container couldn't write into branding/.

Fix: introduce a docker-entrypoint.sh that runs the container briefly
as root, chowns every public/* mount to uid 1001, runs Prisma migrate
deploy, then drops to nextjs via `su-exec`. From now on every deploy
self-heals permissions across all asset folders (branding, footage,
applications, cases, news, parts, operations-inbox) — even if a future
volume gets added with the wrong owner.

Dockerfile changes:
- Adds `su-exec` package (lightweight gosu equivalent for Alpine)
- Removes the static USER directive (entrypoint manages user transitions)
- Replaces CMD with an ENTRYPOINT pointing at the new script

2. metadataBase WARNING
Server logs were emitting:
  ⚠ metadataBase property in metadata export is not set ... using "http://localhost:3000"
That's the layout's generateMetadata not declaring metadataBase, so
Next.js couldn't resolve relative OG/Twitter image URLs to absolute
ones. Reading NEXT_PUBLIC_APP_URL (already set in docker-compose env)
and feeding it as `metadataBase: new URL(...)` silences the warning
and produces correct absolute URLs in social previews.

3. PERMISSIONS DOCS
The entrypoint chown is idempotent and silent on non-existent folders,
so future volumes added to docker-compose just work. No more "did you
sudo chown the new folder" gotchas.

DEPLOY (David)
  cd /opt/flux-srl
  # one-time fix for the existing branding folder so the next deploy
  # doesn't have to chown 65MB of data — but the entrypoint now handles
  # this automatically anyway:
  sudo chown -R 1001:1001 /opt/flux-srl/public/branding
  git pull
  docker compose up -d --build app
2026-05-04 18:17:39 -05:00
davidherran 5abd3a02f6 fix: ship full prod node_modules to runner so prisma migrate deploy works
Deploy to VPS / deploy (push) Has been cancelled
The previous container restart loop ("Error: Cannot find module 'effect'")
happened because the runner stage cherry-picked only specific Prisma
subdirs (.prisma, @prisma, prisma) but missed transitive runtime deps
of the Prisma CLI — like @prisma/config's dep on `effect`.

Cherry-picking is fragile: any minor Prisma upgrade changes the
required dep set and the container stops booting.

Real fix: introduce a dedicated prod-deps stage that runs
`npm ci --omit=dev --include=optional` and ship the resulting
node_modules wholesale to the runner. Trade-off: the runner image
grows by ~200-300MB, gaining bullet-proof prod CLI execution in
exchange. Subsequent rebuilds are fully cached after the first run.

What changed in Dockerfile:
- New stage `prod-deps` produces a prod-only node_modules tree
- Runner stage drops the explicit @prisma/prisma/sharp/@img copies
  (they're already in prod-deps' node_modules)
- Still copies prisma/, prisma.config.ts, .prisma generated client,
  and Next.js standalone artefacts
- CMD unchanged: migrate deploy + server.js
2026-05-04 16:22:47 -05:00
davidherran 320c0862df fix: pin all sharp platform binaries so npm ci works on any host
Deploy to VPS / deploy (push) Has been cancelled
The previous attempts (--include=optional, then a separate npm install
fallback) failed because npm ci runs sharp's install script DURING
installation — and that script crashes ("Please add node-gyp to your
dependencies") before the next Dockerfile step gets to run.

Real fix: pin every sharp platform binary as an optionalDependency in
package.json. npm now records URL+hash for all of them in the lock
file regardless of which OS generated the lock. On any build host,
npm ci picks the matching binary via the os/cpu/libc filters in those
packages and silently skips the rest.

Pinned binaries (sharp 0.34.5):
- @img/sharp-linuxmusl-x64   (Alpine x64 — our VPS)
- @img/sharp-linuxmusl-arm64 (Alpine arm64)
- @img/sharp-linux-x64       (glibc x64)
- @img/sharp-linux-arm64     (glibc arm64)
- @img/sharp-darwin-arm64    (Apple Silicon dev)
- @img/sharp-darwin-x64      (Intel Mac dev)

Side benefit: simplifies the Dockerfile. Drops the secondary
`npm install --no-save --cpu=x64 --os=linux --libc=musl sharp` step
and the vips-dev system package (no source compilation needed when
the prebuilt binary is guaranteed present). The runner stage still
needs `vips` runtime, that stays.
2026-05-04 15:52:22 -05:00
davidherran 4f75943317 fix: force sharp Alpine binary download in Docker build
Deploy to VPS / deploy (push) Has been cancelled
Sharp 0.34 ships a separate prebuilt binary per platform tuple as
optionalDependencies (@img/sharp-linuxmusl-x64 for Alpine, -darwin-arm64
for Apple Silicon, etc).

The lock file only records the dev machine's platform binary. `npm ci`
is strict — it installs exactly what's locked and refuses to add
platform-specific entries that weren't there at lock time. Result on
the VPS Alpine x64 build:

  sharp: Attempting to build from source via node-gyp
  sharp: Please add node-gyp to your dependencies

Fix: after the strict `npm ci`, run a separate `npm install --no-save
--cpu=x64 --os=linux --libc=musl sharp`. This downloads the Alpine
binary into node_modules without modifying package.json or the lock,
so the dev's Mac install stays untouched on the next git pull.

This is the recommended pattern from sharp's own docs for cross-target
Docker builds: https://sharp.pixelplumbing.com/install#cross-platform
2026-05-04 15:43:30 -05:00
davidherran 01a84edee9 fix: prisma migrate now runs at container startup + dotenv optional
Deploy to VPS / deploy (push) Has been cancelled
Two related fixes for the deploy pipeline so DB schema changes never
again leave the site half-deployed.

PRISMA CONFIG (prisma.config.ts)
- "import 'dotenv/config'" was hard-required, but dotenv isn't installed
  in the production runtime image (env vars come from docker-compose).
- Wrapped in try/catch so it loads .env locally and silently no-ops in
  the container — `prisma migrate deploy` works in both environments.

DOCKERFILE
- Copies node_modules/prisma + prisma.config.ts to the runner stage so
  the CLI is available at runtime, not just at build.
- New CMD runs `prisma migrate deploy` before booting the server.
  Idempotent — already-applied migrations are skipped. If the DB is
  unreachable, the container exits and docker-compose retries.
- This means: from now on, `git pull && docker compose up -d --build app`
  is the entire deploy. No more "did you remember to run migrations?".

DEFENSIVE TRY/CATCH (applications/[slug]/page.tsx)
- prisma.application.findUnique and prisma.globalNode.findMany now have
  try/catch with logged errors. A transient DB hiccup or missing
  Application slug now degrades gracefully (renders "not found" or empty
  cases wall) instead of triggering a 500 Internal Server Error.

DEPLOY (David, this is the recovery sequence on the VPS)
  cd /opt/flux-srl
  git pull
  docker compose up -d --build app
  # The container will run pending migrations on its own.
  # No need to run `prisma migrate deploy` manually anymore.
2026-05-04 15:04:35 -05:00
davidherran a199891a3c feat: FluxAI multi-step autonomy + rate limiting + image pipeline
Deploy to VPS / deploy (push) Has been cancelled
Two production-grade hardening additions and one cost optimisation.

FLUXAI AUTONOMY RESTORED (api/chat)
- Brings back the multi-step agentic flow that the system prompt was
  always designed for. The "temporarily removed maxSteps" comment is
  gone — replaced with the AI SDK 6 equivalent stopWhen: stepCountIs(5).
- Cap at 5 chained tool calls per turn bounds latency + LLM cost.
- maxDuration raised 30s → 60s to absorb tool-chain runs.
- Result: one user prompt now triggers, e.g. search_installations →
  energy_savings_calculator → show_case_study → schedule_consultation
  in a single turn — exactly the SPIN methodology in the prompt.

RATE LIMITING (src/lib/rateLimit.ts + api/chat)
- Token-bucket per IP: 30 messages burst, sustained 30/minute. Trips
  to 429 with Retry-After + X-RateLimit-Remaining headers when abused.
- IP extracted from x-forwarded-for (Nginx already passes this).
- In-memory Map with 10-min GC of stale buckets — no Redis dep.
  If we scale to multiple replicas later, swap the Map for Upstash.
- Protects the OpenAI quota from someone hammering the chat endpoint.

IMAGE PIPELINE (src/lib/imageOptimizer.ts)
- sharp-based optimizer: auto-orient (EXIF), cap at 2560px long side,
  re-encode WebP@85, content-hash filename. Re-uploads with same
  content reuse the same hash; new content gets a new URL — perfect
  cache invalidation without header tricks.
- Opt-in via optimize=1 form/query param on /api/assets POST.
- Hero CMS and Site Settings uploads turn it on automatically (those
  are user-facing brand assets where compression matters most).
- App/news/parts uploads remain untouched (editors may be uploading
  CAD drawings, datasheets, etc. that shouldn't be transcoded).
- Falls back gracefully to a no-op for unsupported formats (SVG, GIF,
  videos, anything sharp can't decode) so it never breaks an upload.

DOCKERFILE
- Adds vips/vips-dev for sharp on Alpine + --include=optional so the
  @img/sharp-linuxmusl-x64 prebuilt is downloaded
- Explicitly copies node_modules/sharp + node_modules/@img to the
  runner stage (Next.js trace can miss conditional deps).

NO DB SCHEMA CHANGES.
2026-05-04 14:48:37 -05:00
davidherran 0118774e31 production: docker fixes, nginx SSL config, generateStaticParams fallback
Deploy to VPS / deploy (push) Has been cancelled
2026-04-01 16:57:22 +00:00
davidherran fc24313f15 production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled
2026-03-20 13:46:05 -05:00