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
This commit is contained in:
2026-05-04 15:24:06 -05:00
parent 01a84edee9
commit f8606a45ff
5 changed files with 51 additions and 1 deletions
+2
View File
@@ -65,6 +65,7 @@ services:
- ./public/news:/app/public/news
- ./public/parts:/app/public/parts
- ./public/operations-inbox:/app/public/operations-inbox
- ./public/branding:/app/public/branding
networks:
- flux-net
expose:
@@ -88,6 +89,7 @@ services:
- ./public/parts:/srv/parts:ro
- ./public/footage:/srv/footage:ro
- ./public/operations-inbox:/srv/operations-inbox:ro
- ./public/branding:/srv/branding:ro
depends_on:
- app
networks:
+6
View File
@@ -163,6 +163,12 @@ server {
access_log off;
}
location /branding/ {
alias /srv/branding/;
add_header Cache-Control "public, max-age=300, must-revalidate" always;
access_log off;
}
location / {
proxy_pass http://nextjs;
proxy_set_header Host $host;
@@ -275,6 +275,21 @@ function FooterTab({
<TextField label="Country" value={value.hqCountry} onChange={(v) => onChange({ ...value, hqCountry: v })} />
</FieldGroup>
<FieldGroup title="Headquarters contact">
<TextField
label="Sales email"
value={value.hqEmail}
onChange={(v) => onChange({ ...value, hqEmail: v })}
placeholder="sales@lethepowerflux.com"
/>
<TextField
label="Phone (international format)"
value={value.hqPhone}
onChange={(v) => onChange({ ...value, hqPhone: v })}
placeholder="+39 0424 287 492"
/>
</FieldGroup>
<FieldGroup title="Legal">
<TextField label="Copyright holder" value={value.copyrightHolder} onChange={(v) => onChange({ ...value, copyrightHolder: v })} />
<TextField label="Privacy policy URL" value={value.privacyUrl} onChange={(v) => onChange({ ...value, privacyUrl: v })} />
+24 -1
View File
@@ -1,5 +1,5 @@
import { Link } from "@/i18n/routing";
import { Linkedin, Instagram, Youtube, Mail } from "lucide-react";
import { Linkedin, Instagram, Youtube, Mail, Phone } from "lucide-react";
import { prisma } from "@/lib/prisma";
import { getLocalizedData } from "@/lib/i18nHelper";
import { getTranslations, getLocale } from "next-intl/server";
@@ -97,6 +97,29 @@ export default async function Footer() {
{footer.hqRegion}, {footer.hqCountry}
</p>
{(footer.hqEmail || footer.hqPhone) && (
<div className="flex flex-col gap-2 text-[#86868B] font-light">
{footer.hqEmail && (
<a
href={`mailto:${footer.hqEmail}`}
className="inline-flex items-center gap-2 hover:text-[#0066CC] dark:hover:text-[#00F0FF] transition-colors group"
>
<Mail size={14} className="text-[#86868B] group-hover:text-[#0066CC] dark:group-hover:text-[#00F0FF]" />
<span className="break-all">{footer.hqEmail}</span>
</a>
)}
{footer.hqPhone && (
<a
href={`tel:${footer.hqPhone.replace(/\s+/g, "")}`}
className="inline-flex items-center gap-2 hover:text-[#0066CC] dark:hover:text-[#00F0FF] transition-colors group"
>
<Phone size={14} className="text-[#86868B] group-hover:text-[#0066CC] dark:group-hover:text-[#00F0FF]" />
<span>{footer.hqPhone}</span>
</a>
)}
</div>
)}
{socialLinks.length > 0 && (
<div className="flex items-center gap-3 mt-2">
{socialLinks.map(({ url, icon: Icon, label }) => (
+4
View File
@@ -21,6 +21,8 @@ export interface FooterSettings {
hqCity: string;
hqRegion: string;
hqCountry: string;
hqEmail: string; // e.g. sales@lethepowerflux.com
hqPhone: string; // e.g. +39 0424 287 492
copyrightHolder: string;
privacyUrl: string;
termsUrl: string;
@@ -51,6 +53,8 @@ export const DEFAULT_FOOTER: FooterSettings = {
hqCity: "36060 Romano d'Ezzelino",
hqRegion: "Vicenza",
hqCountry: "Italy",
hqEmail: "sales@lethepowerflux.com",
hqPhone: "+39 0424 287 492",
copyrightHolder: "FLUX Srl",
privacyUrl: "/privacy",
termsUrl: "/terms",