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>
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.
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.
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.
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.