# La Gaufre integration service — O Estúdio app launcher. # Serves a custom gaufre.js widget and services.json for our app list. # Apps set GAUFREJS_URL=https://integration.DOMAIN_SUFFIX/api/v1/gaufre.js # which loads this widget; clicking it fetches /api/v1/services.json. --- apiVersion: v1 kind: ConfigMap metadata: name: integration-config namespace: lasuite data: services.json: | [ { "id": "documentos", "name": "Documentos", "url": "https://docs.DOMAIN_SUFFIX" }, { "id": "ficheiros", "name": "Ficheiros", "url": "https://drive.DOMAIN_SUFFIX" }, { "id": "reunioes", "name": "Reuniões", "url": "https://meet.DOMAIN_SUFFIX" }, { "id": "conversas", "name": "Conversas", "url": "https://chat.DOMAIN_SUFFIX" }, { "id": "correio", "name": "Correio", "url": "https://mail.DOMAIN_SUFFIX" }, { "id": "pessoas", "name": "Pessoas", "url": "https://people.DOMAIN_SUFFIX" }, { "id": "descobrir", "name": "Descobrir", "url": "https://find.DOMAIN_SUFFIX" }, { "id": "administrar", "name": "Administrar", "url": "https://admin.DOMAIN_SUFFIX" } ] gaufre.js: | /** * O Estúdio — La Gaufre widget (self-hosted). * Drop-in replacement for the official integration.lasuite.numerique.gouv.fr widget. * Fetches services.json from its own origin and renders a DSFR-style popup. */ (function () { 'use strict'; const origin = (function () { const s = document.currentScript; if (s && s.src) { const u = new URL(s.src); return u.origin; } return window.location.origin; })(); const SERVICES_URL = origin + '/api/v1/services.json'; const SUITE_NAME = 'O Estúdio'; const STYLE = ` #estudio-gaufre-overlay { display: none; position: fixed; inset: 0; z-index: 9999; } #estudio-gaufre-overlay.open { display: block; } #estudio-gaufre-backdrop { position: absolute; inset: 0; } #estudio-gaufre-panel { position: absolute; top: 56px; left: 16px; background: #fff; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,.15); padding: 16px; width: 320px; z-index: 1; } #estudio-gaufre-panel h2 { font-size: 14px; font-weight: 700; color: #666; margin: 0 0 12px; text-transform: uppercase; letter-spacing: .05em; } #estudio-gaufre-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .estudio-gaufre-app { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 8px; border-radius: 6px; text-decoration: none; color: #333; font-size: 12px; font-weight: 500; transition: background .15s; } .estudio-gaufre-app:hover { background: #f5f5f5; } .estudio-gaufre-icon { width: 40px; height: 40px; border-radius: 8px; background: #e8eaf6; display: flex; align-items: center; justify-content: center; font-size: 18px; } `; const ICONS = { documentos: '📄', ficheiros: '📁', reunioes: '🎥', conversas: '💬', correio: '✉️', pessoas: '👥', descobrir: '🔍', administrar: '⚙️', }; function injectStyles() { const el = document.createElement('style'); el.textContent = STYLE; document.head.appendChild(el); } function buildPanel(services) { const overlay = document.createElement('div'); overlay.id = 'estudio-gaufre-overlay'; const backdrop = document.createElement('div'); backdrop.id = 'estudio-gaufre-backdrop'; backdrop.addEventListener('click', close); const panel = document.createElement('div'); panel.id = 'estudio-gaufre-panel'; const title = document.createElement('h2'); title.textContent = SUITE_NAME; panel.appendChild(title); const grid = document.createElement('div'); grid.id = 'estudio-gaufre-grid'; services.forEach(function (svc) { const a = document.createElement('a'); a.className = 'estudio-gaufre-app'; a.href = svc.url; const icon = document.createElement('div'); icon.className = 'estudio-gaufre-icon'; icon.textContent = ICONS[svc.id] || '🔲'; const label = document.createElement('span'); label.textContent = svc.name; a.appendChild(icon); a.appendChild(label); grid.appendChild(a); }); panel.appendChild(grid); overlay.appendChild(backdrop); overlay.appendChild(panel); document.body.appendChild(overlay); return overlay; } let overlay = null; let services = null; function open() { if (!services) { fetch(SERVICES_URL) .then(function (r) { return r.json(); }) .then(function (data) { services = data; overlay = buildPanel(services); overlay.classList.add('open'); }); } else { if (!overlay) overlay = buildPanel(services); overlay.classList.add('open'); } } function close() { if (overlay) overlay.classList.remove('open'); } function init() { injectStyles(); document.querySelectorAll('.js-lasuite-gaufre-btn').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); overlay && overlay.classList.contains('open') ? close() : open(); }); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); nginx.conf: | server { listen 80; server_name _; location = /api/v1/services.json { alias /etc/integration/services.json; add_header Content-Type "application/json; charset=utf-8"; add_header Access-Control-Allow-Origin "*"; } location = /api/v1/gaufre.js { alias /etc/integration/gaufre.js; add_header Content-Type "application/javascript; charset=utf-8"; add_header Access-Control-Allow-Origin "*"; } location / { return 404; } } --- apiVersion: apps/v1 kind: Deployment metadata: name: integration namespace: lasuite spec: replicas: 1 selector: matchLabels: app: integration template: metadata: labels: app: integration spec: containers: - name: integration image: nginx:alpine ports: - name: http containerPort: 80 volumeMounts: - name: config mountPath: /etc/integration - name: nginx-conf mountPath: /etc/nginx/conf.d/default.conf subPath: nginx.conf resources: limits: memory: 32Mi requests: memory: 16Mi cpu: 5m volumes: - name: config configMap: name: integration-config items: - key: services.json path: services.json - key: gaufre.js path: gaufre.js - name: nginx-conf configMap: name: integration-config items: - key: nginx.conf path: nginx.conf --- apiVersion: v1 kind: Service metadata: name: integration namespace: lasuite spec: selector: app: integration ports: - name: http port: 80 targetPort: 80