Commit Graph

37 Commits

Author SHA1 Message Date
davidherran aa95be45d0 fix: stop ignoring scripts/ in .dockerignore
Deploy to VPS / deploy (push) Has been cancelled
The new docker-entrypoint.sh lives in scripts/, but .dockerignore was
excluding the whole folder from the build context — so the COPY in the
Dockerfile resolved to a missing file:

  COPY scripts/docker-entrypoint.sh: not found

Removing the `scripts/` line lets the build context include it. The
other ignored paths stay (node_modules, .next, .git, env files,
Dockerfile, docker-compose, nginx, certbot, prisma dev.db) — those
genuinely don't need to be sent to the daemon.
2026-05-04 18:29:18 -05:00
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 1f4a95cc47 fix: revert ISR to force-dynamic + drop generateStaticParams (DYNAMIC_SERVER_USAGE)
Deploy to VPS / deploy (push) Has been cancelled
The DYNAMIC_SERVER_USAGE errors persisted even after passing locale
explicitly to next-intl. Some other Server Component in the tree is
still triggering an implicit dynamic API read under ISR — and chasing
it across next-intl, Prisma, the @ai-sdk libs, and the standalone
build was eating the deploy. Pragmatic call: stop trying to keep ISR
while we still have unstable bug surface, take the runtime back to
puro SSR (the working state from before the SEO commit), then bring
ISR back surgically once the site is stable.

CHANGES (5 page.tsx files)
- /[locale]/page.tsx                         revalidate=60 → dynamic="force-dynamic"
- /[locale]/news/page.tsx                    revalidate=60 → dynamic="force-dynamic"
- /[locale]/news/[slug]/page.tsx             revalidate=60 → dynamic="force-dynamic"
- /[locale]/heritage/page.tsx                revalidate=60 → dynamic="force-dynamic"
- /[locale]/applications/[slug]/page.tsx     revalidate=60 → dynamic="force-dynamic"

ALSO: removed generateStaticParams from news/[slug] and applications/[slug].
With it present (even returning [] in prod), Next.js still classified
those routes as SSG-eligible, which conflicted with the force-dynamic
flag and kept the ISR/dynamic boundary ambiguous. Removing it makes
the build output show all locale routes as ƒ (Dynamic) — pure SSR.

WHAT WE KEEP
- generateMetadata still runs per request, so all SEO benefits (canonical
  URLs, hreflang, OG tags, Twitter cards) remain.
- sitemap.xml and robots.txt are unaffected.
- JSON-LD still emits.
- revalidatePath() in /api/assets still works (just becomes a no-op for
  these pages since they're already dynamic — no cache to invalidate).
- Caching at the Nginx layer (max-age=300 + must-revalidate on /_next/image
  and /branding|/cases|/applications|/news|/parts|/footage) is unchanged,
  so static asset performance stays optimal.

WHAT WE LOSE TEMPORARILY
- Page HTML is generated on every request instead of every 60 seconds.
  At Flux's traffic levels this is negligible — Prisma queries are sub-50ms
  and Postgres has connection pooling. We'll move back to ISR once we've
  isolated the offending dynamic read.

DEPLOY (David — IMPORTANT, force a real rebuild this time)
  cd /opt/flux-srl
  git pull
  docker compose build --no-cache app
  docker compose up -d app
  docker compose logs app --tail=30
2026-05-04 17:38:59 -05:00
davidherran e879016879 fix: pass locale explicitly to next-intl helpers (DYNAMIC_SERVER_USAGE)
Deploy to VPS / deploy (push) Has been cancelled
Production logs were spamming "DYNAMIC_SERVER_USAGE" errors and every
ISR-cached page (home, news, heritage, applications, news/[slug]) was
500-ing. Root cause: when force-dynamic was replaced with revalidate=60
last commit batch, the implicit locale-resolution path through
next-intl's getTranslations() / getLocale() / getMessages() became a
problem. Without an explicit locale arg, next-intl reads cookies()
under the hood — which trips Next.js's "dynamic API used inside a
static / ISR render" guard and aborts with DYNAMIC_SERVER_USAGE.

Fix: pass the locale (already known from the URL via params) every
time we call next-intl on the server.

CHANGES
- src/components/layout/Footer.tsx
  - Now accepts `locale` as a prop instead of awaiting getLocale()
  - getTranslations({ locale, namespace: "Footer" })
- src/app/[locale]/layout.tsx
  - Passes locale prop to <Footer locale={locale} />
  - getMessages({ locale }) instead of getMessages()
- src/components/sections/PatrizioLegacy.tsx
  - Accepts `locale` prop, passes it to getTranslations
- src/app/[locale]/page.tsx
  - Renders <PatrizioLegacy locale={locale} />
- src/app/[locale]/news/page.tsx (body)
  - getTranslations({ locale, namespace: "NewsHub" })
- src/app/[locale]/heritage/page.tsx (body)
  - getTranslations({ locale, namespace: "HeritagePage" })

The generateMetadata / generateStaticParams paths in news/[slug] and
heritage already passed locale correctly — only the page bodies and
shared components were leaking dynamic reads.

Net effect: all five ISR pages render statically again, the error spam
in `docker compose logs app` clears, and /en/applications/digital-print
loads its server component without falling through to the error boundary.
2026-05-04 16:53:20 -05:00
davidherran 3d066fa67e fix: error boundaries + defensive try/catch on dynamic pages
Deploy to VPS / deploy (push) Has been cancelled
The /en/applications/digital-print page was still 500-ing after the
previous fixes. Without an error boundary, Next.js shows a generic
"Internal Server Error" with no detail — making remote diagnosis
require a `docker compose logs` round-trip every time.

ERROR BOUNDARIES (visible diagnostics)
- src/app/global-error.tsx: catches errors that bubble past every
  route's error.tsx, including ones from the root layout. Renders
  its own <html>/<body>.
- src/app/[locale]/error.tsx: locale-scoped boundary so the NavBar
  and Footer keep rendering around the error UI. Shows the actual
  error message + digest in a code block — much faster to diagnose
  than a blank 500.

DEFENSIVE WRAPPING (every async + every transform)
- applications/[slug]/page.tsx
  - getApplicationImages: try/catch around fs ops
  - generateMetadata: full body wrapped, falls back to safe defaults
  - getLocalizedData call wrapped (returns rawData if it throws)
  - Cases query already had try/catch — adds same for the locale map
  - JSON-LD build wrapped, falls back to empty array (still renders)
  - Default fallbacks for title/description/category to avoid
    productSchema receiving undefined fields
- news/[slug]/page.tsx
  - prisma.newsArticle.findUnique now has try/catch
  - getLocalizedData wrapped
  - JSON-LD build wrapped, only rendered if non-empty
  - publishedAt / updatedAt fallback to new Date() to avoid
    "Invalid time value" from articleSchema's date conversion

The combination means: if the underlying bug is in any of the SEO
helpers, JSON-LD generation, or i18n merging, the page now degrades
gracefully and shows the actual error in the UI instead of 500-ing.
2026-05-04 16:45:37 -05:00
davidherran 62506f10b4 fix: strip internal container port from redirect URLs
Deploy to VPS / deploy (push) Has been cancelled
The site was redirecting / -> https://rf-flux.com:3000/en, where :3000
is the container's internal port (only "expose"d, not published) — so
the browser saw ERR_CONNECTION_REFUSED.

Root cause: when running behind Nginx in standalone mode, Next.js (via
next-intl in this case) can build absolute redirect URLs that leak the
container's internal PORT/HOSTNAME env into the Location header.

TWO LAYERS OF DEFENCE
1. Nginx (nginx/conf.d/flux.conf)
   - Adds X-Forwarded-Host + X-Forwarded-Port so the upstream knows
     the public port (443) and host
   - proxy_redirect rewrites any Location header that still slips
     through with :3000 back to the public https://$host

2. Middleware (src/proxy.ts)
   - sanitizeRedirectLocation() runs after handleI18nRouting and
     scrubs Location headers that point at internal hostnames (app /
     localhost / 0.0.0.0) or the container port :3000, replacing them
     with the public host derived from x-forwarded-host / host header.

Either layer alone would fix the immediate symptom; together they
also prevent the same class of bug from showing up in any future
redirect path.
2026-05-04 16:32:45 -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 89505c73cc fix: pin @swc/helpers explicitly so npm ci works on npm 10 (Alpine)
Deploy to VPS / deploy (push) Has been cancelled
The Docker build on the VPS failed because:
  npm error Missing: @swc/helpers@0.5.21 from lock file

Cause: Next.js 16.1.6 declares @swc/helpers both as a direct dep at
0.5.15 AND as a peer dep ">=0.5.17". npm 11 (my local) accepts the
0.5.15 install and considers the peer satisfied. npm 10.9.7 (the
Alpine container in the VPS) is stricter — it tries to resolve the
peer to its own version and demands 0.5.21, which our lock file
didn't have.

Fix:
- Add @swc/helpers ^0.5.17 as a direct dependency so both npm 10 and
  npm 11 pick the same up-to-date version (resolved to 0.5.21).
- Regenerate package-lock.json from a clean install so it matches.

`npm ci` now succeeds in the docker-compose build.
2026-05-04 15:35:20 -05:00
davidherran f8606a45ff feat: branding asset serving + footer email/phone fields
Deploy to VPS / deploy (push) Has been cancelled
Two changes that together make Site Settings actually work end-to-end.

BRANDING ASSET SERVING (the broken thumbnails fix)
The favicon/logo previews were broken because uploaded files in
/public/branding had no path to reach the browser:
  1. The folder wasn't mounted into the app container, so uploads
     vanished on next deploy
  2. Nginx had no location block, so /branding/foo.png returned 404
     (everything not in cases/applications/news/parts/footage was a
     proxy_pass to Next.js, which doesn't serve from /public/branding
     in standalone mode)

Fix:
- docker-compose.yml: ./public/branding mounted to /app/public/branding
  (write side) AND /srv/branding (read-only side for Nginx)
- nginx/conf.d/flux.conf: new "location /branding/" block, same
  cache strategy as the other asset locations (max-age=300, must-revalidate)

FOOTER EMAIL + PHONE (David's request)
- siteSettingsTypes.ts: hqEmail and hqPhone fields added to FooterSettings,
  pre-filled with sales@lethepowerflux.com and +39 0424 287 492
- Footer.tsx: clickable mailto: and tel: links with Mail / Phone icons
  shown right under the HQ address. Hidden when fields are empty so the
  layout stays clean for editors who want to suppress contact info.
- /hq-command/dashboard/settings: new "Headquarters contact" group in
  the Footer tab with the two fields (auto-translate ignores them, since
  emails and phone numbers don't need translation).

DEPLOY (David)
  cd /opt/flux-srl
  mkdir -p public/branding   # one-time, creates the folder if missing
  git pull
  docker compose up -d --build app
  docker compose exec nginx nginx -t
  docker compose exec nginx nginx -s reload
2026-05-04 15:24:06 -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 09e6d0c7cf seo: dynamic sitemap + robots + per-page metadata + JSON-LD
Brings the site up to enterprise SEO standards. Google now gets a complete
machine-readable map of the content, with multilingual hreflang tags,
structured data for the knowledge panel, and rich Open Graph cards on
LinkedIn / WhatsApp / Twitter.

NEW
- src/app/sitemap.ts: dynamic sitemap.xml from Prisma. Emits 5 locales x
  every active application + every active news article, with hreflang
  alternates linking each translation. Hourly revalidation.
- src/app/robots.ts: robots.txt blocks /hq-command/, /api/, /parts (B2B
  auth-gated), points crawlers at the sitemap.
- src/lib/seo.ts: helpers for canonical URLs, hreflang alternates, and
  JSON-LD schemas (Organization, WebSite, Article, Product, BreadcrumbList).
- src/components/seo/JsonLd.tsx: server component that emits one
  application/ld+json script tag per page.

PER-PAGE generateMetadata
- Home: localized titles + descriptions in EN/IT/VEC/ES/DE
- News hub: title built from translations, hreflang tags
- News article: title/description from DB, OG image = cover, type=article,
  publishedTime + modifiedTime for date freshness signals
- Applications: title/description from DB, type=product, hero image
- Heritage: localized title/description

JSON-LD STRUCTURED DATA
- Site-wide (in root layout): Organization (with HQ address, founder,
  contact, social profiles) + WebSite — drives Google knowledge panel
- Article pages: Article schema with publisher/datePublished/dateModified
  — required for Google News / Discover eligibility
- Application pages: Product schema (FLUX brand, RF Industrial category)
  + BreadcrumbList — drives rich-snippet breadcrumb in search results

NOTES
- Open Graph metadataBase set from NEXT_PUBLIC_APP_URL so absolute URLs
  for OG images are correct (LinkedIn previews require absolute paths)
- All pages have canonical URLs to prevent duplicate-content penalties
- /parts already has noindex meta (B2B portal) — also blocked in robots
- No DB schema changes. Pure additions to /src/lib and /src/app.
2026-05-04 14:42:43 -05:00
davidherran f931ae281c i18n: preserve technical industry terms across all locales
Deploy to VPS / deploy (push) Has been cancelled
Some technical terms were being translated literally and reading awkwardly
to industrial buyers — fixed across IT, ES, VEC, DE so they match the
English source and industry convention.

PRESERVED IN ENGLISH (industry-standard, never translate)
- "Solid-State RF" — was "RF a Stato Solido" / "RF de Estado Sólido" /
  "RF a Stato Sołido" / "Solid-State-RF"
- "Microwave Systems" — was "Sistemi a Microonde" / "Sistemas de
  Microondas" / "Mikrowellensysteme" / "Sistemi Microwave"
- "Radio Frequency (RF)" — was "Radiofrequenza" / "Radiofrecuencia" /
  "Radiofrequensa" / "Hochfrequenztechnologie" (kept as the technical
  proper noun, with the RF acronym in parentheses for first reference)
- "Pulse Wave" — was "onde pulsate" / "ondas pulsadas" / "onde pulsà" /
  "Pulswellen-Technologie"

Files: messages/it.json, messages/es.json, messages/vec.json,
messages/de.json. messages/en.json unchanged. JSON syntax validated.
2026-05-04 13:14:20 -05:00
davidherran b9a744bdbc feat: site settings CMS — favicon, logo, footer, social, OG image
Adds a full settings dashboard at /hq-command/dashboard/settings so the
client can update favicon, logos, footer text, social links and OG image
without code changes — wired into the SiteSetting model from the previous
commit.

NEW
- src/lib/siteSettingsTypes.ts: pure types + defaults (client-safe import)
- src/lib/siteSettings.ts: server-only loader using the SiteSetting model
- /api/assets gains a "branding" flat scope that writes to /public/branding
- /hq-command/dashboard/settings/{actions.ts, page.tsx} with three tabs:
    1. Branding — favicon, Apple touch icon, main logo, email logo, OG
       image, theme color. Each field has helper text with recommended
       size/format and live preview.
    2. Footer — CTA banner, HQ address, legal links. Optional one-click
       AI translation to IT, VEC, ES, DE.
    3. Social — LinkedIn, Instagram, YouTube, contact email.

WIRED INTO LAYOUT
- src/app/[locale]/layout.tsx now uses generateMetadata + generateViewport
  to pull favicon, OG image and theme color dynamically. Adds Twitter
  Card metadata. Falls back to the default flux-logo when SiteSetting
  table is empty.
- src/components/layout/Footer.tsx reads CTA/HQ/legal copy from DB,
  supports per-locale overrides via translationsJson, and renders social
  icons (LinkedIn / Instagram / YouTube / Mail) only for filled fields.

UX FOR THE EDITOR (David's "12-year-old test")
- Drop-zone uploaders next to URL inputs — paste-or-upload either way
- Live image previews next to every branding field
- "Saved — live in 60 seconds" inline confirmation, no extra modals
- Recommended sizes spelled out next to each field (e.g. "PNG, square,
  minimum 512×512" for favicon)
- Tooltip explaining why each image is needed

NO SCHEMA CHANGES — uses the SiteSetting table created in the previous
commit. Existing rows untouched.
2026-05-04 12:47:10 -05:00
davidherran b9201a437c feat: hero carousel CMS + responsive mobile/iPad fix + flat-scope assets
Replaces the filesystem-scan hero (fs.readdirSync of /public/footage/main)
with a fully CMS-driven HeroSlide model. Editors can now drag-drop reorder,
toggle slides on/off, set focal points for proper mobile cropping, and
auto-translate per-slide captions.

NEW SCHEMA (additive — does not touch existing tables)
- HeroSlide: mediaUrl, mediaType, altText, order, isActive, focalPointX,
  focalPointY, translationsJson, timestamps
- SiteSetting: key-value JSON store for site-wide config (favicon, logo,
  footer, OG image) — wired up in next commit
- Migration 20260504120000_add_hero_slides_and_site_settings/migration.sql
  uses CREATE TABLE IF NOT EXISTS, additive only

HERO REEL REFACTOR (Bug #4 — responsive mobile/iPad)
- Switches from `images: string[]` to `slides: HeroSlideData[]` while
  keeping a backwards-compat path so legacy callers still work
- w-screen → w-full max-w-[100vw] (no horizontal scroll on iOS)
- h-[100vh] → h-[100svh] so iOS Safari URL bar doesn't push content
- Reduces title font sizes on small viewports (text-3xl → text-4xl
  → text-5xl → text-[5.5rem]) so the headline stays inside the canvas
- objectPosition driven by focal-point fields per slide
- Native <video> support for video slides

HQ COMMAND — /hq-command/dashboard/hero
- Drag-drop reorder, click-to-set-focal-point, inline alt-text editing
- Auto-save with "Saving…" / "Saved ✓" indicators
- Per-slide caption overrides (title, subtitle, descriptions)
- Optional one-click AI translation to IT, VEC, ES, DE
- Drop-zone uploader → /api/assets (scope=footage, flat folder)

API — /api/assets
- New flat scopes: "footage" (writes to /public/footage/main) and
  "branding" (writes to /public/branding) — slug-less for site-wide assets
- New buildPublicUrl helper centralises the URL convention
- Revalidate helper expanded with branding + settings scopes

HOME PAGE
- Reads hero slides from DB first; falls back to filesystem scan when
  HeroSlide table is empty (so production keeps working immediately
  after migration runs but before the editor populates rows)

DEPLOY NOTES
- After git pull on VPS, run the migration ONCE:
    docker compose exec app npx prisma migrate deploy
  Then:
    docker compose up -d --build app
  Existing data (AdminUser w/ 2FA, ClientUser, GlobalNode, Application,
  TimelineEvent, NewsArticle, HeritageSection, SparePart, OperationsSignal,
  NotificationRoute, PageContent) is NOT touched. Migration only creates
  two new tables.
2026-05-04 09:34:49 -05:00
davidherran 6e46808c27 fix: instant CMS uploads + heritage dark/light + ISR caching
Eliminates the need to run "docker compose build" after uploading
images via HQ Command. Heritage page now respects light/dark mode.

CACHE INVALIDATION
- New helper src/lib/revalidate.ts called from /api/assets and
  /api/public-upload after every upload, delete, folder create
- Pages switch from force-dynamic to ISR with revalidate=60
  (regenerated on demand whenever content changes, plus 60s safety)
- Nginx now sends "max-age=300, must-revalidate" instead of "expires 30d"
  on /cases/, /applications/, /news/, /parts/, /footage/, /operations-inbox/
  so browsers revalidate via If-Modified-Since (304s on unchanged files)
- Next.js Image Optimizer aligned with same TTL via minimumCacheTTL=300
  and adds /_next/image location block in Nginx for correct headers

HERITAGE DARK/LIGHT FIX (Bug #8)
- Replaces hardcoded #0A0A0C / #00F0FF / text-white with proper
  light + dark variants throughout markdown renderer (tables, lists,
  headings, blockquotes, paragraphs, images)
- Hero section, navigation pill, and CMS-driven sections now switch
  with the global theme toggle

SECURITY HARDENING
- Server actions bodySizeLimit reduced from 500MB to 50MB
  (large uploads still go through /api/assets which uses Nginx 500MB cap)

DEPLOY NOTES
- Run on VPS:
    git pull
    docker compose up -d --build app
    docker compose exec nginx nginx -s reload
- No DB schema changes in this commit. Existing 2FA users / data untouched.
2026-05-04 09:27:46 -05:00
davidherran 226b721721 fixes Network CoverImage3
Deploy to VPS / deploy (push) Has been cancelled
2026-04-21 11:06:40 -05:00
davidherran 8556ef78f4 fixes Network CoverImage2
Deploy to VPS / deploy (push) Has been cancelled
2026-04-21 10:47:58 -05:00
davidherran 64f23d833c fixes Network CoverImage
Deploy to VPS / deploy (push) Has been cancelled
2026-04-21 09:52:54 -05:00
davidherran 7ae5685ca9 fixes News
Deploy to VPS / deploy (push) Has been cancelled
2026-04-21 09:44:26 -05:00
davidherran da5a2ce2a2 fixes ApplicationIMG2
Deploy to VPS / deploy (push) Has been cancelled
2026-04-17 16:28:56 -05:00
davidherran 7fe1863c68 fixes ApplicationIMG
Deploy to VPS / deploy (push) Has been cancelled
2026-04-17 14:37:51 -05:00
davidherran 603ef51637 fixes Application nodeSlug
Deploy to VPS / deploy (push) Has been cancelled
2026-04-16 15:50:43 -05:00
davidherran 9d0ec57d7c fixes Markdown & Apex Globe
Deploy to VPS / deploy (push) Has been cancelled
2026-04-16 12:45:48 -05:00
davidherran 21d0f9ee1c fixes Markdown
Deploy to VPS / deploy (push) Has been cancelled
2026-04-16 10:29:20 -05:00
davidherran 69eb449da8 chore: untrack public assets, manage them outside git
Deploy to VPS / deploy (push) Has been cancelled
2026-04-16 13:18:26 +00:00
davidherran 86ef0e2d75 agregar idiomas a parts
Deploy to VPS / deploy (push) Has been cancelled
2026-04-08 12:47:26 -05:00
davidherran 20976432c9 fix: asset manager no longer closes edit modal on select
Deploy to VPS / deploy (push) Has been cancelled
2026-04-08 12:14:40 -05:00
davidherran c5b78c539e fix: nginx serves uploaded assets directly, docker volumes for all public dirs
Deploy to VPS / deploy (push) Has been cancelled
2026-04-08 17:07:49 +00:00
davidherran 80bb1962b2 fix: bodySizeLimit type
Deploy to VPS / deploy (push) Has been cancelled
2026-04-08 11:28:56 -05:00
davidherran cff3dc44be fix: upload limits and timeouts
Deploy to VPS / deploy (push) Has been cancelled
2026-04-08 11:26:46 -05:00
davidherran 3c20ce5c37 fix: upload limit 500MB, mount all public dirs in docker
Deploy to VPS / deploy (push) Has been cancelled
2026-04-08 15:56:56 +00: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
davidherran b275b19f08 Initial commit from Create Next App 2026-02-22 11:45:37 -05:00