From 14a01dd5e7436122a8e70b3bc51910e643b6c188 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Tue, 3 Mar 2026 16:09:21 +0000 Subject: [PATCH] feat: initial La Gaufre v2 integration service Multi-stage Docker image that: - Builds lagaufre.js v2 widget from suitenumerique/integration source (context must be sunbeam/ root; see sunbeam build integration) - Serves the widget, official La Suite SVG logos, custom logos for drive/mail/people, and a v1-compat gaufre.js wrapper via nginx gaufre.js reveals the ui-kit GaufreButton (adds lasuite--gaufre-loaded to ), loads the v2 widget, and wires button clicks via event delegation to survive React hydration replacing the initial DOM element. services.json is the only runtime-variable file; it is mounted from the integration-config ConfigMap which contains the deployed service list with DOMAIN_SUFFIX substituted at apply time. --- .dockerignore | 16 ++++++++++++++ Dockerfile | 27 ++++++++++++++++++++++++ gaufre.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ logos/drive.svg | 6 ++++++ logos/mail.svg | 6 ++++++ logos/people.svg | 10 +++++++++ nginx.conf | 39 ++++++++++++++++++++++++++++++++++ 7 files changed, 158 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 gaufre.js create mode 100644 logos/drive.svg create mode 100644 logos/mail.svg create mode 100644 logos/people.svg create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e32dcf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# Allowlist: only send the files needed for the integration-service image build. +* + +# Parent directories must be explicitly included for allowlist patterns to work. +!integration/ +!integration/packages/ +!integration/packages/widgets/ +!integration/packages/widgets/** +!integration/packages/integration/ +!integration/packages/integration/public/ +!integration/packages/integration/public/logos/ +!integration/packages/integration/public/logos/*.svg + +# Our service-specific files +!integration-service/ +!integration-service/** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a2fc65d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# ── Stage 1: build lagaufre.js from suitenumerique/integration source ────────── +# Build context must be the sunbeam/ root so we can reach both +# integration/packages/widgets/ and integration-service/. +FROM node:22-alpine AS widget-build +WORKDIR /src +COPY integration/packages/widgets/ . +RUN npm ci && npm run build + +# ── Stage 2: nginx serving all static assets ──────────────────────────────────── +FROM nginx:alpine + +# Official La Suite service logos from the integration package source tree. +COPY integration/packages/integration/public/logos/ /usr/share/nginx/html/logos/ + +# Custom logos for O Estúdio services not in the official La Suite logo set +# (drive, mail, people — same two-tone #000091/#e1000f style as upstream). +COPY integration-service/logos/ /usr/share/nginx/html/logos/ + +# Built lagaufre.js v2 widget (11 kB, Shadow DOM, ARIA-compliant popup). +COPY --from=widget-build /src/dist/lagaufre.js /usr/share/nginx/html/widget/lagaufre.js + +# v1-compat gaufre.js — thin wrapper loaded by people-frontend via sub_filter. +# Derives its origin from the script URL at runtime; no DOMAIN_SUFFIX baked in. +COPY integration-service/gaufre.js /usr/share/nginx/html/gaufre.js + +# Nginx config — only services.json is mounted at runtime (from ConfigMap). +COPY integration-service/nginx.conf /etc/nginx/conf.d/default.conf diff --git a/gaufre.js b/gaufre.js new file mode 100644 index 0000000..700d1a2 --- /dev/null +++ b/gaufre.js @@ -0,0 +1,54 @@ +/** + * O Estúdio — La Gaufre v1 compatibility wrapper. + * + * Loaded by people-frontend via the URL rewritten by nginx sub_filter. + * Responsibilities: + * 1. Add "lasuite--gaufre-loaded" to so the GaufreButton becomes visible. + * 2. Dynamically load the lagaufre.js v2 widget from the same origin. + * 3. Wire button clicks via event delegation — survives React hydration + * replacing the initial SSR'd DOM element. + * + * No DOMAIN_SUFFIX baked in — the integration origin is derived from this + * script's own src URL at runtime, so the same image works in every environment. + */ +(function () { + 'use strict'; + + // Reveal the GaufreButton immediately (synchronously, before anything else). + // @gouvfr-lasuite/ui-kit hides .lasuite-gaufre-btn until this class is present. + document.documentElement.classList.add('lasuite--gaufre-loaded'); + + // Derive the integration service origin from this script's URL. + var origin = (function () { + var s = document.querySelector('#lasuite-gaufre-script'); + return (s && s.src) ? new URL(s.src).origin : window.location.origin; + })(); + + var widgetReady = false; + + // Load the lagaufre v2 widget. We do NOT pass buttonElement to avoid + // holding a stale reference after React hydration replaces DOM nodes. + // Button clicks are handled via event delegation below instead. + var script = document.createElement('script'); + script.src = origin + '/api/v2/lagaufre.js'; + script.onload = function () { + window._lasuite_widget = window._lasuite_widget || []; + window._lasuite_widget.push(['lagaufre', 'init', { + api: origin + '/api/v2/services.json', + label: 'O Estúdio', + closeLabel: 'Fechar', + newWindowLabelSuffix: ' · nova janela', + }]); + widgetReady = true; + }; + document.head.appendChild(script); + + // Event delegation — listens on document (bubbling) so it works regardless + // of which element React hydration puts in the DOM after page load. + document.addEventListener('click', function (e) { + if (!widgetReady) return; + if (!e.target.closest('.js-lasuite-gaufre-btn')) return; + window._lasuite_widget = window._lasuite_widget || []; + window._lasuite_widget.push(['lagaufre', 'toggle']); + }); +})(); diff --git a/logos/drive.svg b/logos/drive.svg new file mode 100644 index 0000000..af3fbdf --- /dev/null +++ b/logos/drive.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/logos/mail.svg b/logos/mail.svg new file mode 100644 index 0000000..b109821 --- /dev/null +++ b/logos/mail.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/logos/people.svg b/logos/people.svg new file mode 100644 index 0000000..3fc0662 --- /dev/null +++ b/logos/people.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d815494 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,39 @@ +server { + listen 80; + server_name _; + + # v2 widget — served from image, short cache OK (changes only on redeploy) + location = /api/v2/lagaufre.js { + alias /usr/share/nginx/html/widget/lagaufre.js; + add_header Content-Type "application/javascript; charset=utf-8"; + add_header Access-Control-Allow-Origin "*"; + add_header Cache-Control "public, max-age=3600"; + } + + # v2 services — mounted from ConfigMap (contains expanded DOMAIN_SUFFIX URLs) + location = /api/v2/services.json { + alias /etc/integration/services.json; + add_header Content-Type "application/json; charset=utf-8"; + add_header Access-Control-Allow-Origin "*"; + add_header Cache-Control "no-cache"; + } + + # SVG logos — served from image, long cache (change only on redeploy) + location ~ ^/logos/[a-zA-Z0-9_-]+\.svg$ { + root /usr/share/nginx/html; + add_header Content-Type "image/svg+xml"; + add_header Access-Control-Allow-Origin "*"; + add_header Cache-Control "public, max-age=86400"; + } + + # v1 gaufre.js — thin wrapper served from image (no DOMAIN_SUFFIX needed) + location = /api/v1/gaufre.js { + alias /usr/share/nginx/html/gaufre.js; + add_header Content-Type "application/javascript; charset=utf-8"; + add_header Access-Control-Allow-Origin "*"; + } + + location / { + return 404; + } +}