8a98f880479c7e06f435625e27a596fd9376b34d
74 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8a98f88047 |
fix(ui): dark mode support on Team and Privacy pages
Deploy to VPS / deploy (push) Has been cancelled
The new public pages were built light-mode only, so toggling dark mode (which adds .dark to <html>) left them unchanged. Add dark: variants matching the rest of the site: - team/page.tsx: page bg, eyebrow, title, description, empty state - team/TeamGrid.tsx: card bg/border/shadow, portrait gradient, name, role, bio, social-link buttons (cyan accent on hover in dark) - privacy/page.tsx: page bg, title, dates, the template-notice callout, section headings, body text, mailto links Palette consistent with ApplicationClient/CaseStudyModal: light #F5F5F7/#1D1D1F/#0066CC -> dark #050505/#fff/#00F0FF, secondary #A1A1A6, cards #111. Verified: build compiles, TypeScript clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7c689e034e |
fix(session): validate SESSION_SECRET lazily, not at module load
Deploy to VPS / deploy (push) Has been cancelled
The module-level SESSION_SECRET check threw during `next build` (page-data collection imports session.ts but has no runtime env vars), breaking the Docker build at /hq-command/dashboard. Move the check inside getEncodedKey() so it runs at runtime when a session is actually created. Security is unchanged: the secret is still never defaulted, and any attempt to mint a session without a valid 32+ char secret throws loudly. It just no longer blocks the build. Verified: `next build` now compiles with SESSION_SECRET unset (replicating the Docker builder stage) — 56/56 pages generated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3e0b286f1a |
fix(deploy): stop tracking public/team + public/branding
Deploy to VPS / deploy (push) Has been cancelled
These upload directories are owned by root on the VPS (the container entrypoint chowns mounted volumes to 1001:1001), so `git reset --hard` failed trying to create public/team/.gitkeep as the deploy user. Treat them like the other upload dirs (applications, cases, news, parts, operations-inbox, footage) — gitignored, created by the Docker volume mount at runtime. Removes public/team/.gitkeep from tracking and adds public/team/ + public/branding/ to .gitignore. This unblocks clean `git reset --hard origin/main` on the VPS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e0399ccf3b |
feat(i18n): preserve English technical terms in AI translation
Deploy to VPS / deploy (push) Has been cancelled
Fixes terms like "Radio Frequency" and "solid-state" being mistranslated
into each locale. English is the master language, so brand/technical terms
must stay in English everywhere.
- New src/lib/translationGlossary.ts: a curated PROTECTED_TERMS list plus
deterministic mask/unmask helpers. Before translation each term is
replaced with a stable __FLUXTERM_n__ placeholder; after translation the
placeholders are restored to their canonical English form. Preservation
is therefore guaranteed, not left to the model's discretion.
- aiTranslator.ts now masks every field before sending, restores every
field of every locale afterwards, and reinforces the rule in the prompt
(explicit glossary + "keep tokens byte-for-byte"). A tolerant cleanup
regex recovers placeholders even if the model adds stray spaces, so a
mangled token never leaks to the public site.
- Whole-word, case-insensitive matching ("RF" in "surf" is not touched);
longest terms masked first to avoid overlaps; casing normalised to the
canonical brand form on restore.
- 4 new golden tests (17 total) cover round-trip, simulated translation,
whole-word safety, and mangled-token recovery.
To extend: add terms to PROTECTED_TERMS — no other change needed.
Verified: build compiles, TypeScript clean, npm run test:ai 17/17 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bf8b2aa631 |
feat(map): connect Global Map cases to their full application case pages
Closes the disconnect where a case study opened from the 3D globe had no
path to its full write-up — you had to leave the globe, open the right
application, and hunt for it.
- CaseStudyModal: new "View full case study" CTA for real installations
(not events / HQ). It deep-links via the locale-aware next-intl Link to
/applications/{application}#case-{nodeId}, closes the modal, and fires a
case_study_viewed GA event.
- ApplicationClient: on mount it reads a "#case-<id>" hash, auto-expands the
matching case in the "Proven Solutions" wall, and smooth-scrolls to it.
Each case row now carries id="case-<id>" + scroll-mt for correct offset.
- viewFullCase string added to the CaseStudyModal namespace in all 5 locales.
The GlobalNode.application field (already equal to the Application slug) is
the join key — no schema change needed.
Verified: production build compiles, TypeScript clean, 5 message files valid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
afcaf991b5 |
feat(applications): drag-to-reorder on the public site
Editors can now control the order applications appear in, the same way they already reorder Hero slides. - Application gains an `order Int @default(0)` column (additive migration 20260605120000_add_application_order, IF NOT EXISTS, safe for deploy) plus an (isActive, order) index. - New reorderApplications(orderedSlugs) server action — single $transaction renumbering, mirrors reorderHeroSlides. - HQ applications panel: rows are now draggable by a grip handle (HTML5 DnD, optimistic local reorder, persisted on drop, toast feedback). - All public-facing queries now order by [order asc, createdAt asc]: home ApplicationsDashboard + GlobalOperations, the footer apps list, and the HQ list itself. Existing rows default to 0 so current order is preserved until the editor drags something. Verified: production build compiles, TypeScript clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fbfffb28d9 |
feat(analytics): activate GA4 (G-KQ1JRV3KN7) + GDPR privacy page + GSC support
Client provided the GA4 Measurement ID and approved the standard policy. - Activate analytics: NEXT_PUBLIC_GA_ID set to the FLUX property G-KQ1JRV3KN7 in the env template, with the same value as the docker-compose build-arg fallback so it works out of the box on deploy. (GA Measurement IDs are public — they ship in page HTML — safe to commit.) - New GDPR-compliant Privacy & Cookie Policy page at /[locale]/privacy (all 5 locales), linked from the consent banner. Includes a clearly marked template disclaimer for legal review and a TODO on the contact email. Added to sitemap. - Consent banner now links via the locale-aware next-intl Link. - Google Search Console: optional NEXT_PUBLIC_GSC_VERIFICATION env var emits the google-site-verification meta tag (Dockerfile arg + docker-compose wired). Empty by default. Verified: build inlines G-KQ1JRV3KN7 into the client bundle; the 5 /privacy routes render; TypeScript clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
148aefc68f |
feat(team): public Team page + HQ CMS panel
New "Team" section — a LinkedIn-style minimal profile page for the FLUX team, fully editable from the HQ Command Center. Data model: - New TeamMember model (name, role, bio, photoUrl, optional social links: email/linkedin/x/website, order, isActive, translationsJson). - Additive migration 20260602120000_add_team_member (IF NOT EXISTS guards). - Name stays as written; role + bio are translatable via the AI engine. HQ panel (/hq-command/dashboard/team): - Drag-to-reorder (same HTML5 pattern as the Hero panel). - Inline auto-save for name/role/visibility; expandable editor for photo upload, bio, social links, and AI auto-translate to IT/VEC/ES/DE. - Photo upload reuses /api/assets with a new flat "team" scope -> /public/team/. - Dashboard tile added. Public page (/[locale]/team): - Responsive card grid (framer-motion stagger), portrait + name + role + bio + social icons (only the links that exist render). - Per-member Person JSON-LD + breadcrumb for SEO. - Localized via getLocalizedData; new TeamPage namespace in all 5 locales. - NavBar item "Team" inserted before "Spare Parts" (translated 5 locales). - Added to sitemap. Infra: - "team" scope registered in /api/assets (SCOPE_ROOTS + FLAT_SCOPES + buildPublicUrl) and revalidate.ts (RevalidateScope + path). - Nginx serves /team/ from disk; docker-compose mounts public/team in both app and nginx (rw + ro). Verified: production build compiles, all 5 /[locale]/team routes + the HQ panel render; TypeScript clean; 5 message files valid JSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1ee8288c7e |
feat(analytics): GA4 with GDPR Consent Mode v2
Google Analytics integration, off by default and GDPR-compliant for EU:
- src/lib/analytics/gtag.ts: typed event helpers + consent control. Every
function is a safe no-op when NEXT_PUBLIC_GA_ID is unset.
- GoogleAnalytics.tsx: loads gtag.js with Consent Mode v2, all storage
defaulting to "denied". anonymize_ip on, send_page_view off.
- ConsentBanner.tsx: on-brand cookie banner, localized to all 5 locales,
persists choice for one year, flips analytics_storage to granted on accept.
- PageViewTracker.tsx: fires page_view on App Router client navigation
(inside Suspense for useSearchParams).
- Key conversion events wired: ai_consultation_submitted (primary funnel
goal) and ai_chat_opened.
- Consent strings added to messages/{en,it,vec,es,de}.json.
Build plumbing:
- NEXT_PUBLIC_GA_ID inlined at build time via Dockerfile ARG +
docker-compose build.args (NEXT_PUBLIC_* must exist during next build,
not just runtime).
- Nginx CSP extended to allow googletagmanager.com + google-analytics.com.
- env template documents NEXT_PUBLIC_GA_ID (empty = analytics disabled).
Verified: production build inlines the Measurement ID into the client
bundle; site builds cleanly both with and without the ID set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3a94e7c003 |
feat(security+ai): security hardening + FluxAI conversation analytics
Security (critical):
- SESSION_SECRET fail-fast: refuse to boot without a 32+ char secret
(src/lib/session.ts, src/app/actions/clientAuth.ts)
- Rate limit with pluggable backend: in-memory by default, auto-promotes
to Upstash Redis when REDIS_URL is set (src/lib/rateLimit.ts)
- CSRF (double-submit HMAC) + Zod validation on /api/consultation;
new /api/csrf endpoint mints tokens (src/lib/csrf.ts)
- escapeHtml + safeMailto helpers; consultation email template now
fully escapes user-controlled fields (src/lib/escapeHtml.ts)
- Magic-byte validation for /api/public-upload — rejects HTML/JS
payloads renamed to .png/.mp4 (src/lib/fileType.ts)
- Nginx: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
Permissions-Policy + 5r/m upload zone for /api/public-upload and
/api/assets (nginx/conf.d/flux.conf)
Quality:
- Delete GlobalOperations_old.tsx dead code (310 LOC)
- NavBar: replace 2s session polling with CustomEvent("flux:session-
changed") + visibilitychange listener (no more interval leaks)
- Type-safe CMS shapes via src/types/cms.ts (replaces any[] in
ApplicationsDashboard + GlobalOperations)
- /api/health now pings Postgres; docker-compose healthcheck added
- Structured JSON logger (src/lib/logger.ts) — drop-in replacement
for console.error across API routes
- Prisma indices on isActive/category/nodeType filters
FluxAI persistence + analytics:
- New models AiConversation + AiEvent with funnel stage detection
(DISCOVERY -> QUALIFY -> RECOMMEND -> HANDOFF) and OperationsSignal
back-ref so converted chats link to their consultation ticket
- /api/chat persists every user msg, ai msg, tool call, tool result;
IP is sha256-hashed with SESSION_SECRET salt; promptCacheKey wired
for when @ai-sdk/openai lands the feature
- New HQ dashboard at /hq-command/dashboard/conversations: 4 KPIs
(total, conversion rate, avg messages, avg tools), funnel + industry
breakdowns, last-50 table, per-id transcript with tool timeline
- SilentObserver sends sessionId/locale/pageUrl in transport body so
the route can stitch messages into the same conversation
- src/lib/aiSessionId.ts: localStorage UUID with sessionStorage +
in-memory fallbacks for privacy mode
- Golden tests via node --test (13 cases, no new deps);
npm run test:ai
Migration:
- prisma/migrations/20260526180000_add_indexes_and_ai_telemetry —
additive only, IF NOT EXISTS guards, safe for migrate deploy
env template hardened: SESSION_SECRET documented as required + how
to generate; REDIS_URL/REDIS_TOKEN documented as opt-in for multi-
instance deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
792dd6794b |
fix(ai): guard datasheet/gallery/videos against non-array values
Deploy to VPS / deploy (push) Has been cancelled
EquipmentConfigurator and CaseStudyViewer crashed with "e.datasheet.find is not a function" when DB nodes had malformed JSON in specificDatasheetJson. Both components now normalize datasheet, gallery, and videos to safe arrays via Array.isArray before any array method calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
7278d5d00f |
fix(ai): defensive navigation — auto-resolve phantom section IDs to page routes
Deploy to VPS / deploy (push) Has been cancelled
GPT-4o ignores tool description instructions and sends section:"news" instead of url:"/news". The client now maintains a whitelist of valid homepage DOM IDs and a fallback map (news→/news, heritage→/heritage, parts→/parts). Any section value not in the homepage whitelist gets auto-resolved to the correct page route via router.push. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
8941d1a2c3 |
feat(ai): FluxAI Level 2 — smart recommender, funnel-aware SPIN, contextual quick-replies
Deploy to VPS / deploy (push) Has been cancelled
Three improvements to the FluxAI sales intelligence: 1. New `recommend_application` tool: analyzes prospect's industry, problem, and process keywords against the database to rank-match the best FLUX products with confidence scores. Bridges the gap between "I have a problem" and "here's our solution." 2. Funnel-aware system prompt: replaces flat SPIN with 4-stage pipeline (Qualify → Recommend+Educate → Quantify+Prove → Specify+Convert) with clear rules for when to ask vs. act. 3. Contextual quick-reply buttons: after each AI response, dynamic suggestions appear based on which tools were used — guiding the prospect through the natural next step in the funnel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
95132476ae |
feat(ai): extend FluxAI navigation with cross-page routing
The navigate_to_section tool now supports two modes:
A) Same-page scroll — scrollIntoView to real homepage DOM IDs
(technology, applications-dashboard, applications-deep, global,
our-story, legacy)
B) Cross-page routing — router.push to /applications/{slug},
/news, /heritage, /parts with automatic locale prefix.
Fixed: system prompt listed phantom section IDs (hero, news,
heritage, timeline, parts-catalog, contact) that don't exist in
the DOM — causing all non-homepage navigations to silently fail.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
c45a5be99e |
feat(i18n): translate hardcoded article page strings to 5 locales
Deploy to VPS / deploy (push) Has been cancelled
- Add ArticlePage namespace (backToNewsHub, backToNews, mediaGallery, joinLinkedIn, internalRelease) to all 5 locale message files (en, it, es, de, vec) - Replace 5 hardcoded English strings in news/[slug]/page.tsx with getTranslations() calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
cb7458cded |
feat(seo): visual breadcrumb navigation on article + application pages
- Create Breadcrumbs.tsx server component — semantic <nav> + <ol>/<li> with aria-current, ChevronRight separators, Apple-clean styling - Add breadcrumbs to news article hero overlay (reuses JSON-LD crumbs) - Add breadcrumbs to application detail hero (passed as prop to client component) - Refactor breadcrumb data into shared array for JSON-LD + visual nav Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
7ec99734c5 |
feat(seo): LocalBusiness + CollectionPage structured data schemas
- Add localBusinessSchema() with geo coords, phone, opening hours for Google Local Pack and Knowledge Panel visibility - Add collectionPageSchema() with ItemList for article listing pages - Inject LocalBusiness alongside Organization+WebSite in root layout - Inject CollectionPage in /news hub page with article items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
8d80cbbc27 |
perf(seo): image sizes, semantic HTML, X-Robots-Tag headers
- Add `sizes` prop to 8 <Image> components across news, heritage, and
application pages — tells the browser which srcset variant to download,
improving LCP and reducing bandwidth
- Replace date <span> with <time dateTime={ISO}> on news pages —
Google uses datetime for article freshness signals
- Wrap news cards and article content in <article> tags — semantic
boundary for crawlers
- Add X-Robots-Tag: noindex, nofollow header to all /hq-command
responses in proxy.ts — defense-in-depth alongside meta robots
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
6b9a94490b |
fix: add dedicated square favicon for HQ Command Center
Deploy to VPS / deploy (push) Has been cancelled
Programmatic icon.tsx using Next.js convention — generates a 32×32 PNG with dark bg + cyan "F" glyph. Replaces the inherited 16:9 fallback that appeared stretched in browser tabs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
c3d196df03 |
feat: auto-optimize images on CMS upload via AssetBucketBrowser
Deploy to VPS / deploy (push) Has been cancelled
Enables the existing Sharp pipeline for all uploads — WebP conversion, auto-orient, 2560px cap, content-hash filenames. Upload toast now shows compression savings percentage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
8ac372125a |
feat: dedicated 3D Models bucket in AssetBucketBrowser
Deploy to VPS / deploy (push) Has been cancelled
Add a "3D Models" bucket (path: models, accept: .glb/.gltf/.usdz) to the cases scope. Previously 3D files were mixed into the Media bucket at the root. Now they have their own tab with proper file type hints and purple accent color matching the AR viewer UI. - cases/media bucket no longer lists .glb/.gltf/.usdz in accept - New bucketHint for models bucket warns on non-3D file uploads - Network page 3D tab updated with purple accent + "3D Models" button label pointing to the dedicated bucket Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
f6c3b89e08 |
feat: dashboard operations intelligence — analytics cards + signal breakdown
Deploy to VPS / deploy (push) Has been cancelled
Add real-time operational metrics to the HQ dashboard: - Signals This Month: count with trend % vs previous 30 days - Pending Actions: highlights when tickets need attention (rose border) - Email Delivery: success rate percentage + sent/failed counts - B2B Clients: approved vs total registered Plus a Signal Breakdown panel showing all-time distribution by type (Orders, Diagnostics, Consultations, B2B Access) with proportional bars. All queries run in parallel via Promise.all for minimal latency. Wrapped in try/catch so the dashboard never breaks if DB is slow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
9c1e0cce01 |
feat: inbox UX polish — auto-save routing, search filter, email badges
Deploy to VPS / deploy (push) Has been cancelled
Three improvements to the Operations Inbox: 1. Auto-save email routing: typing in the routing config auto-saves after 800ms with inline "Saving…" / "✓ Saved" indicator. No more manual Save button per route. 2. Search filter: instant client-side search across name, company, email, and ticket ID. Composes with existing type/status filters. 3. No-email badge: signals without email delivery now show a dim MailQuestion icon in the list view instead of nothing, making it clear at a glance which tickets need attention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
7502c9c674 |
feat: generateStaticParams for application + news slug pages
Deploy to VPS / deploy (push) Has been cancelled
Pre-render all known slugs at build time so first visits are instant from cache. New slugs added after deploy render on-demand and get cached by ISR (revalidate=60). try/catch ensures the build never fails if the DB is unreachable during docker build — pages just fall back to on-demand rendering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
ce8a13d7f8 |
fix: restore ISR on public pages — isolate DYNAMIC_SERVER_USAGE root cause
Deploy to VPS / deploy (push) Has been cancelled
Root cause: next-intl's getMessages/getTranslations internally resolves requestLocale by reading cookies/headers, which trips DYNAMIC_SERVER_USAGE under ISR. Fixed by calling setRequestLocale(locale) in layout + every public page — caches the locale in React cache so next-intl never reads cookies. Changes: - [locale]/layout.tsx: +setRequestLocale, +generateStaticParams (5 locales), wrap NavigationManager in <Suspense> (uses useSearchParams) - 5 public pages: force-dynamic → revalidate=60, +setRequestLocale - HQ dashboard pages: unchanged (still force-dynamic for auth) Build verified: home/heritage/news pre-render as SSG with 1m revalidation, slug pages render on-demand with ISR cache. Nginx s-maxage=60 remains as safety net. Zero DYNAMIC_SERVER_USAGE errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
59a146ef10 |
feat: HQ-wide Toast + Confirm — no more browser alert()/confirm() popups
Deploy to VPS / deploy (push) Has been cancelled
The Inbox panel got the polished Toast + Confirm primitives a few
commits back. This commit propagates them across every other panel in
HQ Command so the editor experience is uniformly on-brand. No more
1990s browser dialogs interrupting the dark CMS look.
NINE PANELS, ELEVEN CALL SITES UPGRADED
- health/page.tsx — DB export errors → toast
- network/page.tsx — Delete deployment → confirm + toast
- heritage/page.tsx — Delete section → confirm + toast
- users/page.tsx — Revoke architect → confirm + error toast
- news/page.tsx — Delete article → confirm + toast
- applications/page.tsx — Save error toast + delete-app confirm
- parts/page.tsx — Delete component → confirm + toast
- hero/page.tsx — Delete slide → confirm + toast
- timeline/page.tsx — Delete milestone → confirm + toast
Each destructive confirm now spells out what it does ('Permanently
remove this case from the global map. The asset folder on disk is
kept for safety') instead of a generic 'Delete?' prompt — much
clearer for editors who aren't sure whether files get nuked too.
Each success toast names the action ('Component deleted', 'Slide
deleted', 'Architect access revoked') so the editor sees exactly
what fired. Errors come in as red toasts with the actual error text.
NO BACKEND CHANGES. Pure UX layer on top of existing actions.
The HqUiProvider was already mounted in src/app/hq-command/layout.tsx,
so wiring up was just useHqUi() per page + the replacement calls.
|
||
|
|
e177bca92f |
feat: HQ Hero — import existing /footage/main files as managed slides
Deploy to VPS / deploy (push) Has been cancelled
The hero CMS only listed HeroSlide rows from the database. Files dropped
directly into /public/footage/main were rendering on the live site (via
the home page's filesystem fallback) but invisible in the editor — so
the editor couldn't manage their focal point, alt text, ordering or
on/off toggle without re-uploading.
NEW SERVER ACTIONS (src/app/hq-command/dashboard/hero/actions.ts)
- listImportableFootage()
Scans /public/footage/main, returns the list of files that aren't
already referenced by a HeroSlide row. Each entry has filename,
publicUrl, mediaType (image/video), file size and mtime.
- importFootageFiles(filenames[])
For each selected filename: validates extension, checks existence,
skips already-imported files, derives a sensible alt text (filename
with extension stripped, dashes/underscores → spaces, leading
numeric prefix removed), and creates a HeroSlide row at the next
available order position. Returns {created, skipped} so the UI can
show a precise toast.
UI (src/app/hq-command/dashboard/hero/page.tsx)
- New amber-tinted panel above the slide list, visible only when
importable.length > 0. Shows every uncovered file as a thumbnail
card with checkbox-style selection.
- 'Select all' / 'Clear' toggle, 'Import all' for one-click bulk
import, 'Import N selected' once a subset is picked.
- After import: panel re-renders empty (since those files now have
HeroSlide rows) and the new slides appear in the regular list,
ready for focal-point and caption editing.
NO BACKEND-DATA CHANGES BEYOND ROW CREATION
- Filesystem untouched (no rename, no move). Files keep their original
path under /public/footage/main, the new HeroSlide row simply points
at /footage/main/<filename>.
- Public-facing pages render the same images either way (they read
from HeroSlide first, footage scan second).
- After import: the home page now reads the slides from the DB and
uses the focal-point + alt text data the editor sets.
|
||
|
|
778b35f15a |
feat: AssetBucketBrowser polish — bulk select, drag-move, rename in place
Deploy to VPS / deploy (push) Has been cancelled
The unified bucket browser graduated from "acceptable" to "actually
useful for bulk work". Editors can now manage dozens of files in a
single session without dragging each one through a modal.
NEW FEATURES (frontend)
1. BULK SELECTION
- Click on a file when nothing's selected → opens it as before.
- Click on the corner checkbox, or click the file once selection is
active → toggle that one in/out.
- Shift-click → range select between last anchor and current item.
- Cmd/Ctrl-click → toggle without affecting others.
- "Select all" toggle in the toolbar respects the search filter.
2. BULK ACTIONS TOOLBAR
When at least one file is selected the toolbar morphs into:
[N selected] [Delete] [Move to: Videos | Renders | …]
Delete fires the new bulk DELETE endpoint with filePaths[], shows
a single toast for the whole batch + per-file failure breakdown.
Move iterates PATCH /api/assets per file (sequential, with a 'Moving…'
indicator in the bucket helper bar).
3. DRAG TO MOVE BETWEEN BUCKETS
Drag any file (or the whole selection if you started the drag from
a selected file) onto another bucket tab. The tab highlights green
with 'drop to move' while you hover. Drop fires the same per-file
PATCH flow. No dialog, no friction.
4. RENAME IN PLACE
Double-click a filename (in either grid or list view) → input opens
in place. Enter saves, Escape cancels, blur saves. Sanitizes to
safe characters. PATCH endpoint refuses to overwrite an existing
file (returns 409, surfaced as a toast).
5. KEYBOARD HINT FOOTER
Bottom-of-modal cheat sheet: Click / ⇧Click / ⌘Click / 2× click /
drag onto another tab. So new editors don't have to discover the
power-user features.
NEW BACKEND (src/app/api/assets/route.ts)
PATCH method
{ scope, slug, fromPath, toPath } → fs.renameSync.
Used for both rename (same dir, new name) and move (different bucket).
Refuses to overwrite an existing destination (409 conflict).
Creates intermediate folders if needed.
DELETE extended
Now accepts either { filePath: "x" } or { filePaths: ["a", "b"] }.
Bulk path deletes one-by-one and returns per-file success/failure
so the UI can show a precise toast.
REVIEWED FOR REGRESSIONS
- Single-file API still works — old { filePath } DELETE shape preserved.
- The 4 inline AssetManager call sites (network, news, applications,
parts) use AssetBucketBrowser via the alias added in the previous
commit; their integration is unchanged. Same props, same onSelect
callback shape.
- Toast/Confirm calls go through the existing HqUiProvider mounted in
hq-command/layout.tsx — no extra wiring.
|
||
|
|
330fecc3cc |
feat: favicon multi-variant — 1 upload generates 6 sizes + PWA manifest
Deploy to VPS / deploy (push) Has been cancelled
The single-favicon upload was leaving most platforms with the wrong icon:
Android Chrome looks for 192×192, iOS for 180×180, Windows for 48×48,
HiDPI tabs for 32×32, legacy tabs for 16×16, PWA splash for 512×512 —
six different files, all from the same source image.
ONE EDITOR ACTION → SIX FILES
- POST /api/branding/favicon takes a single PNG/JPG/WebP (≥ 192×192,
ideally 512×512+) and runs it through sharp to produce:
/branding/favicon-16.png /branding/favicon-32.png
/branding/favicon-48.png /branding/favicon-180.png
/branding/favicon-192.png /branding/favicon-512.png
/branding/favicon-master.png (kept for re-generation)
- Square-ish source images get center-cropped automatically.
- Returns the variant list + soft warnings ("not square — center-cropped",
"upload at least 512×512 for retina") so the editor sees what happened.
- Validates dimensions and file type, caps at 20MB.
ROOT LAYOUT (src/app/[locale]/layout.tsx)
- Detects whether the multi-variant set exists on disk. If yes, emits
<link rel="icon" sizes="16x16 / 32x32 / 192x192">, an explicit
Apple touch link, and a Safari pinned-tab mask-icon. If no, falls
back to the legacy single faviconUrl from SiteSetting.
- Adds <link rel="manifest" href="/manifest.webmanifest"> so Android
Chrome surfaces "Add to Home Screen" properly.
PWA MANIFEST (src/app/manifest.ts)
- Auto-served at /manifest.webmanifest via Next.js's MetadataRoute.
- Pulls themeColor from SiteSetting.branding so editor changes there
cascade to the standalone-app theme.
- Lists 192 + 512 icons with both 'any' and 'maskable' purposes for
Android adaptive icons.
HQ SETTINGS UX (src/app/hq-command/dashboard/settings/page.tsx)
- New <FaviconMasterField/> at the top of the Branding tab — replaces
the old standalone Favicon ImageField. Shows a 32×32 preview, an
upload button, the generation result with each variant's preview,
and warnings when the source isn't ideal.
- The Apple Touch Icon ImageField below it stays as an override path
for editors who want a different icon on iOS (rare, but supported).
- Cache buster query string on previews so re-uploads visibly refresh
without forcing the editor to hard-reload.
NO BACKEND/DB CHANGES OUTSIDE FILE I/O.
The SiteSetting.branding row is unchanged. The favicon variants are
plain files on disk under public/branding/, served by Nginx with the
existing 5-min cache headers.
|
||
|
|
ea1300bfdc |
feat: HQ Toast + Confirm dialog primitives, replace browser alert()/confirm()
Deploy to VPS / deploy (push) Has been cancelled
The Operations Inbox panel was using browser-native alert() and
confirm() — those popups break out of the dark-themed CMS aesthetic
and look like a 1998 form validation. Worse, they're modal blocking,
so the editor can't see the surrounding context (the ticket card,
the activity feed) while the dialog is up.
NEW PRIMITIVES (src/components/hq/Toast.tsx)
- <HqUiProvider> mounts a global toast stack (bottom-right) and a
global confirm dialog. Mounted in src/app/hq-command/layout.tsx so
every panel under /hq-command/* can use it.
- useHqUi() returns { toast, confirm }:
ui.toast("Saved", "success") // ephemeral, 3s
ui.toast("Save failed: ...", "error") // 5s
await ui.confirm({ // returns boolean
title: "Delete ticket",
message: "This permanently...",
confirmLabel: "Delete",
destructive: true,
})
- Toasts auto-dismiss with a manual close button. Confirm dialog
uses red accent for destructive actions.
- Zero deps. ~140 lines total.
INBOX (src/app/hq-command/dashboard/inbox/page.tsx)
- All 5 alert() calls replaced with ui.toast() — success / error
toned and persisting just long enough to read.
- All 4 confirm() calls replaced with ui.confirm() — destructive
ones (delete ticket, purge files, delete client) get the red
accent + 'cannot be undone' copy.
- Action descriptions are richer ('Resolve & purge attachments'
instead of 'Resolve') so the editor knows exactly what fires.
NO BACKEND CHANGES. Pure UX layer on top of the existing actions.
|
||
|
|
aebcabd767 |
fix: nginx cache ignores Set-Cookie + adds explicit valid 60s
Deploy to VPS / deploy (push) Has been cancelled
The HTTP cache was always reporting MISS on /en even on consecutive hits. Two reasons converging: 1) next-intl writes a NEXT_LOCALE cookie on response, so every upstream reply included a Set-Cookie header. Nginx refuses to cache responses with Set-Cookie by default — that's a safe default to avoid leaking session cookies, but it's the wrong default for our public marketing pages, where the cookie just records a locale preference and the HTML body is identical for every visitor on the same URL. 2) proxy_cache_valid wasn't set, so even when Cache-Control would have authorised caching, Nginx fell back to its conservative no-cache stance. Fix: - proxy_ignore_headers Set-Cookie X-Accel-Expires Expires; - proxy_hide_header Set-Cookie; - proxy_cache_valid 200 60s; Net result: marketing pages now actually cache. The Set-Cookie is still emitted by Next.js (the upstream is unchanged), Nginx just strips it before storing/relaying — locale detection still works because next-intl persists locale through the URL prefix anyway. DEPLOY (David) cd /opt/flux-srl git pull docker compose restart nginx docker compose exec nginx nginx -s reload Then verify: curl -sI https://rf-flux.com/en | grep -iE 'x-cache|cache-control|set-cookie' curl -sI https://rf-flux.com/en | grep -iE 'x-cache|cache-control|set-cookie' Second hit should show: x-cache-status: HIT |
||
|
|
1a4abfc7f2 |
nginx: include lethepowerflux.com → rf-flux.com 301 redirect
Deploy to VPS / deploy (push) Has been cancelled
The VPS already had a server block redirecting lethepowerflux.com and www.lethepowerflux.com to https://www.rf-flux.com, but it lived only on the live config — not in git. That's why the latest pull complained about local changes that would be overwritten. Adding it here so the repo is the single source of truth for the Nginx config again. Behaviour is unchanged on the VPS (redirect was already in place) — this commit just lets future git pulls flow without manual intervention. |
||
|
|
abd75798ef |
feat: timeline + heritage HQ panels — drag-drop + inline auto-save
Deploy to VPS / deploy (push) Has been cancelled
Bringing the same UX pattern we shipped on Hero and Settings to the
Company Legacy and Our Heritage panels. No more modal-driven editing
where you have to click "Edit" → modify → click "Save" → wait → close
modal. Every field is now editable in place; saves fire on blur and
flash a "Saved ✓" confirmation; rows reorder via drag-drop.
NEW SERVER ACTIONS (additive — old formData-style actions still exist)
- timeline/actions.ts:
patchTimelineEvent(id, partial) — granular field update
reorderTimelineEvents(ids[]) — single transaction
createTimelineStub() — instant blank row
- heritage/actions.ts:
patchHeritageSection(id, partial) — granular field update
reorderHeritageSections(ids[]) — single transaction
createHeritageStub(type) — text/image/video stubs
PAGES REWRITTEN
- timeline/page.tsx: 208 → 215 lines, but radically simpler — drag
handle + inline year input + inline title + inline description
textarea per row, plus eye toggle for isActive and a trash button.
Global "Auto-translate edits" checkbox at top applies to every patch.
- heritage/page.tsx: 209 → 270 lines (more functionality fits in less
cognitive load). Three coloured "Add" buttons (Text / Image / Video)
spawn a stub that renders the right card type. Image/Video cards have
a built-in MediaPicker with upload-or-paste-filename + live preview.
UX UPGRADES THE EDITOR SEES
- Type-and-tab to save. No modals, no submit buttons per row.
- Saving / Saved ✓ badge per row so it's obvious when a field has
hit the database.
- Drag handle + drop target on every card → reordering becomes
"grab and pull", same metaphor as Hero slides.
- AI translation is one toggle at the top, not a per-modal switch.
- Soft-warning empty state with a clear "do this first" hint instead
of a blank page.
NO BREAKING CHANGES
- Old createTimelineEvent / updateTimelineEvent / createHeritageSection
/ updateHeritageSection actions are kept (in case anything still
imports them — nothing in the repo does, but external scripts
might). Internal call sites all use the new patch flow.
- Database schema unchanged.
- Public-facing /heritage page unchanged.
|
||
|
|
7fe5108f66 |
feat: HTTP shared cache for public marketing pages
Deploy to VPS / deploy (push) Has been cancelled
Pages got fast again. Public marketing routes are still rendered
per-request by Next.js (force-dynamic, until the ISR bug gets isolated),
but their HTML is now cached at the Nginx layer for 60s with a 5-minute
stale-while-revalidate window. Result: only the first hit on a URL
inside a 60s window pays the SSR cost; every other visitor in that
window gets a sub-10ms cached response. While a cached entry is
revalidating, peers keep getting the stale copy — no cold starts, no
thundering herds.
NEXT.JS MIDDLEWARE (src/proxy.ts)
- isCacheablePublicPath() identifies routes safe to share-cache:
/, /<locale>, /<locale>/applications, /<locale>/news,
/<locale>/heritage. Excludes /<locale>/parts (auth-gated B2B portal)
and /hq-command/*, /api/*, /_next/*.
- hasAuthCookie() short-circuits caching when the request carries a
flux_session (admin CMS) or flux_b2b_session (client portal) cookie.
Authenticated users always get a fresh per-account render.
- When both checks pass, the response gets:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
NGINX (nginx/nginx.conf)
- New shared zone:
proxy_cache_path /var/cache/nginx/flux levels=1:2
keys_zone=flux_html:50m max_size=1g inactive=24h
use_temp_path=off;
- Access log gets a `cache=$upstream_cache_status` field so we can
audit hit/miss ratios in the live logs.
NGINX (nginx/conf.d/flux.conf — location /)
- proxy_cache flux_html + proxy_cache_revalidate on
- proxy_cache_use_stale: serves stale on backend errors / timeout /
during update, so 502s during a Next.js restart never reach users.
- proxy_cache_background_update + proxy_cache_lock: only one upstream
request fires when a cached entry expires; others keep getting stale.
- proxy_cache_bypass / proxy_no_cache wired to flux_session +
flux_b2b_session cookies — admin and B2B traffic skips the shared
cache entirely.
- X-Cache-Status response header (HIT/MISS/EXPIRED/STALE/UPDATING/BYPASS)
for live debugging — open dev tools, refresh, watch the value flip.
WHAT YOU'LL FEEL
- First visitor on /en within a 60s window: ~150-300ms (SSR + DB).
- Second through Nth visitors in the same window: <10ms.
- Editor publishes a change in HQ Command → revalidatePath() inside
the existing actions invalidates the Next.js cache; the next
marketing-page request rebuilds and primes Nginx fresh. The 60s
TTL bounds how long stale content can linger if revalidation is
ever skipped.
NO BREAKING CHANGES
- Auth flows untouched (cookies bypass cache).
- HQ Command + API endpoints untouched (separate Nginx locations).
- Static assets (cases/, applications/, /branding/, /_next/static)
unaffected — they had their own cache headers already.
- Server-side cache invalidation via revalidatePath() still works.
DEPLOY (David)
cd /opt/flux-srl
git pull
docker compose up -d --build app
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
|
||
|
|
fece168486 |
fix: FluxAI image paths now include the node slug
Deploy to VPS / deploy (push) Has been cancelled
The two AI tool cards rendered inside the chat — CaseStudyViewer (the
"Show me proven installations" card) and EquipmentConfigurator (the
"Show equipment specs" card) — were composing image URLs without the
node-slug segment:
/cases/<filename> ← what the cards were emitting (404)
/cases/<nodeSlug>/<filename> ← what the public site actually serves
Result: cover images and gallery thumbnails inside chat cards came back
as broken-image icons, while the same files rendered fine on
/en/applications/<slug> and inside the CaseStudyModal (those
already used the correct path).
Fix: both components now derive the slug from data.title with the same
nodeToSlug() the public pages use, and prefix it on every /cases/
URL — cover and gallery thumbnails alike.
CHANGES (2 files)
- src/components/ai/CaseStudyViewer.tsx
- Added local nodeToSlug() helper (mirrors ApplicationClient + assetFolders)
- coverSrc: /cases/${nodeSlug}/${mediaFileName}
- gallery image: /cases/${nodeSlug}/${img}
- src/components/ai/EquipmentConfigurator.tsx
- Added local nodeToSlug() helper
- coverSrc: /cases/${nodeSlug}/${mediaFileName}
No backend / API / DB changes. Pure client-side path correction.
|
||
|
|
014a9eb094 |
refactor: unified AssetBucketBrowser replaces 4 inline AssetManagers
Deploy to VPS / deploy (push) Has been cancelled
Every HQ Command panel had its own ~210-line AssetManager component
copy-pasted into the page file. Same UI, same API, four diverging
implementations — and no consistent metaphor for "where does this file
go?". Editors had to think about subfolder names (videos/, renders/)
that the front-end implicitly expects.
ONE COMPONENT. CLEAR BUCKETS. SAME PATHS.
src/components/hq/AssetBucketBrowser.tsx — the single picker. Takes
scope + slug, shows bucket tabs (Media / Videos / Renders / etc.) and
maps each to the on-disk path the public site already reads from:
cases: Media (root) | Videos (/videos) | Renders (/renders)
applications: Media (root) | Videos (/videos) | Renders (/renders)
news: Media (root)
parts: Media (root) | Renders (/renders)
footage: Hero Reel (root)
branding: Brand Assets (root)
Drop a file into the Videos tab → POSTs to /api/assets with path=videos
→ lands at /public/{scope}/{slug}/videos/<file> — exactly where
ApplicationClient.tsx and CaseStudyModal.tsx already look. Zero
front-end path changes, zero data migration.
UX upgrades the editor sees:
- Tabs make the bucket layout discoverable instead of buried in folder
navigation. Each tab has its own description and accent colour.
- Soft-warning hints flag obvious mismatches ("Videos bucket usually
holds .mp4 — this file may not display correctly") without blocking
the upload.
- Search + grid/list views.
- Hover actions per file: copy URL, delete (with confirm).
- Persistent on-screen path (/{scope}/{slug}/{bucket}) so editors can
always see the canonical location.
REPLACEMENT (4 page files)
- network/page.tsx: 719 → 513 lines (-206) — direct alias
- news/page.tsx: 484 → 313 lines (-171) — direct alias
- applications/page.tsx: 555 → 433 lines (-122) — adapter wraps the
picker's onSelect into the markdown-syntax onInsert callback this
panel uses. No call-site changes.
- parts/page.tsx: 413 → 320 lines (-93) — direct alias
Net: -592 lines of duplicated UI, +560 lines of single shared component.
Future bucket-layout changes live in one file instead of four.
NO PATH/API CHANGES — /api/assets is unchanged. The on-disk layout is
unchanged. Existing assets keep rendering on the public site. Existing
DB rows (mediaFileName, galleryJson, videosJson, rendersJson) are
unaffected because we never moved files.
|
||
|
|
9b28f8ffaf |
fix: nextjs primary group + auto-create asset folders on entity create
Deploy to VPS / deploy (push) Has been cancelled
THREE INTERLOCKING FIXES so editors stop hitting permission walls.
1) DOCKERFILE — gid 65533 (nogroup) on uploaded files
The container was creating files as 1001:65533 because Alpine's
`adduser --system --uid 1001 nextjs` doesn't set a primary group.
Files written through /api/assets ended up with `nogroup` ownership,
which surprised host sysadmins and made `chown -R 1001:1001` revert
on each fresh container start.
Fix: `adduser --system --uid 1001 --ingroup nodejs nextjs`. Now
every file written by the container is 1001:1001 (nextjs:nodejs),
matching the host conventions and the existing chown automation.
2) ENTRYPOINT — recursively normalise existing files
The recursive chown in scripts/docker-entrypoint.sh now sweeps every
subfolder of /app/public/branding|footage|applications|cases|news|
parts|operations-inbox|heritage on each container start, fixing any
files that previously slipped through with the wrong group. Single
fast pass, idempotent. Adds /app/public/heritage to the list (was
missing).
3) AUTO-CREATE ASSET BUCKETS on entity create
The big editor UX win: when an admin creates a Case (GlobalNode), an
Application or a News article in HQ Command, the server now also
mkdir's the well-known asset subfolders for that entity. So after
creating "Acme Industries" as a case, the editor immediately gets
/public/cases/acme-industries/{videos,renders,gallery,datasheet,models}
ready — no more "EACCES because the dir wasn't created" gotcha
when they upload their first video.
Implementation:
- src/lib/assetFolders.ts: typed helper with per-scope bucket lists
+ a titleToSlug helper that mirrors the front-end's slugger so the
folder name matches what ApplicationClient expects when rendering
/cases/<slug>/videos/<file>.
- network/actions.ts: createNode -> ensureAssetFolders("cases", slug).
Plus a new server action ensureNodeAssetFolders(id) so the editor
can fix existing nodes without recreating them (one-click "Repair").
- news/actions.ts: createNewsArticle -> ensureAssetFolders("news",slug)
- applications/actions.ts: createApplication -> ensureAssetFolders(...)
DEPLOY (David)
cd /opt/flux-srl
git pull
docker compose up -d --build app
# The entrypoint will fix existing 1001:65533 files automatically
# as the container boots — no manual chown needed.
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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
|
||
|
|
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.
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
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. |