new-term-radar
Daily radar for new AI terms: names of new models, products and repos, caught on X, Hugging Face, GitHub and sitemaps before keyword tools list them.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Plateforme SaaS de cartes de fidélité digitales. Voir SPEC.md pour le cahier des charges complet.查看完整 README ↗
Plateforme SaaS de cartes de fidélité digitales. Voir SPEC.md pour le cahier des charges complet.
app/, components/, lib/, prisma/, tests/)prisma/schema.prisma) — User, Merchant,
LoyaltyProgram, Customer, LoyaltyMembership, Transactionlib/db/prisma.ts)lib/auth/supabase-browser.ts, supabase-server.ts)lib/auth/session.ts) — requireMerchant(),
requireCustomer()lib/loyalty/visit.ts) — addVisit(),
redeemReward(), avec les contrôles de sécurité du §18 déjà en placelib/qr/generate.ts)/)lib/validation/auth.ts)lib/auth/actions.ts) — inscription commerçant,
connexion, déconnexionproxy.ts (ex-middleware.ts, renommé en Next 16) — rafraîchit la session Supabase à chaque requête
(pattern @supabase/ssr) et redirige tôt sur /dashboard/*,
/customer/*, /admin/*/login et /register (app/(public)/)app/dashboard/layout.tsx (requireMerchant()) et
app/customer/layout.tsx (requireCustomer())/dashboard et /customer (remplacées aux étapes 04 et 06)lib/dashboard/stats.ts) — clients, visites du
jour, récompenses disponibles, toutes filtrées par merchantId/dashboard (§6) — bouton principal "SCANNER UN CLIENT", 3 tuiles de
stats, encart programme, navigationcomponents/merchant/) — StatCard, NavCard,
StepPlaceholder/dashboard/{program,customers,scan,history,settings} en
placeholder, pour éviter les liens mortslib/validation/program.ts) — visitsRequired borné
entre 1 et 100 côté serveurlib/programs/actions.ts) — merchantId pris dans la
session, jamais dans le formulaire ; un seul programme par commerce en V0.1/dashboard/program (§7) — formulaire Nom / Visites / Récompense, puis
fiche du programmegenerateProgramJoinQr()/join/[programId] (§8) — page publique atteinte via le QR du commercelib/customers/actions.ts) — session anonyme Supabase
(le §8 ne demande ni mot de passe ni confirmation), puis User + Customer
+ carte créés dans une seule écriture imbriquée/customer — liste des cartes ; /customer/card/[membershipId] (§8) —
compteur X/Y, visites restantes, récompense (§12), bouton AFFICHER MON QRgenerateCustomerQr() — n'encode que qrToken (§9)addVisit() : une même carte ne peut pas être créditée
deux fois en moins de N minutesLoyaltyProgram.minMinutesBetweenVisits (défaut 30, 0 désactive)LOYALTY_MIN_MINUTES_BETWEEN_VISITS ne fixe plus que la valeur donnée
aux programmes NOUVELLEMENT créés/admin) pour
régler ce délai sans passer par SupabaseLe commerçant ne voit pas ce réglage : c'est un paramètre d'exploitation.
Dans Supabase → Table Editor → table loyalty_programs → colonne
min_minutes_between_visits → modifier la ligne du commerce concerné.
Prise en compte immédiate, sans redéploiement : la valeur est lue en base à chaque validation de visite.
Ordres de grandeur : restauration rapide 60-120, restaurant 180, coiffeur ou institut 1440 (une visite par jour), commerce à forte rotation 0 (désactivé).
lib/loyalty/scan.ts — lecture seule, refait les contrôles
d'appartenance du §18 avant de révéler quoi que ce soitPOST /api/loyalty/scan (§15) — requireMerchant() rappelé dans la
route : un layout ne protège pas un Route Handlercomponents/scanner/qr-scanner.tsx — caméra + décodage jsqr/dashboard/scan (§10) — nom du client, compteur, bouton "+1 VISITE"
(branché à l'étape 09)jsqr (~30 Ko, Apache 2.0)POST /api/loyalty/visit (§15) — authentifie puis délègue à addVisit(),
sans logique métier propre : l'atomicité du §11 reste dans lib/loyalty/visit.ts/dashboard/scan, désactivé pendant l'appel
pour qu'un double-clic ne parte pas deux foisPOST /api/loyalty/{scan,visit} sans session → HTTP 401
(cf SPEC §18 : "Client → appeler directement l'API de validation : Refusé.")POST /api/loyalty/redeem (§15) — délègue à redeemReward(), qui
revérifie reward_available dans la même transaction que la remise à
zéro : la double utilisation est impossible (§18)/dashboard/scan, en confirmation
à deux temps (le geste est irréversible)lib/loyalty/scan.ts refactorisé — findMembershipByQrToken() et
findMembershipById() partagent les contrôles du §18POST /api/loyalty/redeem sans session → HTTP 401lib/transactions/queries.ts — lecture seule de l'historique immuable ;
côté commerçant filtré par merchantId, côté client par carte/dashboard/history (§14) — date, heure, client, action, variation ;
liste sur mobile, tableau dès sm:lib/format.ts — fuseau Europe/Brussels fixé explicitement, sinon un
serveur en UTC décale les heures affichées/api/transactions : les deux vues sont des Server Components
qui lisent directement en base (le §15 autorise l'adaptation à Next.js)vitest (dépendance de développement uniquement), npm testtests/loyalty.test.ts — 12 tests contre la vraie base, couvrant §11,
§13 et §18 ; chaque test crée puis supprime son propre commerce et son
propre client, les données de dev ne sont pas touchéestests/setup.ts — charge .env (vitest ne le fait pas) et fige le délai
anti-cumul à 30 min pour que la suite soit déterministe@/ reproduit dans vitest.config.mts, sans dépendance de plusPOST /api/loyalty/{scan,visit,redeem} sans session → 401npm install
cp .env.example .env
# Remplir DATABASE_URL, DIRECT_URL, NEXT_PUBLIC_SUPABASE_URL,
# NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY
npm run db:generate # génère le client Prisma
npm run db:push # crée les tables sur Supabase (dev rapide)
npm run dev
npm test # lance la suite une fois
npm run test:watch
Les tests écrivent dans la base pointée par DATABASE_URL : à ne PAS lancer
contre la base de production.
Dans le dashboard Supabase, Authentication → Sign In / Providers :
Puis, sur le provider Email :
/dashboard. Pour tester le flux complet
en local, désactiver la confirmation ;Site URL = http://localhost:3000
en dev.Deux projets Supabase distincts : un pour le développement, un pour la
production. npm test écrit dans la base pointée par DATABASE_URL — le
faire tourner contre la prod créerait et supprimerait des données chez de
vrais commerçants.
localhost:3000, elle rend ces liens inutilisables.| Variable | Remarque |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | projet de PROD |
NEXT_PUBLIC_SUPABASE_ANON_KEY | projet de PROD |
SUPABASE_SERVICE_ROLE_KEY | projet de PROD |
DATABASE_URL | pooler, port 6543, ?pgbouncer=true |
DIRECT_URL | connexion directe, port 5432 |
NEXT_PUBLIC_APP_URL | l'URL de production — encodée dans le QR d'inscription |
LOYALTY_MIN_MINUTES_BETWEEN_VISITS | 30 en production |
NEXT_PUBLIC_APP_URL est inlinée au build : la modifier exige un
redéploiement, pas seulement un redémarrage.
Les identifiants de prod vivent dans .env.production.local (ignoré par git).
Charger ce fichier dans l'environnement, puis npx prisma db push.
Vercel ne déploie en production que sur un push vers la branche par défaut du dépôt. Un dépôt poussé AVANT la connexion à Vercel ne déclenche rien : il faut un nouveau commit.
Deux univers volontairement distincts (cf SPEC §17) :
Côté client — du papier. La carte est un ticket : fond crème grainé, perforation, numéro de série, tampons posés de travers. Elle doit ressembler à un objet qu'on possède, lisible à un mètre dans une boutique très éclairée.
Côté commerçant — sombre, avec le vert de la marque réservé à l'action du moment. Il travaille avec, en vitesse, derrière un comptoir : grosses cibles tactiles, contrastes élevés, une seule chose colorée par écran pour qu'on ne puisse pas la manquer.
Jetons dans globals.css : --surface, --surface-raised, --line,
--fg, --fg-soft, --fg-faint. Ne pas réintroduire de classes
neutral-* de ce côté : elles viennent du scaffold et cassent le thème.
Trois formes, dans components/brand/logo.tsx :
| Forme | Usage |
|---|---|
Wordmark | Le mot complet, deux E ajourés en vert. Au-delà de ~120 px de large |
BrandMark | Pastille ronde, R ajouré. De 24 à 64 px |
BrandMarkSolid | Pastille au R plein. En dessous de 24 px |
En dessous de 24 px les entailles se referment et la lettre devient une tache : la version simplifiée n'est pas une trahison du logo, c'est la seule façon de rester lisible sur un favicon.
Vert #2FBF71 sur fond sombre, #1E8A50 sur fond clair — sinon il vibre.
La marque ne prend pas la place du commerce. La pastille en haut de la carte client porte l'initiale du COMMERÇANT : c'est son identité que le client vient chercher. Recompens signe discrètement en pied de carte.
Chargée par next/font dans app/layout.tsx : Archivo Black (titres et
grands chiffres), Instrument Sans (texte), IBM Plex Mono (numéros de
carte et libellés du ticket), Saira et Saira Stencil (la marque, et
elle seule — deux membres de la même famille, ce qui permet d'alterner lettres
pleines et ajourées sans disloquer le mot). Avant ça, globals.css référençait une variable
--font-geist-sans que rien ne chargeait : l'app s'affichait en Arial.
Une seule valeur en base, Merchant.brandColor (hex, défaut #A63A28), suffit
à décliner la carte : encre des tampons, bouton, accents. Le papier et le texte
ne changent pas — c'est ce qui donne une famille reconnaissable plutôt qu'un
patchwork. lib/theme.ts en dérive une couleur de texte lisible, pour qu'un
commerçant qui choisit un jaune vif n'obtienne pas un bouton illisible.
Le commerçant ne choisit pas sa couleur : comme le délai anti-cumul, c'est un
réglage d'exploitation. Supabase → Table Editor → merchants → colonne
brand_color. Effet immédiat, sans redéploiement.
Un seul gabarit de carte pour tous les métiers, donc aucun visuel à produire pour accueillir un nouveau commerçant.
Merchant.cardStyle vaut TICKET (défaut) ou VESSEL.
Quand cardStyle = VESSEL, Merchant.vesselShape choisit la silhouette.
Trois existent, et elles racontent la progression de trois façons :
| Silhouette | Métier | Comment ça progresse |
|---|---|---|
CUP | café | la tasse se remplit |
PASTA | restaurant | la portion grossit — une assiette ne se remplit pas par le bas |
SANDWICH | sandwicherie | l'objet se construit, une couche par visite |
SANDWICH est la plus lisible des trois : on voit où en est le client sans
lire le chiffre. C'est celle à montrer à quelqu'un qui découvre le produit. Les silhouettes vivent dans lib/loyalty/vessels.ts :
géométrie, couleur du contenu et garniture. Ajouter un métier = ajouter une
entrée dans ce fichier et une valeur à l'enum.
VESSEL sans silhouette retombe sur TICKET : un réglage incomplet ne casse jamais la carte d'un client.
La couleur du CONTENU appartient à la silhouette, pas au commerce — des pâtes
sont couleur pâtes. Seul le fond de la carte vient de brandColor.
La carte ouverte dans les deux minutes qui suivent une visite validée
(STAMP_ANIMATION_WINDOW_MS) joue sa séquence, une fois :
gesture dans lib/loyalty/vessels.ts) :
drop — elle TOMBE dans le contenant. Le geste le plus fort, mais il
suppose un contenant ouvert par le haut et un objet crédible : un grain
de café dans une tasse. Sa zone de chute est découpée et s'arrête à
l'ouverture, sinon elle a l'air de passer devant le contenant.streak — elle TRAVERSE l'écran et sort par le coin. Marche partout,
y compris quand rien ne tombe naturellement dans le contenant : rien ne
tombe dans une assiette de pâtes.Aucune boucle : c'est une récompense, pas une animation d'ambiance — et un écran qui gigote vide la batterie. Le réglage système « réduire les animations » est respecté.
Envoyés par Resend, branché en SMTP personnalisé sur Supabase (Project Settings → Authentication → SMTP Settings) :
Host smtp.resend.com
Port 587
Username resend ← en minuscules, la casse compte
Sender [email protected]
Le domaine recompens.com est vérifié chez Resend. Ses enregistrements DNS
vivent chez OVH et ne touchent pas le SPF racine : Resend passe par les
sous-domaines send et rsend, ce qui laisse la boîte OVH intacte.
Score mail-tester : 9,6/10, authentification SPF + DKIM + DMARC complète.
⚠️ Supabase → Authentication → URL Configuration → Redirect URLs doit
contenir https://recompens.com/**. Sans cette autorisation, le lien de
réinitialisation ne revient pas sur /auth/reset et le parcours casse.
Les modèles de messages sont versionnés dans emails/. Ils ne sont pas lus
par l'application : il faut les coller dans Supabase → Authentication →
Emails. Les garder ici évite qu'ils n'existent que dans un tableau de bord.
Par ordre d'importance, hors périmètre V0.1 déjà livré.
lib/programs/update.ts,
formulaire replié sur /dashboard/program. Un programme s'ARRÊTE
(active = false) mais ne se supprime pas : la suppression emporterait
les cartes et l'historique de tous ses clients.
Règle sur le seuil : on peut débloquer une récompense, jamais en
retirer une. Relever le seuil ne reprend pas ce qu'un client a gagné.recompens.com. Le service intégré de
Supabase, limité à quelques mails par heure, n'est plus utilisé./forgot-password, /auth/reset,
/reset-password. La réponse est la même que l'adresse existe ou non :
dire « compte inconnu » révélerait qui est inscrit.recompens.com, avec recompense.be en
redirection pour protéger l'orthographe naturelle.CUP et PASTA sont faites. Traduire les erreurs d'authentification. registerMerchantAction
remonte tel quel le message de Supabase, en anglais — « Error sending
confirmation email » s'affiche à un commerçant français. loginAction
est déjà traité : il trace la cause réelle et distingue l'adresse non
confirmée du dépassement de quota.
Confirmer le mot de passe à l'inscription. Il n'est saisi qu'une fois : une faute de frappe crée un compte inaccessible, et le message d'erreur ne peut pas la distinguer d'un mot de passe oublié. Cas rencontré en vrai le 2026-09-02.
Liste des clients (/dashboard/customers), encore en placeholder.
/admin)L'accès s'ouvre au rôle ADMIN et aux adresses listées dans
ADMIN_EMAILS (séparées par des virgules). Cette seconde porte existe parce
que l'exploitant est aussi commerçant de ses commerces de démonstration : lui
donner le rôle ADMIN lui ferait perdre /dashboard.
Trois gestes y vivent :
estAdmin() est
revérifié en base à chaque requête ;visit_count) n'est jamais modifié depuis le
client. Seul lib/loyalty/visit.ts y touche, après vérification serveur
du merchant authentifié et de l'appartenance du membership à son programme.LoyaltyMembership.qrToken (uuid opaque),
jamais l'id métier ni le compteur.Transaction, créée dans la
même transaction DB que la mise à jour du membership (atomicité).LOYALTY_MIN_MINUTES_BETWEEN_VISITS) limite le cumul d'achats sur une seule
carte. Il ne remplace pas un vrai anti-fraude.identities vide. Créer une ligne métier sur cet
id produit un enregistrement orphelin, que plus rien ne rattache au vrai
compte — la connexion échoue alors définitivement. registerMerchantAction
teste donc identities.length === 0 avant toute écriture.deleteAuthUser()
supprime le compte. Sans ça, la personne est enfermée — la connexion répond
« compte incomplet », la réinscription « adresse déjà utilisée », et il n'y
a plus d'issue. C'est arrivé en vrai.User.id est l'id Supabase Auth : c'est la jointure utilisée par
getCurrentUser(). L'inscription crée le compte Supabase puis User +
Merchant dans une seule écriture Prisma imbriquée (donc une transaction).user_metadata Supabase sert uniquement au routage
du middleware (runtime Edge, pas d'accès Prisma). Il est modifiable par
l'utilisateur lui-même : la source de vérité reste User.role en base,
revérifié par requireMerchant() / requireCustomer() dans les layouts et
dans chaque route API.