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