From 8568b48461ecc23322fbcd014facd5f18164d978 Mon Sep 17 00:00:00 2001 From: Kevin Lecou Date: Fri, 18 Sep 2026 12:16:25 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20la=20cr=C3=A9ation=20d'=C3=A9v=C3=A9ne?= =?UTF-8?q?ment=20via=20n8n=20et=20refond=20la=20DA=20en=20cartes=20papier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouvelle Edge Function create-event + RPC public.create_event : génère slug/admin_token/expire_at et insère l'event, réservée à service_role, appelée par le workflow n8n "Créer un événement" (Form Trigger, publié). - Passe DA "polaroid poussé à fond" : les cartes d'état (chargement/erreur/ succès), la modale de bienvenue et le formulaire passent en papier clair avec ruban washi tape et léger tilt, plutôt qu'en carte sombre sur fond sombre (ombre invisible dans l'ancienne version). Admin resté sobre/sombre mais cartes plus lisibles (liseré coloré, contraste renforcé). - Générateur QR et carton imprimable : lisent maintenant ?slug= et ?baseUrl= pour se pré-remplir depuis le lien renvoyé par create-event. - Correctifs : icône manquante sur "Télécharger" (admin), bug [hidden] anticipé sur l'état vide de la galerie invité. --- docs/decisions.md | 125 ++++++++++ src/admin/gallery/gallery.js | 5 +- src/admin/gallery/style.css | 80 +++++-- src/admin/print-template/index.html | 28 ++- src/admin/qrcode/index.html | 138 +++++++++-- src/e/style.css | 222 ++++++++++++++---- src/galerie/index.html | 9 +- src/galerie/style.css | 132 ++++++++++- supabase/functions/create-event/index.ts | 195 +++++++++++++++ .../20260918100000_create_event_rpc.sql | 154 ++++++++++++ ...arden_generate_unique_slug_search_path.sql | 42 ++++ 11 files changed, 1038 insertions(+), 92 deletions(-) create mode 100644 supabase/functions/create-event/index.ts create mode 100644 supabase/migrations/20260918100000_create_event_rpc.sql create mode 100644 supabase/migrations/20260918101500_harden_generate_unique_slug_search_path.sql diff --git a/docs/decisions.md b/docs/decisions.md index a0b0d0f..a66aabe 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -5,6 +5,131 @@ la date à laquelle elles ont été prises. Toute nouvelle décision structurant (schéma DB, auth, hébergement, etc.) doit être ajoutée ici après validation par Kévin. +## 2026-09-18 (six) + +- **Limitation connue (pas un bug à corriger)** : sur /e/OS + navigateur + DuckDuckGo, le bouton "Prendre une photo" (`capture="environment"`) + ouvre la galerie au lieu de l'appareil photo, et l'inverse pour "Depuis + la galerie". Vérifié : le HTML/JS est conforme à la spec (wiring + correct), c'est DuckDuckGo Android qui n'honore pas fiablement + l'attribut `capture`. Décision : ne rien coder de spécifique pour ce + navigateur (combo /e/OS + DuckDuckGo trop marginal côté invités réels, + qui arrivent très majoritairement via Safari/Chrome par défaut) — à + revérifier par Kévin sur iPhone/Android dès qu'il en a sous la main. +- **Événement de test créé en conditions réelles** via le workflow n8n + + `create-event` (slug `test-da-verification-kevin`) : premier passage + bout-en-bout du pipeline complet (n8n → Edge Function → Telegram), + fonctionnel. Confirme aussi que le déploiement de la passe DA + "polaroid poussé à fond" (voir entrée précédente) rend bien en + conditions réelles sur le domaine de prod, pas seulement en local — + modale de bienvenue et état vide de la galerie vérifiés par capture + d'écran sur `https://photobooth.assistantaikev.eu`. +- **Déployé sur le VPS** (scp direct des 7 fichiers modifiés vers + `/home/kevin/photobooth-qr/site/`, pas de rebuild nécessaire — site + statique servi par nginx). + +## 2026-09-18 (cinq) + +- **2e itération de la 4e passe DA, plus radicale** : retour Kévin sur la + version "rim light + grain" ("franchement c'est nul", captures à l'appui + après avoir vérifié moi-même dans Chrome) — l'effet était bien trop + subtil, l'app restait un grand vide noir avec une petite carte sombre + illisible. Changement structurel plutôt qu'un réglage plus fort du même + hack : les cartes "props" côté invité (bloc de chargement/erreur/succès, + modale de bienvenue, formulaire prénom+message) passent en **papier + clair** (`#FAF6F0`, encre `#12100E`) posé sur la table sombre, avec un + ruban "washi tape" en haut de carte et une légère inclinaison + (-2.2°/+1.8° selon l'état) — comme un vrai polaroid, plutôt qu'un panneau + UI sombre. Une carte claire sur fond quasi noir projette une vraie ombre + (`--shadow-paper`), contrairement à une carte sombre sur fond sombre : + ça résout le problème à la racine plutôt qu'en le maquillant. +- Bug technique corrigé au passage : l'inclinaison ne s'affichait pas du + tout au premier essai — une `animation` qui touche `transform` + (`fade-rise`) remplace toute la propriété, elle ne fusionne pas avec un + `rotate()` posé à côté en statique. Fix : rotation intégrée dans les + keyframes (`state-card-in`), avec repli statique sous + `prefers-reduced-motion: reduce`. +- **Écran admin** (galerie + générateur QR) : reste sombre/sobre, sans + tilt ni tape (exception du skill DA conservée), mais `--color-card` + légèrement éclairci (`#1E1B18` → `#241F1A`) et liseré coloré plein en + haut de carte (plus un fond teinté translucide) pour rester repérable — + même diagnostic que côté invité (contraste carte/fond trop faible pour + se voir), remède différent (sobre, pas de papier). Le générateur QR + (`src/admin/qrcode/`) n'avait quasiment aucune mise en forme avant cette + passe (champs nus sur le fond, aucun regroupement visuel, police + "General Sans" référencée mais jamais chargée dans ce fichier) : plus + gros écart identifié lors de l'audit, corrigé (carte de formulaire, + police chargée, contraste champs/carte). +- **Vérifié visuellement cette fois** : captures d'écran via Chrome headless + local (`google-chrome --headless=new --screenshot=...`) sur les 5 écrans + + la modale de bienvenue forcée visible temporairement pour vérification + — pas juste une relecture de CSS. Contrastes WCAG AA recalculés pour les + nouvelles combinaisons papier (encre/muted sur `#FAF6F0`) : ≥5:1 sur tous + les textes ajoutés. +- Pas testé avec un vrai événement (formulaire rempli, grille de photos + garnie, écran de succès) faute de slug valide en local — seuls les états + vides/erreur ont pu être vérifiés en conditions réelles de rendu. + +## 2026-09-18 (quater) + +- **4e passe DA "Polaroid Néon"** : suite au retour "ça fait vraiment + low-cost, il faut me rendre cette application sexy", audit écran par + écran (upload, galerie invité, admin galerie, générateur QR, carton + imprimable). Diagnostic : ce n'était pas la direction elle-même + (conservée intégralement, aucune valeur de palette changée) mais des + défauts d'exécution — ombres noires invisibles sur fond quasi noir + (`#12100E`), fond en aplat sans texture, pastille de marque non + généralisée à tous les écrans, icône manquante sur le lien "Télécharger" + admin. Corrigé via : liseré clair ("rim light") sur les cartes/boutons, + grain de fond en SVG data URI inline (~350 octets, 0 requête réseau), + pastille corail derrière le logo sur tous les écrans, icône ajoutée au + lien de téléchargement admin. Bug `[hidden]` masqué par un `display` + auteur (déjà rencontré 3 fois sur ce projet) anticipé et corrigé au + passage sur `#empty-state` (galerie invité, passé en `display: flex`). + Contrastes WCAG AA revérifiés, aucun n'a changé (aucune couleur de texte/ + fond modifiée). Non vérifié visuellement dans un navigateur avant cette + entrée (pas d'accès Chrome dans l'environnement) — à prévisualiser en + local avant déploiement. + +## 2026-09-18 (ter) + +- **`create-event` déployée en prod** sur `kevin-lecou-hub` (migration + `20260918100000_create_event_rpc.sql` + Edge Function + `supabase/functions/create-event/`). Vérifié après coup : grants limités à + `service_role` (n'apparaît pas dans les fonctions `SECURITY DEFINER` + exécutables par `anon`/`authenticated`, contrairement à + `get_public_event`/`insert_public_photo` qui le sont intentionnellement). + Un `search_path` manquant sur la fonction interne + `photobooth.generate_unique_slug` (signalé par le linter sécurité Supabase + juste après déploiement) a été corrigé dans la foulée + (`20260918101500_harden_generate_unique_slug_search_path.sql`). +- Le workflow n8n `photobooth-qr — Créer un événement` reste **inactif** : + à activer par Kévin une fois le DNS `git.assistantaikev.eu` et un premier + test réel du formulaire validés. + +## 2026-09-18 (bis) + +- **Création d'événement** : via un **formulaire n8n** (Form Trigger), + plutôt qu'un formulaire dans l'appli photobooth-qr ou du SQL manuel. Le + workflow insère l'event en base (génère `slug` + `admin_token`) et + renvoie les liens invité/admin à Kévin. Choisi pour rester cohérent avec + la stack n8n déjà en place et éviter d'ajouter une nouvelle couche + d'authentification "admin produit" dans l'appli (aujourd'hui il n'existe + que des tokens par événement, pas de compte admin global). +- **Pas de personnalisation visuelle par événement** : la DA "Polaroid + Néon" reste identique pour tous les événements ; seules les infos + textuelles varient par event (nom, date, limite de photos). Pas de + marque blanche (palette/logo par client) en V1 — jugé de la + sur-ingénierie pour un besoin V2/V3 pas encore confirmé par un client + réel. +- **Remote Git : Gitea auto-hébergé sur le VPS existant** (plutôt que + GitHub). Cohérent avec la préférence d'autonomie de Kévin — tout reste + chez lui, pas de dépendance à un service tiers pour le code source. Le + VPS héberge déjà n8n (Traefik/SSL en place) ; Gitea tournera comme + service Docker supplémentaire derrière le même reverse proxy. Détails + d'implémentation (sous-domaine, ressources, sauvegarde) à définir avant + déploiement. + ## 2026-09-18 - **Galerie invité (revirement)** : les invités peuvent désormais voir diff --git a/src/admin/gallery/gallery.js b/src/admin/gallery/gallery.js index 2b203e4..ede0390 100644 --- a/src/admin/gallery/gallery.js +++ b/src/admin/gallery/gallery.js @@ -273,7 +273,10 @@ function renderPhotoGrid() { // à la place (limitation connue, acceptée pour éviter un fetch+blob par // photo qui multiplierait les requêtes réseau). downloadLink.download = ""; - downloadLink.textContent = "Télécharger"; + // Icône + texte plutôt qu'un lien texte nu : cohérent avec le langage + // iconographique du reste de l'écran (Actualiser, Télécharger tout). + downloadLink.innerHTML = + 'Télécharger'; downloadLink.addEventListener("click", (evt) => { if (isPhotoLinkLikelyExpired(photo)) { evt.preventDefault(); diff --git a/src/admin/gallery/style.css b/src/admin/gallery/style.css index 9d4f3b1..b3ab11a 100644 --- a/src/admin/gallery/style.css +++ b/src/admin/gallery/style.css @@ -45,9 +45,14 @@ --color-yellow: #ffd166; --color-mint: #06d6a0; - /* Exception admin du skill : cartes en #1E1B18. */ - --color-card: #1e1b18; - --color-card-muted: #262220; + /* Exception admin du skill : cartes sombres, jamais blanc cassé. Un peu + éclairci par rapport à la version initiale (#1E1B18 -> #241F1A) lors de + la passe "polaroid poussé à fond" (2026-09-18) : l'écart avec + --color-bg restait trop faible pour se voir sans l'aide d'une ombre, + qui elle-même ne portait pas sur un fond aussi sombre — repéré en + capture d'écran, pas en théorie. */ + --color-card: #241f1a; + --color-card-muted: #2c2621; --color-border-ui: rgba(250, 246, 240, 0.14); --color-border-ui-strong: rgba(250, 246, 240, 0.26); @@ -58,13 +63,19 @@ --radius-pill: 999px; --shadow-soft: 0 10px 24px rgba(0, 0, 0, 0.35); - --shadow-card: 0 6px 16px rgba(0, 0, 0, 0.4); + --shadow-card: 0 16px 32px rgba(0, 0, 0, 0.5); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-standard: cubic-bezier(0.22, 1, 0.36, 1); --font-display: "General Sans", -apple-system, BlinkMacSystemFont, sans-serif; --font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* Rim light : voir le commentaire détaillé dans src/e/style.css. Même + nécessité ici (#1E1B18 sur #12100E, écart de luminance faible : une + ombre noire classique n'y est presque pas visible). */ + --inset-rim: inset 0 1px 0 0 rgba(255, 255, 255, 0.05); + --inset-rim-btn: inset 0 1px 0 0 rgba(255, 255, 255, 0.16); } * { @@ -78,7 +89,13 @@ html { body { margin: 0; font-family: var(--font-ui); - background: var(--color-bg); + /* Grain identique aux écrans invité (voir src/e/style.css) : cohérence + visuelle admin/invité, casse l'aplat du fond, sans impact mesurable + sur le contraste WCAG déjà vérifié pour ce fichier. */ + background-color: var(--color-bg); + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxODAiIGhlaWdodD0iMTgwIj48ZmlsdGVyIGlkPSJuIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC44NSIgbnVtT2N0YXZlcz0iMiIgc3RpdGNoVGlsZXM9InN0aXRjaCIgcmVzdWx0PSJub2lzZSIvPjxmZUNvbG9yTWF0cml4IGluPSJub2lzZSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMSAgMCAwIDAgMCAxICAwIDAgMCAwIDEgIDAgMCAwIDAuMDUgMCIvPjwvZmlsdGVyPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbHRlcj0idXJsKCUyM24pIi8+PC9zdmc+"); + background-repeat: repeat; + background-size: 180px 180px; color: var(--color-text); line-height: 1.45; } @@ -104,11 +121,19 @@ body { color: var(--color-muted-2); } +/* Pastille corail derrière la marque, cohérente avec les écrans invité + (src/e/style.css, src/galerie/style.css) — un détail de marque, pas une + décoration : reste discret, n'entre pas en conflit avec l'exception + admin (sobre, data-first). */ .brand__mark { - width: 16px; - height: 16px; + width: 22px; + height: 22px; + padding: 4px; + box-sizing: border-box; flex-shrink: 0; color: var(--color-coral); + background: rgba(255, 94, 91, 0.14); + border-radius: 50%; } .brand__name { @@ -135,16 +160,31 @@ body { margin: 0.4rem 0 0; } +/* Liseré haut plein plutôt qu'un fond entièrement teinté (l'ancien + .state-block--error en rgba coral 0.14% se distinguait à peine de + --color-card une fois les deux sombres) : reste "sobre" (aucune couleur + décorative sur toute la surface) tout en rendant la carte repérable. */ .state-block { + position: relative; text-align: center; - padding: 1.25rem 1rem; + padding: 1.5rem 1rem 1.25rem; border-radius: var(--radius-ui); margin-bottom: 1rem; background: var(--color-card); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.state-block::before { + content: ""; + display: block; + position: absolute; + inset: 0 0 auto 0; + height: 3px; + background: var(--accent-color, var(--color-coral)); } .state-block--error { - background: var(--color-error-bg); color: var(--color-text); } @@ -205,7 +245,7 @@ body { cursor: pointer; background: var(--color-coral); color: var(--color-ink); - box-shadow: var(--shadow-card); + box-shadow: var(--shadow-card), var(--inset-rim-btn); transition: transform 0.12s var(--ease-standard), background 0.12s ease, box-shadow 0.12s ease; } @@ -219,7 +259,7 @@ body { background: var(--color-card); color: var(--color-text); border: 1.5px solid var(--color-border-ui-strong); - box-shadow: none; + box-shadow: var(--inset-rim); } .btn:disabled { @@ -245,7 +285,7 @@ body { .btn--secondary:hover:not(:disabled) { background: var(--color-card-muted); border-color: var(--color-coral); - box-shadow: none; + box-shadow: var(--inset-rim); } } @@ -284,7 +324,7 @@ a:focus-visible { overflow: hidden; display: flex; flex-direction: column; - box-shadow: var(--shadow-card); + box-shadow: var(--shadow-card), var(--inset-rim); transition: box-shadow 0.2s var(--ease-standard), transform 0.2s var(--ease-standard); } @@ -304,7 +344,7 @@ a:focus-visible { @media (hover: hover) { .photo-card:hover { - box-shadow: var(--shadow-soft); + box-shadow: var(--shadow-soft), var(--inset-rim); transform: translateY(-3px); } @@ -374,7 +414,10 @@ a:focus-visible { } .photo-card__download { - display: block; + display: flex; + align-items: center; + justify-content: center; + gap: 0.35rem; text-align: center; padding: 0.6rem; background: var(--color-coral); @@ -382,6 +425,13 @@ a:focus-visible { text-decoration: none; font-size: 0.82rem; font-weight: 700; + transition: background 0.15s var(--ease-standard); +} + +.photo-card__download .btn-icon { + width: 15px; + height: 15px; + flex-shrink: 0; } .photo-card__download:hover { diff --git a/src/admin/print-template/index.html b/src/admin/print-template/index.html index f414804..7cad506 100644 --- a/src/admin/print-template/index.html +++ b/src/admin/print-template/index.html @@ -224,10 +224,19 @@ color: var(--color-muted); } + /* Pastille corail derrière la marque, même traitement que les écrans + numériques (src/e/style.css, src/galerie/style.css, + src/admin/gallery/style.css) — adapté en unités mm pour l'impression, + opacité un peu plus marquée qu'à l'écran pour rester visible une fois + imprimé (l'encre "délave" toujours un peu le rendu). */ .card__mark { - width: 5mm; - height: 5mm; + width: 7mm; + height: 7mm; + padding: 1.2mm; + box-sizing: border-box; color: var(--color-coral); + background: rgba(255, 94, 91, 0.12); + border-radius: 50%; } .card__brand-name { @@ -439,6 +448,21 @@ baseUrlInput.placeholder = DEFAULT_BASE_URL; + // Pré-remplissage depuis l'URL (?slug=...&baseUrl=...), même logique + // que src/admin/qrcode/index.html — le lien "qrcode" renvoyé par + // create-event pointe vers le générateur QR, pas vers ce carton, mais + // les paramètres sont repris ici aussi pour un usage direct/futur + // (lien partagé à la main, favori, etc.). + const initialParams = new URLSearchParams(window.location.search); + const initialSlug = initialParams.get('slug'); + const initialBaseUrl = initialParams.get('baseUrl'); + if (initialBaseUrl) { + baseUrlInput.value = initialBaseUrl; + } + if (initialSlug) { + slugInput.value = initialSlug; + } + function humanizeSlug(slug) { return slug .replace(/[-_]+/g, ' ') diff --git a/src/admin/qrcode/index.html b/src/admin/qrcode/index.html index 0311869..f68e586 100644 --- a/src/admin/qrcode/index.html +++ b/src/admin/qrcode/index.html @@ -6,6 +6,7 @@ Générateur de QR code — photobooth-qr (admin) +