# syntax=docker/dockerfile:1
#
# Image Docker pour le frontend DigitalKin Hub (React + Vite SPA).
# Bundle Magicbox — dérivé de l'image production Vercel, taillé pour un
# environnement de dev : le build doit être **court** et l'image **petite**.
#
# Stratégie : deux stages seulement
#   1. build   : pnpm install → vite build → dist/
#   2. runtime : nginx non-privilégié (variante slim) qui sert /dist en statique
#
# Mesuré sur le clone Horizon (colima 6 CPU / 9 Go), contre le Dockerfile
# précédent (deps → build → runtime, cache mounts, `pnpm run build`) :
#   · build à froid        : 11 min → ~1 min 40
#   · bump de package.json : ~4 min → ~1 min (install + build)
#   · commit sur src/      : ~4 min → ~30 s (le seul `vite build` re-tourne)
#   · image finale         : 39 Mo → 15 Mo
#
# Ce qui fait la différence :
#   · `Dockerfile.dockerignore` (à côté de ce fichier) réduit le contexte au
#     strict nécessaire — pas de .git/node_modules/dist/docs du clone ;
#   · **un seul stage Node** : l'ancien `COPY --from=deps node_modules` copiait
#     ~1 700 paquets (100 k fichiers) d'un stage à l'autre — 150 s à lui seul ;
#   · **une seule couche `pnpm install`**, store et `node_modules` écrits dans la
#     même couche → pnpm hardlinke (46 s à froid, téléchargement compris).
#     Les deux alternatives « classiques » sont plus lentes ici, mesures à
#     l'appui : un store en cache mount BuildKit vit sur un autre système de
#     fichiers, pnpm COPIE alors fichier par fichier (+90 s) — et la politique
#     GC par défaut du builder Docker plafonne l'ensemble des cache mounts à
#     quelques centaines de Mo (`docker buildx inspect` → règle
#     `type==exec.cachemount`), donc sur un poste qui build aussi les archétypes
#     (cache uv ~630 Mo) le store est évincé après chaque build ; et séparer
#     `pnpm fetch` (lockfile) de l'install (package.json) en deux couches fait
#     passer l'install par le copy-up overlayfs de chaque fichier (150 s), plus
#     cher que le téléchargement qu'on voulait épargner ;
#   · pas de type-check `tsc -b` par défaut (`HORIZON_TYPECHECK=1` pour le
#     rétablir, ~30 s) : Vite/esbuild ne s'en sert pas pour produire le bundle,
#     et c'est la CI d'Horizon qui garantit le typage — ici on veut l'app qui
#     tourne. Ni sitemap (`scripts/generate-sitemap.mjs` pointe sur
#     hub.digitalkin.com — sans objet en local) ;
#   · pas de sourcemaps quand aucun `SENTRY_AUTH_TOKEN` n'est fourni (−17 s de
#     build, −36 Mo dans l'image) : Vite les génère en `hidden` — jamais
#     référencées par le JS, donc invisibles au navigateur — uniquement pour que
#     le plugin Sentry les téléverse ;
#   · les ARG VITE_* sont déclarés APRÈS les COPY → changer une valeur ne
#     recopie ni les dépendances ni le code, seul `vite build` re-tourne ;
#   · runtime `nginx-unprivileged:alpine-slim`, sans `apk add` (healthcheck via
#     le `wget` de busybox, nginx sait être PID 1) → aucun accès réseau ni
#     changement d'utilisateur au stage runtime.
#
# Les variables VITE_* sont inlinées dans le bundle au build time. Magicbox les
# passe via `build.args` (cf. `ComposeBuilder.build_horizon`) depuis l'env
# résolu de `<profile>/env/horizon.yml`. Pour des variables runtime (changer
# sans rebuild) il faudrait une autre approche (entrypoint qui injecte
# /env-config.js) — pas le cas ici, on garde le modèle Vercel.

# ─────────────────────────────────────────────────────────────
# Stage 1 — build : dépendances (pnpm) + vite build
# ─────────────────────────────────────────────────────────────
FROM node:24-alpine AS build

# PNPM_HOME fixe aussi l'emplacement du store (`$PNPM_HOME/store`) — dans la
# couche d'image, à côté de node_modules (cf. en-tête : pas de cache mount, à
# dessein).
ENV PNPM_HOME=/pnpm \
    PATH="/pnpm:$PATH" \
    # Skip husky : .git n'est pas dans le contexte, et le hook `prepare` le lancerait
    HUSKY=0 \
    CI=true \
    # pnpm 10+ peut refuser les builds ignorés (postinstall non approuvés via
    # onlyBuiltDependencies). Les scripts critiques (esbuild, oxide, sentry-cli)
    # sont approuvés dans package.json ; le reste devient un simple avertissement.
    npm_config_strict_dep_builds=false \
    # Plus de heap pour Vite/rollup sur les gros bundles
    NODE_OPTIONS=--max-old-space-size=4096

# pnpm 10 (cohérent avec `pnpm.onlyBuiltDependencies` du package.json — pnpm 11
# ne lit plus ce format). Installé via npm plutôt que corepack : corepack n'est
# plus livré avec Node ≥ 25.
RUN npm install -g --no-fund --no-audit pnpm@10

WORKDIR /app

# ── Dépendances ──
# Une seule couche, invalidée par package.json / lockfile / .npmrc (cf.
# en-tête : la couper en fetch + install coûte plus qu'elle ne rapporte).
# .npmrc est crucial : `public-hoist-pattern[]=rxjs` (et autres) hoisse au
# top-level les deps transitives importées en direct par le code.
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile

# ── Code applicatif ──
# `Dockerfile.dockerignore` exclut node_modules/.git/dist/docs/outillage… :
# contexte minimal, couche invalidée uniquement par un vrai changement de
# source (pas par le churn de `.git`, un README ou une config eslint).
COPY . .

# ── Variables VITE_* (inlinées dans le bundle au build) ──
# Déclarées ici, juste avant le build et APRÈS les COPY, pour préserver le cache
# des couches ci-dessus (BuildKit n'invalide qu'à la PREMIÈRE UTILISATION d'un
# ARG, pas à sa déclaration). Toutes optionnelles : si non passées, Vite mettra
# `undefined` côté code.
ARG VITE_API_URL
ARG VITE_CLERK_PUBLISHABLE_KEY
ARG VITE_POSTHOG_HOST
ARG VITE_POSTHOG_KEY
ARG VITE_SENTRY_DSN
ARG VITE_INCIDENT_IO_WIDGET_URL
# `vite.config.ts` définit `import.meta.env.VITE_VERCEL_ENV` depuis
# `process.env.VERCEL_ENV` (un `define`, qui prime sur le `.env`) → c'est
# VERCEL_ENV qu'il faut poser, sinon le bundle embarque la chaîne "undefined".
# `development` par défaut : on est dans un environnement local, pas en prod
# (GTM ne s'initialise qu'en `production`, Sentry/PostHog taguent l'env).
ARG VITE_VERCEL_ENV=development
ARG VITE_REACT_QUERY_DEVTOOLS=false
ARG VITE_SCAN=false
ARG VITE_LOG_I=false
# ── Upload sourcemaps Sentry (optionnel) ──
ARG SENTRY_ORG
ARG SENTRY_PROJECT
ARG SENTRY_AUTH_TOKEN
# ── Type-check `tsc -b` avant le bundle (désactivé par défaut, cf. en-tête) ──
ARG HORIZON_TYPECHECK=0

ENV VITE_API_URL=$VITE_API_URL \
    VITE_CLERK_PUBLISHABLE_KEY=$VITE_CLERK_PUBLISHABLE_KEY \
    VITE_POSTHOG_HOST=$VITE_POSTHOG_HOST \
    VITE_POSTHOG_KEY=$VITE_POSTHOG_KEY \
    VITE_SENTRY_DSN=$VITE_SENTRY_DSN \
    VITE_INCIDENT_IO_WIDGET_URL=$VITE_INCIDENT_IO_WIDGET_URL \
    VITE_VERCEL_ENV=$VITE_VERCEL_ENV \
    VERCEL_ENV=$VITE_VERCEL_ENV \
    VITE_REACT_QUERY_DEVTOOLS=$VITE_REACT_QUERY_DEVTOOLS \
    VITE_SCAN=$VITE_SCAN \
    VITE_LOG_I=$VITE_LOG_I \
    SENTRY_ORG=$SENTRY_ORG \
    SENTRY_PROJECT=$SENTRY_PROJECT \
    SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN

# Le script `build` d'Horizon = `tsc -b && vite build && node scripts/generate-sitemap.mjs`.
# On n'en garde que `vite build` (cf. en-tête) ; `tsc -b` sur demande, ses
# .tsbuildinfo (node_modules/.tmp, cf. tsconfig.*.json) persistent dans un cache
# mount → seuls les fichiers modifiés sont re-checkés (petit, donc épargné par
# la GC des cache mounts). Sans token Sentry, pas de sourcemaps du tout
# (`--sourcemap false` prime sur `build.sourcemap: 'hidden'` du vite.config.ts) ;
# avec, le plugin Sentry les téléverse puis les supprime lui-même
# (`filesToDeleteAfterUpload`) — le `find` final est le filet de sécurité.
RUN --mount=type=cache,id=horizon-tsbuildinfo,target=/app/node_modules/.tmp \
    set -eu; \
    if [ "$HORIZON_TYPECHECK" = "1" ]; then pnpm exec tsc -b; fi; \
    if [ -n "$SENTRY_AUTH_TOKEN" ]; then \
        pnpm exec vite build; \
    else \
        pnpm exec vite build --sourcemap false; \
    fi; \
    find dist -type f -name '*.map' -delete

# ─────────────────────────────────────────────────────────────
# Stage 2 — runtime : nginx non-privilégié qui sert le statique
# ─────────────────────────────────────────────────────────────
# nginx-unprivileged tourne déjà en uid 101 (nginx) sur le port 8080, sans
# bidouille de permissions/PID — le bon défaut pour Kubernetes. Variante
# `alpine-slim` : même nginx (ssl, gzip, realip…), sans les modules dynamiques
# (njs, xslt, geoip, image-filter) dont la conf ne se sert pas — 21 paquets apk
# au lieu de 69. Rien n'est installé par-dessus : nginx est un PID 1 correct
# (il relaie les signaux à ses workers et les moissonne) et le healthcheck
# passe par le `wget` de busybox.
FROM nginxinc/nginx-unprivileged:1.29-alpine-slim AS runtime

# Conf nginx custom Magicbox (reproduit vercel.json : SPA fallback, sécurité,
# proxy PostHog). Le fichier est apporté par le contexte nommé `magicbox-bundle`
# (cf. `additional_contexts` du compose override généré par `dk prepare`) — il
# vit à `services/horizon/Dockerfile/nginx.conf` côté Magicbox et reste
# indépendant du clone Horizon (qui peut, ou non, en avoir un à la racine).
# `--link` : couches indépendantes de l'image de base → un bump de nginx ne
# refait pas la copie, et inversement.
COPY --link --from=magicbox-bundle nginx.conf /etc/nginx/conf.d/default.conf

# Bundle statique (lecture seule : root:root 644 suffit à l'uid 101)
COPY --link --from=build /app/dist /usr/share/nginx/html

EXPOSE 8080

# Healthcheck Docker (Kubernetes utilisera plutôt liveness/readiness probes sur /healthz)
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
  CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1

CMD ["nginx", "-g", "daemon off;"]
