Nouveautés

Historique des versions et des fonctionnalités de 3amia · AI.

v1.22.11 — Juillet 2026 — FIX CRITIQUE (prod figée)
  • ·Boucle infinie sur /articles/[slug] — introduite en v1.22.10 (commit bd041f0) — ouvrir une fiche article gelait l'onglet (boucle infinie sur le thread principal). Cause confirmée : INLINE_RE, la regex /g du tokenizer inline, était déclarée au niveau module et réutilisée en RÉ-ENTRANCE (le parsing récursif sur le label d'un lien / l'intérieur d'un gras/italique appelait renderInline() récursivement avec le MÊME objet RegExp). Le dernier exec() d'un appel imbriqué qui retourne null réinitialise automatiquement lastIndex à 0 côté moteur JS — la boucle while de l'appel PARENT reprenait alors sa recherche depuis le début du texte au lieu de continuer où elle en était, rejouant indéfiniment le premier lien/gras rencontré. Reproduit isolément : sur une ligne aussi simple qu'un gras + un lien, l'ancienne logique ne termine jamais (confirmé — abandon manuel après 20 000 itérations en test isolé, sans le fix).
  • ·Tokenizing extrait en fonction puresrc/lib/articleInline.ts (nouveau, aucune dépendance React/JSX) : tokenizeInline(text, depth) instancie une regex fraîche (new RegExp(...)) à CHAQUE appel, y compris chaque appel récursif — jamais d'objet partagé. La branche "run arabe" est strictement une feuille (jamais de récursion dessus, 2ᵉ cause potentielle identifiée). Récursion sur lien/gras/italique bornée à MAX_INLINE_DEPTH = 3 (au-delà, contenu traité en feuille via tokenizeArabicOnly()). Garde-fou dur MAX_INLINE_ITERATIONS = 5000 dans la boucle while (throw silencieux → coupe et retourne, ne devrait jamais se déclencher avec les protections ci-dessus mais protège contre toute régression future).
  • ·ArticleContent.tsxrenderInline()/INLINE_RE (module-level) remplacés par un appel à tokenizeInline() (logique pure) + renderTokens() (mapping JSX pur, aucune regex). parseBlocks(), la détection des paires AR/FR (système bilingue des articles) et le composant a des blocs raw (liens /mot/...next/link, ajouté en v1.22.10) sont inchangés — l'objectif de v1.22.10 reste acquis : **gras** rend du gras, [texte](/mot/x) rend un lien cliquable, dans les blocs section/bilingual comme dans les blocs raw.
  • ·Nouveau test src/lib/articleInline.test.ts (npm run test:article-inline) — 6 cas (ligne 100% arabe vocalisée, arabe+lien interne, terme arabe en gras, FR avec lien+gras mélangés, ligne mixte AR+FR, cas adversarial avec formations répétées denses) + vérification du garde-fou. Chaque cas doit se terminer sans exception en moins de 50ms. Le npm run build de ce projet ne peut pas détecter ce type de bug (pas de Supabase dans le sandbox → génération statique des pages articles jamais exercée, le rendu réel n'est jamais exercé) : ce test runtime est donc la seule garde-fou de non-régression pour ce composant. Vérifié : le test échoue (boucle non-terminante) contre l'ancienne implémentation, passe contre la nouvelle.
  • ·CLAUDE.md — 2 nouvelles règles permanentes (§ Gotchas développement) : regex /g jamais partagée en ré-entrance récursive (toujours new RegExp(...) local à la fonction) ; tout parsing récursif basé sur une regex/boucle while doit avoir un test runtime de terminaison dédié, tsc --noEmit/npm run build ne suffisent pas dans cet environnement.
v1.22.10 — Juillet 2026
  • ·Fix ArticleContent.tsx — syntaxe markdown affichée littéralement**gras** et [lien](/mot/...) s'affichaient tels quels (astérisques/crochets visibles) dans les paires de lignes AR/FR (blocs section/bilingual de parseBlocks(), ex: titres arabes + sous-titre français, lignes séparées par <br>). Cause confirmée en base (article la-chaleur-en-arabe-classique) : ces blocs ne passent jamais par ReactMarkdown — seuls les blocs raw en bénéficiaient — et étaient rendus via un simple <p>{texte}</p>/renderFrLine() (isolation bidi arabe uniquement, aucun parsing markdown).
  • ·renderFrLine() remplacée par renderInline() — même isolation bidi des séquences arabes (dir="rtl", mêmes plages Unicode qu'avant), plus parsing inline pour lien [texte](url), gras **texte**, italique *texte* (récursif sur le contenu imbriqué). Appliquée aux lignes ar ET fr des blocs section/bilingual (le AR peut aussi contenir un lien, cf. l'article test). Aucun changement à parseBlocks() ni à la logique de détection des paires AR/FR (système bilingue des articles non touché).
  • ·Liens internes /mot/...<Link> Next.jsrenderInline() route les hrefs commençant par / vers next/link, le reste vers <a target="_blank" rel="noopener noreferrer">. Même traitement ajouté aux blocs raw existants via un nouveau composant a dans les components de ReactMarkdown (AnchorComponent, absent auparavant — seul strong était overridé pour le raccourci "mot en gras = lien vers sa fiche").
  • ·Vérifié : regex testée directement contre la ligne réelle en base (نَقُولُ [صَحْرَاءُ](/mot/sa7raa2-tn) عَنْ...) — lien et gras correctement extraits, séquences arabes isolées individuellement.
v1.22.9 — Juillet 2026
  • ·Fix autocomplete — mot anglais tapé directement introuvable — le mode field='fr' de autocompleteSearch() (api/search/route.ts) ne cherchait (via la RPC search_df_trad) que translation_fr : taper "know"/"help"/"dishes" directement ne remontait rien (il fallait taper la traduction française pour voir apparaître le mot). Ajout d'un ILIKE séparé sur translation_en (jamais .or() avec variable — règle PostgREST virgule), en parallèle de la RPC, fusionné par id via le Set de dédup existant. La RPC search_df_trad elle-même n'est pas touchée (v1.22.8 documentait déjà pourquoi : RETURNS TABLE(...) explicite, risque d'overloads dupliqués si modifiée — cf. v1.14.2/v1.14.3) ; l'enrichissement translation_en pour l'affichage des lignes RPC (déjà en place depuis v1.22.8) est conservé tel quel.
  • ·scoreRow() étendu à translation_en — paramètre optionnel translationEn (7ᵉ arg, rétro-compatible) propagé à scoreWordRelevance() (déjà capable de le prendre en compte depuis v1.22.7) ; matchType='trad' déclenché aussi sur exact/préfixe/contenu de translation_en, même priorité que translation_fr. Appliqué à tous les appels de scoreRow() (secondarySearch, dfRowToSuggestion, RPC MSA) pour cohérence, sans changer le ranking existant (arabe/latin > traduction > exemple).
  • ·Pas de priorisation par locale côté serveur — les deux champs sont cherchés sans distinction (le fallback explicitement accepté par la demande : détecter la locale serveur aurait nécessité un paramètre supplémentaire côté client non nécessaire ici). Tabs Transliterated/Arabic/French-English inchangés.
v1.22.8 — Juillet 2026
  • ·Fix autocomplete — traduction affichée figée en français — les suggestions de SearchBar.tsx affichaient toujours translation_fr, même en locale EN. Suggestion (api/search/route.ts) gagne un champ translation_en : ajouté aux select(...) explicites (secondarySearch, mode arab, variantes phonétiques, pool fallback 200-lignes) et lu directement sur les lignes RPC search_df_fuzzy/search_words_fuzzy (RETURNS SETOF dialect_forms/words — colonne déjà présente, aucun changement RPC). Seule search_df_trad (mode fr, RETURNS TABLE(...) explicite sans translation_en) ne l'expose pas : enrichissement par un lookup id séparé après l'appel RPC plutôt que modifier sa signature — évite le bug des overloads dupliqués sur RETURNS TABLE déjà documenté (v1.14.2/v1.14.3). Aucune RPC ni logique de matching touchée.
  • ·SearchBar.tsx : helper translationFor(s) — retourne translation_en si locale==='en' et non vide, sinon fallback translation_fr. Utilisé dans les 3 modes d'affichage (arabe/français/latin) y compris pour le texte passé à <HighlightText> — le surlignage matche donc désormais le bon champ selon la locale (ex: taper "know" surligne dans translation_en, pas translation_fr).
  • ·Tab "Français"/"English" adaptatif — le pill de mode fr (recherche par traduction) affiche désormais t('search.fieldEn') ("English"/"Anglais") en locale EN au lieu de traduire littéralement le mot "French". Nouvelle clé search.fieldEn dans fr.json/en.json. Aucune auto-détection de langue de la query n'existait déjà côté mode — non ajoutée (hors-scope, RPC backend inchangée).
v1.22.7 — Juillet 2026
  • ·Fix recherche par translation_en — le mode field='fr' de searchWords() (db.ts, mode par défaut de /dictionnaire) ne cherchait que translation_fr + example_fr : un mot tapé en anglais ne remontait jamais. Ajout d'un 3ᵉ ILIKE parallèle sur translation_en (jamais .or() — même règle PostgREST que les autres colonnes), dans searchDf() (dialect_forms) et searchMsa() (words MSA), fusionné dans l'ordre translation_fr → translation_en → example_fr. translation_en était déjà retourné par les select('*') existants, aucun changement de SELECT nécessaire.
  • ·Tri de pertinence étendu à translation_enscoreWordRelevance() (search-utils.ts) accepte un 7ᵉ paramètre optionnel translationEn (rétro-compatible, tous les appels existants inchangés) : exact/préfixe/contenu sur translation_en évalués au même niveau de priorité que translation_fr (0.77/0.75/0.72). Ranking existant (arabe/latin > traduction > exemple) inchangé. Appel de tri dans searchWords() mis à jour pour passer translation_en.
  • ·Placeholder barre de recherche — mention des 3 languessearch.placeholderFr (fr.json/en.json), affiché par défaut sur /dictionnaire (mode fr initial), disait uniquement "Chercher une traduction française…" / "Search a French translation…". Corrigé en "Chercher en arabe, français ou anglais…" / "Search in Arabic, French or English…" — déjà conditionné par useLocale()/t(), aucun changement de code dans SearchBar.tsx.
v1.22.6 — Juillet 2026
  • ·Fiche article — bloc de partage remonté sous le header — le bloc "Partager cet article" (src/app/articles/[slug]/page.tsx) était placé après le contenu Markdown et la navigation série, tout en bas de page. Déplacé juste après le bloc Auteur/date, avant ArticleContent — visible sans scroller jusqu'au bas de l'article. Déplacement JSX pur, logique de partage (liens Twitter/WhatsApp) inchangée.
  • ·Icône WhatsApp officiellepublic/icons/whatsapp.png (128×128, généré à partir du path officiel simple-icons — fond vert #25D366, glyphe blanc — via sharp, aucun asset copié depuis un CDN externe non accessible depuis ce réseau). Remplace le texte seul "WhatsApp" par <img src="/icons/whatsapp.png" width={24} height={24} /> + libellé, cohérent avec le bouton Twitter à côté.
v1.22.5 — Juillet 2026
  • ·Fiche mot — "Je l'utilise" remonté au-dessus du pli mobile — la section WordContribution (bouton "Je l'utilise" + 3 autres tabs) est déplacée juste après le bloc hero (forme arabe + translittération + traduction), avant "Exemple d'utilisation"/"Équivalents dans d'autres dialectes" (grille Details), Morphologie, Famille de mots, Même schème, Déclinaisons, Régions d'usage et Action links. Objectif : sur mobile, "Je l'utilise" est visible sans scroller jusqu'en bas de la fiche. Aucun changement de logique de soumission — déplacement JSX pur dans src/app/mot/[slug]/page.tsx.
v1.22.4 — Juillet 2026
  • ·WordCard — exemple arabe affiché en permanence — le prop showExample (n'affichait l'exemple que si !!query, donc seulement en résultats de recherche) est supprimé ; l'exemple arabe (word.example_phrase) s'affiche désormais dans toutes les situations où WordCard est utilisé dès qu'il existe : /dictionnaire (browse ET recherche), page d'accueil ("Mots récents"), /racine/[racine], section mots liés de /mot/[slug]. Condition word.example_phrase && déjà en place — aucun espace vide laissé pour les mots sans exemple.
v1.22.3 — Juin 2026
  • ·Fix recherche par mot-clé d'exemple — branches field='fr'/'arab'/'latin' dans searchDf() et searchMsa() — La recherche /dictionnaire retournait field='fr' (défaut) qui tombait sur un early-return limité à translation_fr, sans jamais toucher example_fr. Même problème pour field='arab' (ignorait example) et field='latin' (ignorait example). Chaque branche réécrite avec des ILIKE parallèles sur les colonnes exemple (jamais .or() avec variable — règle PostgREST), dédup par id. Exemple concret : requête "montre" remonte désormais mounassib-tn (dont example_fr = "Cette montre est appropriée pour l'occasion") alors qu'il était invisible auparavant.
v1.22.2 — Juin 2026
  • ·Cohérence de la recherche autocomplete ↔ /dictionnaire — Les deux moteurs cherchent désormais dans les mêmes champs : form_ar, form_latin, translation_fr, example, example_fr. Avant ce fix, /dictionnaire ignorait les colonnes example/example_fr alors que l'autocomplete les couvrait via secondarySearch(). Tri par pertinence ajouté dans searchWords() : exact (1.0) > prefix (0.9) > contenu ar/latin (0.8) > traduction (0.72–0.77) > exemple (0.60). Fonction partagée scoreWordRelevance() extraite dans search-utils.ts, utilisée par db.ts et /api/search/route.ts. WordCard : nouveau prop showExample — affiche l'exemple arabe sous le mot dans les résultats de recherche (donnée de contexte visuel, surtout utile quand le match vient de l'exemple).
v1.22.1 — Juin 2026
  • ·Version anglaise — Phase 2 : traductions DB affichées — Les champs translation_en et example_en (colonnes Supabase, ~2191 mots enrichis) sont désormais utilisés à l'affichage quand locale === 'en'. Composants corrigés : WordCard (grille dictionnaire), fiche /mot/[slug] (bloc Traduction + traduction de l'exemple, via <T>), WordFamily (famille de mots / même racine), PatternMatches (mots de même schème). SynonymList inchangé (affiche arabe + translittération uniquement). DB : example_translation_en ajouté au type Word (mappé depuis example_en en base), translation_en ajouté aux selects et mappers de getWordsByPattern/getWordsByRacine via les interfaces PatternMatch/FamilyMatch.
v1.22.0 — Juin 2026
  • ·Version anglaise — Phase 1 complète — Toggle FR/EN dans la Navbar (cookie NEXT_LOCALE), aucun changement d'URL ni d'ISR. LocaleProvider + useLocale() + t() dans src/lib/i18n.tsx. Composant <T fr="..." en="..." /> pour les pages server ISR. 215 clés de traduction dans src/locales/fr.json + src/locales/en.json. Toutes les pages publiques couvertes : homepage, dictionnaire, mot/[slug], racines, racine/[racine], qcm, qcm/[slug], articles, articles/[slug], articles/proposer, ajouter, guide-phonétique, a-propos. Tous les composants UI traduits : Navbar, SearchBar, RacineSearchBar, DialectFilter, POSFilter, WordCard, WordFamily, WordMorphology, SuggestEdit, WordContribution, QuizPlayer, ShareButton. SEO (generateMetadata) et ISR inchangés côté serveur.
v1.21.6 — Juin 2026
  • ·Fix noms variables UpstashloginRateLimit.ts corrigé pour lire KV_REST_API_URL et KV_REST_API_TOKEN (convention Vercel Storage × Upstash) au lieu de UPSTASH_REDIS_REST_URL/TOKEN (convention Upstash brute). Noms confirmés depuis le dashboard Vercel de l'intégration "protect-admin".
v1.21.5 — Juin 2026
  • ·Rate limiting login — migration in-memory → Upstash Redissrc/lib/loginRateLimit.ts migré de Map en mémoire vers @upstash/redis (clé admin:login:{ip_hash}, TTL 30 min, nettoyage automatique Redis). Protection désormais cross-instances Vercel (persistée en dehors des lambdas). Variables attendues : UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN. Fallback gracieux si Redis indisponible : rate limiting désactivé mais le mot de passe reste vérifié côté serveur. @upstash/ratelimit aussi installé mais non utilisé ici (compte toutes les requêtes, pas seulement les échecs — incompatible avec la sémantique "5 failures" demandée). Comportement utilisateur inchangé : mêmes messages, mêmes seuils.
v1.21.4 — Juin 2026
  • ·Sécurité — brute-force protection sur /admin — rate limiting sur /api/admin/login : 5 tentatives échouées en 15 min → blocage 15 min (429). Implémenté via src/lib/loginRateLimit.ts (Map en mémoire dédiée, séparée du rate limiter contributions). Le compteur se réinitialise après un login réussi. Avertissement progressif dès les 2 dernières tentatives restantes ("X tentative(s) restante(s) avant blocage"). Délai 500ms conservé sur chaque échec (ralentit en dessous du seuil). Limitation connue : en mémoire, non persisté entre instances Vercel — suffisant pour un site mono-admin ; upgrade Supabase/Upstash si besoin de persistance inter-instances.
  • ·Sécurité — noindex sur /adminsrc/app/admin/layout.tsx créé (server component) avec robots: { index: false, follow: false } → balise <meta name="robots" content="noindex,nofollow"> générée par Next.js. Disallow: /admin ajouté à public/robots.txt. Double protection : robots.txt indique aux crawlers de ne pas visiter, noindex leur interdit d'indexer même si visitée directement.
v1.21.3 — Juin 2026
  • ·Fix DYNAMIC_SERVER_USAGE sur /racine/[racine]export const dynamic = 'force-dynamic' restauré (retiré par erreur en v1.14.3 puis re-confirmé nécessaire). Cause précise : (await searchParams).dialect est lu en generateMetadata et dans le composant page — accéder à searchParams dans une page avec revalidate (ISR) est incompatible avec le rendu statique, Next.js lançait une erreur DYNAMIC_SERVER_USAGE (1989 occurrences dans les logs Vercel depuis le 23 juin). revalidate = 60 retiré en cohérence avec force-dynamic (ISR n'a plus de sens une fois le rendu contraint par-requête). Règle permanente déjà documentée dans CLAUDE.md v1.14.3 : tout segment dynamique + searchParams doit porter force-dynamic.
  • ·Page d'erreur 500 thématiséesrc/app/error.tsx créé (composant client, error boundary App Router). Affiche « خطأ » en grand (même style que la page 404), titre "Une erreur est survenue", bouton "Réessayer" (reset()) + lien retour accueil. Corrige le second symptôme des logs : "Failed to load static file for page: /500 ENOENT" (663 occurrences) — Next.js ne pouvait pas servir de page 500 car aucun error boundary n'était défini.
v1.21.2 — Juin 2026
  • ·HeroSlideshow desktop — 5 images paysageSLIDES_DESKTOP remplacé par les vrais chemins public/heroimagesweb/ : Chefchaouen ruelle bleue (MA), Aït Benhaddou kasbah (MA), Sidi Bou Said coucher de soleil (TN), deux photos de dunes (coucher + lever de soleil). Responsive maintenu : mobile continue avec public/hero/ (4 images portrait inchangées), desktop bascule sur les nouvelles images à partir de md:.
v1.21.1 — Juin 2026
  • ·Fix font-bold sur texte arabe dans les listes — SynonymList, WordFamily, PatternMatches, DeclinaisonsSection : font-boldfont-normal (400) sur les mots arabes affichés dans les items de liste — cohérent avec le fix appliqué sur WordCard en v1.20.0. La règle "Readex Pro poids 400 partout" est maintenant complète sur toutes les vues publiques de /mot/[slug].
  • ·HeroSlideshow — structure responsive mobile/desktop — refactorisé en deux blocs Tailwind (md:hidden / hidden md:block) avec SlideshowInner partagé. Desktop prêt à recevoir les images public/heroimagesweb/ dès que Mehdi les pousse sur main (SLIDES_DESKTOP = SLIDES_MOBILE en attendant — aucune régression).
  • ·Fix contraste titre hero sur images doréestext-shadow double couche ajouté sur h1 (0 2px 10px rgba(0,0,0,.55), 0 1px 3px rgba(0,0,0,.35)) et sur عامية mobile (0 2px 12px rgba(0,0,0,.65), 0 1px 4px rgba(0,0,0,.45)). La shadow est héritée par le <em> doré "des dialectes arabes" — titre lisible sur n'importe quelle image, y compris fond jaune/orange (dunes coucher de soleil).
v1.21.0 — Juin 2026
  • ·Refonte DialectBadge — drapeau plein, sans fond coloré — les badges dialecte (WordCard, SynonymList, WordFamily, PatternMatches, fiche mot /mot/[slug]) affichent désormais le drapeau SVG remplissant un carré à coins arrondis (2px), sans fond coloré derrière. Implémentation : prop square ajoutée à FlagSVG (preserveAspectRatio="xMidYMid slice"), composant DialectBadge entièrement réécrit (carré 22/28/32px selon size). Le code dialecte (TN, MA…) ou le nom complet (showName) s'affiche à côté du carré, jamais par-dessus. Suppression de la class CSS .dialect-badge (plus utilisée).
v1.20.1 — Juin 2026
  • ·Fix hero /a-propos — fallback image si vidéo bloquée — section hero de /a-propos : ajout d'un background-image CSS (tataouine-troglodyte.jpg) directement sur la <section>. Si le navigateur bloque l'autoplay de la vidéo (mobile, politique stricte), l'image reste visible en fond sous l'overlay. HeroVideo joue par-dessus quand il peut.
  • ·Fix tirets cadratins /a-propos — 4 em-dashes dans le bloc Présentation (étymologie zeedna) remplacés par une construction à deux-points et parenthèses ; 1 em-dash section "Pourquoi" → deux-points ; 1 em-dash section "Rôle IA" → deux phrases distinctes avec virgule. Ton plus naturel, moins "généré".
v1.20.0 — Juin 2026
  • ·Refonte typographie arabe — Amiri → Readex Pro partout — décision de simplification : Amiri jugée trop lourde en usage courant. Nouvelle règle unique : Readex Pro (font-ar-display + font-arabic + .section-ar → tous redirigés vers var(--font-readex)) pour tout texte arabe sur le site. Seule exception conservée : le logo "زيدنا عامية" dans la Navbar (classe .zeedna-brand-name, Amiri inchangée). Ajustement poids sur WordCard (mot arabe 32px) : font-boldfont-normal. Polices des champs d'input arabes (SuggestEdit, FieldProposal) : var(--font-arabic)var(--font-readex)
v1.19.1 — Juin 2026
  • ·Fix Cormorant Garamond poids non déclaréfont-translit ne fixait que font-family via Tailwind, laissant font-weight: 400 hérité du body. Le navigateur devait résoudre le @font-face 300 italic par matching implicite (parfois instable selon le moteur). Correction : règle CSS .font-translit { font-weight: 300; font-style: italic } ajoutée dans globals.css hors de tout @layer (priorité cascade > utilities Tailwind) → match exact et déterministe sur le @font-face chargé par next/font/google
v1.19.0 — Juin 2026
  • ·Chantier typographie — translittération — Cormorant Garamond 300 italic (--font-cormorant, font-translit Tailwind) remplace font-mono/font-sans pour toutes les translittérations : fiche mot hero, WordCard, SynonymList, WordFamily, PatternMatches, DeclinaisonsSection, WordMorphology (tables conjugaisons + formes dérivées), SearchBar (modes arab, fr, latin), hero landing (cartes featured + mot du jour)
  • ·Chantier typographie — polices arabes — WordCard : mot arabe 32px passe de Readex Pro à Amiri (arabic-text font-ar-display) — uniformisation avec la règle display vs UI déjà appliquée aux autres composants
  • ·Fix tri "Mots récents" — sort parseInt(b.id)new Date(b.created_at).getTime() (les IDs sont des chaînes TEXT, pas des entiers, ce qui donnait un ordre aléatoire)
v1.18.2 — Juin 2026
  • ·Corrections hero landing — per-image objectPosition sur HeroSlideshow (desert-1 : center 80%, desert-2 : center 65%, desert-3 : center 70%, tataouine : center 35%) + quality={90} pour limiter l'artefact JPEG sur les images claires ; section CTA Contribuer supprimée de la landing (redondante avec les boutons dans le Hero)
  • ·Contenu /a-propos enrichi — 5 nouvelles sections : "Pourquoi ce dictionnaire" (problème des 400M locuteurs sans ressource structurée), "Ajouter un mot en 30 secondes" (guide en 3 étapes numérotées), "Ce que fait l'IA" (rôle de Gemini 2.5 Flash + validation humaine), "De la soumission à la publication" (4 étapes du pipeline), "Feuille de route" (QCM ✓, racine ✓, dialectes, audio, mobile)
v1.18.1 — Juin 2026
  • ·Hero → diaporama d'imagesHeroVideo remplacé par HeroSlideshow sur la landing page : 4 images en public/hero/ (dunes, coucher de soleil, campement berbère, troglodytes de Tataouine), crossfade CSS opacity 1.5s automatique toutes les 7 secondes, next/image avec fill + sizes="100vw" + priority sur la première image, overlay gradient légèrement renforcé pour assurer le contraste sur les images claires (notamment le troglodyte blanc)
  • ·Réorganisation sections landing — nouvel ordre : Hero → Articles récents → Mot du jour + Grille des dialectes → Mots récents → CTA Contribuer
  • ·Vidéo → /a-proposHeroVideo (3 vidéos en fond) déplacée en arrière-plan de la section introductive de /a-propos avec overlay sombre rgba(10,7,3,.65/.55) pour la lisibilité du texte
v1.18.0 — Juin 2026
  • ·Refonte landing page — réalignement complet sur le mockup de référence : nouvel ordre des sections (Hero → Mot du jour + Grille des dialectes → Mots récents → Articles récents → CTA Contribuer) ; ancienne section "Dialectes du monde arabe" supprimée
  • ·Mot du jour — nouvelle carte fond #161008 avec pattern SVG losanges (opacité 3%), sélection pseudo-aléatoire stable par jour (eligible.sort(id).at(dayIndex % n)), badge dialecte pill, mot arabe Amiri clamp(52px, 7vw, 96px), séparateur ZeednaDivider, traduction DM Serif Display 38px #E8C460, encadré exemple avec bordure gauche dorée (arabe + traduction FR italique), bouton "Voir la fiche complète" + lien partage Twitter (server-safe, pas de window.location)
  • ·Grille des dialectes — 3×2 : top 5 dialectes par nombre de mots + 1 carte pointillée "+N autres" ; chaque carte avec emoji drapeau, nom FR, nom arabe, lettre watermark, compteur de mots
  • ·ArticleCard variant photo — image fixe 160px, gradient sombre overlay, badges catégorie + dialectes en overlay bas-gauche, titre arabe conditionnel, titre FR 17px DM Serif Display, excerpt, date + "Lire →" ; landing utilise désormais variant="photo" sur les 3 articles récents
  • ·getRecentPublishedArticles — champ titre_ar ajouté au SELECT (était absent, nécessaire pour le variant photo)
v1.17.2 — Juin 2026
  • ·Fix 500 OG image (root cause)/mot/[slug]/opengraph-image : await r.arrayBuffer() manquant dans loadFont() (rejet non capturé par le try/catch) ; new ImageResponse(...) enveloppé dans try/catch avec fallback ; footer remplace le texte arabe example (crash Satori : fontStyle:'italic' sans police italique chargée) par example_translation_fr (français, pas de style italique) ; null guards ?? '' sur tous les champs word
v1.17.1 — Juin 2026
  • ·Fix egress Supabase (Fair Use Policy)/articles/[slug] et /mot/[slug] repassent en ISR (revalidate=3600) au lieu de force-dynamic ; /racine/[racine] : suppression de force-dynamic (revalidate=60 inchangé) ; les 3 routes bénéficient de revalidatePath côté admin pour propagation immédiate après édition
  • ·Fix 500 sur /mot/[slug]/opengraph-imagegetWordBySlug enveloppé dans .catch(() => null) ; aussi parallélisation font+DB fetch via Promise.all
  • ·Cache-Control sur API publiques/api/racines/count (s-maxage=60) et /api/words/[slug] (s-maxage=300) : les réponses 200 sont désormais cachées par le CDN Vercel
  • ·Nettoyage — logs de diagnostic console.log retirés de /racine/[racine] et /articles/[slug]
v1.17.0 — Juin 2026
  • ·Titre arabe sur les articles — colonne titre_ar (nullable) surfacée sur 3 points : page détail article (titre Amiri doré #E8C460 au-dessus du titre français DM Serif Display blanc cassé), cartes ArticleCard (ligne arabe #C9A84C en petit au-dessus du titre FR, sur landing et /articles), formulaire admin (champ dir="rtl" optionnel) ; APIs PATCH + POST mises à jour
v1.16.5 — Juin 2026
  • ·Refonte /a-propos — design system Qirtâs × Amiri — corners rounded-2xl--z-radius-sm (2px) sur les cartes statistiques et la carte de présentation ; fonds → --z-bg-surface ; valeurs statistiques et h1 → font-fr-display (DM Serif Display) ; texte de présentation → font-fr-ui (DM Sans)
v1.16.0 — Juin 2026
  • ·Upload d'image de couverture par article — nouvelle route POST /api/admin/articles/upload-image : upload vers le bucket Supabase Storage "article-images" (service_role), chemin {slug}/{timestamp}-{filename}, validation type (jpg/png/webp) + taille (5 Mo), suppression de l'ancienne image si remplacement
  • ·Formulaire admin enrichi — le champ "Image URL" de l'onglet 📝 Articles se transforme en vrai upload avec bouton "Choisir une image", preview live de l'image, fallback texte pour URL manuelle, indicateur de chargement, gestion des erreurs
  • ·Section "Derniers articles" sur la landing page — 3 articles les plus récents (status='published') affichés en grille après "Derniers mots ajoutés" ; la section est masquée si aucun article n'est publié
  • ·ArticleCard extrait en composant partagé src/components/ArticleCard.tsx (utilisé par /articles, la landing, et extensible) ; formatArticleDate() exportée
  • ·getRecentPublishedArticles(n) ajouté à src/lib/db.ts
v1.15.1 — Juin 2026 (hotfix)
  • ·Fix logo — nouveau SVG simplifié (5 éléments, viewBox 44×72, currentColor) remplaçant l'ancien encensoir complexe dans ZeednaLogo.tsx et public/logo.svg (favicon)
  • ·Fix polices — variables CSS des fontes (--font-amiri, --font-readex, --font-dm-serif, --font-dm-sans) déplacées de <body> vers <html> dans layout.tsx — pattern officiel Next.js/next-font, garantit que les variables sont disponibles dès la racine de la cascade CSS
v1.15.0 — Juin 2026 (redesign)
  • ·Redesign Qirtâs × Amiri — nouvelle direction visuelle sur toutes les pages principales
  • ·Typographie : Amiri (arabe, titres décoratifs), Readex Pro (arabe UI), DM Serif Display (titres français), DM Sans (corps français)
  • ·Design tokens CSS --z-* : palette dorée/terre/sable, dark-first (:root sombre, .light override), bordures carrées (--z-radius-sm: 2px, --z-radius-md: 3px), pattern géométrique SVG sur le fond
  • ·Composants : FlagSVG (22 drapeaux SVG inline + icône MSA), DialectBadge, ZeednaDivider, ScoreRing
  • ·Pages redessinées : homepage (/), dictionnaire, fiche mot, articles, QCM listing et détail
  • ·Composants partagés : WordCard, SynonymList, WordFamily, PatternMatches, Navbar, DialectFilter
  • ·Branch : redesign-qirtas-amiri (à merger sur main)
v1.14.4 — Juin 2026 (fix)
  • ·Fix 404 persistant sur /articles/[slug] pour les articles nouvellement publiés — cause : le cache ISR (revalidate = 3600) pouvait mémoriser une réponse 404 si quelqu'un visitait l'URL avant publication (article encore en draft). Après publication via /admin, le cache n'était pas invalidé → la 404 restait servie pendant 1 heure
  • ·Fix : ajout de revalidatePath('/articles/{slug}', 'page') + revalidatePath('/articles', 'page') dans PATCH /api/admin/articles/[id] (édition/publication) et POST /api/admin/articles (création directement publiée). Dès qu'un article est publié ou modifié via l'admin, le cache ISR de sa page est immédiatement invalidé — prochaine visite re-génère la page depuis la DB
  • ·La structure de /articles/[slug]/page.tsx était déjà correcte (dynamicParams = true, generateStaticParams = []) — la solution ciblait uniquement l'invalidation de cache au bon moment
  • ·Audit des autres routes dynamiques : /mot/[slug] (dynamicParams = true, revalidate = 60, generateStaticParams = []) et /racine/[racine] (dynamicParams = true, revalidate = 60, force-dynamic) sont correctes — pas de correctif nécessaire
v1.14.3 — Juin 2026 (hotfix)
  • ·Fix réel du 500 persistant sur /racine/[racine] — le fix DB de v1.14.2 (overloads RPC dupliqués) était nécessaire mais pas suffisant : le 500 a continué en production après coup (x-matched-path: /500, confirmé par requête HTTP directe sur zeednalearn.com), preuve que le crash venait du code Next.js, pas de la base (la RPC get_words_by_racine_normalized retourne des résultats corrects en appel direct)
  • ·Cause : /racine/[racine]/page.tsx est la seule route du projet combinant un segment dynamique ([racine]) avec searchParams lus en page + en generateMetadata, sous ISR (revalidate = 60, dynamicParams = true), sans export const dynamic = 'force-dynamic'. /articles/page.tsx (searchParams seul, pas de segment dynamique) a déjà ce flag avec le commentaire // searchParams = dynamic — seule la combinaison segment dynamique + searchParams n'était pas couverte. /mot/[slug] (segment dynamique, pas de searchParams) et /racines (searchParams, pas de segment dynamique) fonctionnent tous les deux normalement en prod, validant que c'est bien cette combinaison précise qui posait problème
  • ·Fix : ajout de export const dynamic = 'force-dynamic' sur /racine/[racine]/page.tsx
  • ·Instrumentation ajoutée (diagnostic, conservée) : console.log('[racine] params reçus:', racine, dialect) avant l'appel RPC (page + generateMetadata), getWordsByRacineGroup() désormais entourée d'un try/catch avec console.error('[racine] erreur RPC:', error) — l'appel n'était auparavant protégé par aucun try/catch, laissant toute erreur remonter telle quelle au handler 500 générique de Next.js sans log applicatif
v1.14.2 — Juin 2026 (hotfix)
  • ·Fix critique 500 Internal Server Error sur /racine/[racine] — les migrations add_dialect_filter_to_get_words_by_racine_normalized (v1.14.1) et add_dialect_filter_to_search_racine_groups (v1.14.0), qui ajoutaient un paramètre p_dialect via CREATE OR REPLACE FUNCTION, ont chacune créé un overload Postgres dupliqué au lieu de remplacer la fonction existante (signature de paramètres différente ⇒ Postgres traite ça comme une nouvelle fonction distincte plutôt qu'un remplacement). PostgREST se retrouvait avec 2 fonctions du même nom et ne pouvait plus choisir laquelle appeler ⇒ erreur d'ambiguïté côté Supabase, remontée en 500 générique côté Next.js
  • ·Diagnostiqué par introspection directe (pg_proc/pg_get_function_identity_arguments) — confirmé 2 overloads pour get_words_by_racine_normalized (p_racine seul vs p_racine, p_dialect) et pour search_racine_groups/search_racine_groups_latin (p_q, p_limit vs p_q, p_limit, p_dialect)
  • ·Fix : DROP FUNCTION IF EXISTS ciblé sur l'ancienne signature pour les 3 fonctions (migrations drop_old_single_arg_get_words_by_racine_normalized et drop_old_two_arg_search_racine_groups_overloads), ne laissant qu'un seul overload (le bon) par fonction. Vérifié par ré-introspection + appel RPC direct retournant des résultats corrects
  • ·Aucun changement de code applicatif — fix base de données uniquement, actif immédiatement sans redéploiement
  • ·Règle à retenir pour toute future migration RPC additive : après un CREATE OR REPLACE FUNCTION qui change la liste de paramètres d'une fonction existante, toujours vérifier via pg_proc qu'il ne reste qu'un seul overload — tsc/npm run build ne détectent pas ce genre de régression côté base de données
v1.14.1 — Juin 2026
  • ·Masquage du "#" à l'affichage des racines — le placeholder CamelTools (lettre faible non résolue) ne doit jamais être visible côté utilisateur, seulement structurel en base. Nouveau src/lib/racine.ts : sanitizeRacineDisplay() (remplace # par le tatwil arabe ـ) et formatRacineWithSeparators() (ex-formatRacine(), dupliquée dans WordMorphology.tsx/WordFamily.tsx, désormais centralisée). Appliqué partout où une racine est affichée : /racine/[racine], /racines, RacineSearchBar, section "Morphologie" et "Famille de mots" sur /mot/[slug]. Les valeurs utilisées pour la navigation/les requêtes DB (href, encodeURIComponent, RPC) restent la racine brute, jamais sanitizée — seul le rendu visuel change
  • ·Propagation du filtre dialecte sur /racine/[racine] — la page de détail d'une racine ignorait jusqu'ici le dialecte sélectionné en amont (mots de tous les dialectes mélangés même après un filtre TN sur /dictionnaire). RPC get_words_by_racine_normalized(p_racine, p_dialect DEFAULT NULL) (migration add_dialect_filter_to_get_words_by_racine_normalized) — nouveau paramètre optionnel additif, rétro-compatible. src/lib/db.ts : getWordsByRacineGroup(racine, dialect?) transmet p_dialect
  • ·?dialect=XX propagé dans les liens vers /racine/[racine] depuis RacineSearchBar (suggestions) et /racines (cartes), comme déjà fait pour /racines elle-même
  • ·/racine/[racine] affiche un badge "Filtré : {dialecte}" (même style que celui de /racines) avec bouton de retrait quand un filtre est actif ; generateMetadata reflète aussi le filtre dans le compte de mots
v1.14.0 — Juin 2026
  • ·Filtre dialecte sur l'onglet "Par racine" de /dictionnaire — auparavant seul l'onglet "Par mot" avait un filtre par dialecte ; "Par racine" cherchait toujours toutes les racines tous dialectes confondus
  • ·RPC search_racine_groups(p_q, p_limit, p_dialect DEFAULT NULL) et search_racine_groups_latin(p_q, p_limit, p_dialect DEFAULT NULL) (migration add_dialect_filter_to_search_racine_groups) — nouveau paramètre optionnel additif, p_dialect IS NULL préserve le comportement existant pour tout appelant non modifié. GET /api/search-racine lit ?dialect= et le transmet. get_words_by_racine_normalized (page /racine/[racine]) volontairement inchangée — hors scope, cette page affiche déjà les mots groupés par dialecte
  • ·RacineSearchBar.tsx : nouvelle prop dialect?: string, incluse dans l'URL de fetch et les deps du debounce
  • ·dictionnaire/page.tsx (mode "Par racine") : bloc "Filtrer par dialecte" (DialectFilter, même composant que le mode "Par mot") au-dessus de RacineSearchBar — pas de filtre type de mot (pos_type n'a pas de sens au niveau racine, plusieurs mots d'une même racine peuvent avoir des pos_type différents). Sélectionner "MSA" ne retourne que les racines ayant au moins un mot classique, utile pour isoler les racines classiques pures
  • ·src/lib/db.ts : getAllRacinesGrouped(dialect?: string) — filtre .eq('dialect', ...) conditionnel, alimente à la fois le compteur et /racines
  • ·GET /api/racines/count accepte désormais ?dialect=. Le compteur "Parcourir toutes les racines (N)" se met à jour à chaque changement de dialecte en mode "Par racine"
  • ·/racines?dialect=XX — le filtre se propage depuis le lien "Parcourir toutes les racines" : la page liste alors uniquement les racines du dialecte sélectionné (titre/description SEO adaptés, badge de filtre actif avec bouton de retrait)
v1.13.0 — Juin 2026
  • ·Parcours alphabétique de toutes les racines — nouvelle page /racines listant les 922 racines distinctes du dictionnaire, groupées par lettre initiale selon l'alphabet arabe traditionnel (alefbaa hijaiyya), pas l'ordre Unicode brut. Complément à la recherche libre par racine existante (RacineSearchBar//api/search-racine//racine/[racine]), aucun des trois n'est modifié
  • ·src/lib/arabicAlphabet.ts (nouveau) — ARABIC_ALPHABET_ORDER, normalizeArabicLetterForSort() (variantes alef/hamza regroupées pour le tri), getArabicLetterRank(), sortByArabicAlphabet()
  • ·src/lib/db.ts : getAllRacinesGrouped() — agrégation côté JS (pas de nouvelle RPC/migration, volume vérifié avant implémentation : 1545 lignes, largement suffisant pour un GROUP BY applicatif)
  • ·/racines (ISR 1h, generateMetadata) — nav rapide par lettre, sections groupées, cartes racine cliquables, section "Non classées" pour les racines # non résolues
  • ·GET /api/racines/count (nouveau) — sert le badge dynamique "Parcourir toutes les racines (N)" ajouté sous RacineSearchBar sur /dictionnaire (mode "Par racine")
v1.12.0 — Juin 2026
  • ·Module QCM (configuration + structure) — nouvelle section quiz du site, branchée sur les tables Supabase déjà créées (quizzes, quiz_questions, quiz_answers, RLS déjà activé côté Supabase). Aucun contenu de quiz réel créé : tant qu'aucun quiz status='published' n'existe en base, /qcm affiche un état vide "QCM à venir 🕊️" avec liens retour dictionnaire/articles
  • ·Nouveaux types Quiz/QuizQuestion/QuizAnswer + unions QuizLanguage/QuizCategory/QuizDifficulty/QuizStatus/QuizSource/QuizQuestionType (src/types/index.ts), et related_quiz_category/related_quiz_dialect ajoutés à Article
  • ·src/lib/db.ts : getPublishedQuizzes(filtres), getQuizBySlug(slug) (quiz + questions + réponses imbriquées triées par order_index), getQuizForArticle(category, dialect?), countPublishedQuizzes()
  • ·GET /api/qcm (filtres category/dialect/difficulty/language) — utilisé par la page listing et par le badge "Bientôt" de la Navbar
  • ·/qcm (listing, filtres style /dictionnaire, grille de cartes ou état vide) + /qcm/[slug] (page quiz, ISR 60s, generateMetadata dynamique) + composant client QuizPlayer (une question à la fois, single/multiple, feedback immédiat + explication, score final, support RTL si language='ar')
  • ·Navbar.tsx : lien "QCM" entre Articles et Guide phonétique, badge doré "Bientôt" tant que countPublishedQuizzes() === 0
  • ·/admin : nouvel onglet "🧩 QCM" (QuizzesTab) — liste tous statuts, création/édition de la structure (titre/slug auto/description/langue/catégorie/dialecte si catégorie dialectale/difficulté/statut), toggle publier/dépublier, suppression. Pas de formulaire questions/réponses pour l'instant. Nouvelles routes GET/POST /api/admin/quizzes + GET/PATCH/DELETE /api/admin/quizzes/[id] (mêmes patterns checkAdminAuth/supabaseAdmin/logAction que /api/admin/articles)
  • ·/articles/[slug] : si related_quiz_category est rempli, recherche un quiz publié correspondant (getQuizForArticle) et affiche un encart doré "Teste tes connaissances sur ce thème →" vers /qcm/{slug} ; rien si aucun quiz trouvé
v1.11.0 — Juin 2026
  • ·Recherche par racine en écriture arabe ET en translitération — extension de la recherche par racine (v1.10.0) : l'utilisateur peut désormais taper د.ع.م ou d3m indifféremment dans RacineSearchBar
  • ·Nouvelle colonne dialect_forms.racine_latin (TEXT, migration add_racine_latin_column_and_search_rpc) — translittération Maghreb chiffrée de racine (points retirés), même convention que form_latin (3/7/9/5/gh/dh/T…)
  • ·scripts/maghreb_translit.py : nouvelle fonction racine_to_latin() — port direct caractère-par-caractère de translateToLatin() (src/lib/transliteration.ts), sans logique chadda/diacritiques (une racine n'en contient jamais)
  • ·scripts/backfill_racine_latin.py (nouveau) — backfill ponctuel one-shot : échantillon de 20 lignes toujours affiché avant écriture (dry-run par défaut, --apply + confirmation explicite OUI), lots de 50. Exécuté en prod : 1540/1544 mots avec racine ont désormais racine_latin (4 racines non convertibles laissées NULL, mêmes garde-fous que le reste du pipeline racine)
  • ·3 points d'écriture de racine mis à jour pour maintenir racine_latin en cohérence : publishContribution.ts (calcule racine_latin via translateToLatin() à la publication), scripts/extract_racine_batch.py et scripts/reconcile_racine_hash.py (calculent racine_latin via racine_to_latin() à chaque écriture de racine)
  • ·Nouvelle RPC SQL search_racine_groups_latin(p_q, p_limit) (migration add_racine_latin_column_and_search_rpc) — même forme TABLE(racine, word_count) que search_racine_groups(), mais ILIKE sur racine_latin ; GET /api/search-racine dispatch entre les deux RPC via isArabicText(q) (déjà utilisée par /ajouter) — la racine retournée reste toujours en arabe, même quand le match s'est fait via le latin
  • ·RacineSearchBar.tsx : dir du champ devient dynamique (rtl/ltr selon isArabicText(query)), placeholder mis à jour pour mentionner les deux formats
  • ·Aucun changement sur /racine/[racine] (le param de route reste la racine arabe encodée, déjà géré par le decodeURIComponent défensif de v1.10.0) ni sur l'affichage (racine toujours montrée en arabe avec points)
v1.10.0 — Juin 2026
  • ·Recherche par racine trilitère/quadrilitère — nouveau mode de recherche en complément de la recherche par mot, sans toucher à l'existant (SearchBar.tsx, /api/search, fetchWords de dictionnaire/page.tsx inchangés)
  • ·2 nouvelles fonctions SQL Postgres (migration add_search_racine_rpc_functions) : search_racine_groups(p_q, p_limit) (suggestions, GROUP BY racine + COUNT) et get_words_by_racine_normalized(p_racine) (liste complète) — toutes deux comparent REPLACE(racine, '.', '') des deux côtés (réutilise l'index idx_dialect_forms_racine_normalized déjà en place) ; nécessaire car supabase-js/PostgREST ne sait filtrer ni grouper sur une expression calculée sans RPC
  • ·src/lib/db.ts : nouvelle fonction getWordsByRacineGroup(racine) — distincte de getWordsByRacine() (qui reste inchangée, utilisée par la section "Famille de mots" de /mot/[slug])
  • ·GET /api/search-racine?q=<arabe partiel>{ ok: true, data: [{ racine, count }] }, limite 10, q vide → réponse immédiate sans appel DB
  • ·Nouvelle page /racine/[racine] (ISR 60s, comme /mot/[slug]) : mots groupés par dialecte (ordre TN, MA, DZ, EG, LB, MSA puis alphabétique), cartes WordCard réutilisées telles quelles, titre racine en grand format gold (#C9A84C), état vide avec lien retour /dictionnaire. Décodage défensif (decodeURIComponent avec fallback try/catch) du param de route — bug trouvé et corrigé en test local : sans ce décodage explicite, le param de route arrivait parfois encore encodé (%D8%AF...) au lieu d'être auto-décodé par Next.js, donnant 0 résultat malgré des mots existants en base
  • ·Nouveau composant src/components/RacineSearchBar.tsx, autonome et séparé de SearchBar.tsx (mêmes idiomes : debounce 380ms, onMouseDown preventDefault anti-race condition blur/click) — navigue directement vers /racine/[racine] au clic sur une suggestion
  • ·dictionnaire/page.tsx : toggle additif "Par mot" / "Par racine" au-dessus de la barre de recherche — en mode racine, affiche RacineSearchBar à la place de SearchBar et masque filtres + grille (recherche racine = navigation directe, pas de listing inline)
  • ·mot/[slug]/page.tsx + WordFamily.tsx : lien "Voir tous les mots de cette racine →" vers /racine/[racine], affiché seulement si la racine compte plus de 10 mots (fetch avec limit=11 au lieu de 10 pour détecter le dépassement, signature de getWordsByRacine() inchangée — juste l'argument au call site)
v1.9.2 — Juin 2026
  • ·Fix pré-remplissage /ajouter selon la langue tapée : le paramètre ?mot= (envoyé par SearchBar.tsx et dictionnaire/page.tsx quand une recherche ne donne aucun résultat) était toujours injecté dans le champ « Mot en arabe », même quand l'utilisateur avait cherché en français/translittération — le mode restait aussi bloqué sur l'onglet arabe, et translateToLatin() était appliqué à du texte non-arabe (résultat incohérent)
  • ·Nouvelle fonction isArabicText(text) (src/lib/transliteration.ts) — détecte la présence d'au moins un caractère dans les plages Unicode "Arabic" (U+0600–U+06FF) et "Arabic Supplement" (U+0750–U+077F), via comparaison de codepoints plutôt qu'une regex à séquences d'échappement \u (contournement d'un souci d'encodage rencontré pendant l'implémentation — fonctionnellement identique)
  • ·ajouter/page.tsx : l'état initial de entryMode et form dérive maintenant de isArabicText(motParam) — texte arabe → champ arabe + onglet arabe (comportement inchangé) ; texte latin/français → champ traduction française + onglet français (nouveau) ; pas de motParam → comportement par défaut inchangé
  • ·not-found.tsx (404 générique) ne transmettait déjà aucun paramètre mot — rien à corriger côté 404
v1.9.1 — Juin 2026
  • ·Scan hebdomadaire automatisé des doublons : .github/workflows/weekly-duplicates-scan.yml lance find_duplicates.py tous les dimanches 9h Paris l'été / 8h l'hiver (cron: '0 7 * * 0' fixe, GitHub Actions ne gère pas le DST nativement — décision assumée plutôt qu'un double-cron + garde-fou horaire) ou manuellement via workflow_dispatch
  • ·scripts/find_duplicates.py : nouvelle option --summary-json — écrit un résumé machine-readable (high_count, review_count, high_groups[]) en plus du rapport texte, pour éviter de re-parser du texte dans le step suivant
  • ·Nouveau scripts/notify_duplicates_telegram.py : lit ce résumé JSON et poste un récap sur le bot Telegram existant ([HIGH]/[REVIEW] + liste des form_ar des groupes [HIGH] si le message reste sous ~3500 caractères, sinon juste les comptes — marge sous la limite Telegram de 4096)
  • ·Le workflow upload aussi report.csv + summary.json en artifact (rétention 90j) pour inspection au-delà des logs Actions
  • ·4 secrets GitHub Actions à ajouter manuellement (Settings → Secrets and variables → Actions, distinct du coffre Vercel) : SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
v1.9.0 — Juin 2026
  • ·Détection de doublons diacritique-insensible : les 2 checks existants (/ajouter temps réel + publishContribution.ts avant publication admin) comparaient form_ar en égalité stricte SQL (.eq()) — deux graphies du même mot ne différant que par le tachkil (ex: عَلَى خَاطِرْ avec sukun final vs عَلَى خَاطِر sans) n'étaient donc jamais détectées comme doublons
  • ·Nouvelle fonction SQL normalize_arabic_diacritics(text) (IMMUTABLE, strip tachkil U+064B-U+0670 + tatweel U+0640 + collapse espaces) + RPC find_duplicate_dialect_form(p_form_ar, p_dialect) — utilisées par les 2 points de contrôle existants à la place du .eq('form_ar', ...) exact (migration appliquée en prod via MCP Supabase)
  • ·src/lib/duplicateDetection.ts : normalizeArabicForDup(), miroir TS de la fonction SQL (même classe de caractères que normalizeSearch() dans search-utils.ts) — référence partagée, la comparaison réelle se fait côté Postgres via la RPC
  • ·Nouveau script scripts/find_duplicates.py (lecture seule) : scanne dialect_forms, groupe par (dialect, form_ar normalisé). Découverte en testant sur la prod : le stripping de tachkil seul produit un nombre significatif de faux positifs sur les mots courts — les voyelles internes (fatha/damma/kasra) distinguent souvent 2 mots réellement différents qui partagent le même squelette consonantique non-vocalisé (ex: بَيِّنْ "expliquer" vs بين "entre"). Ajout d'un tag de confiance par groupe ([HIGH] si translation_fr partage un mot-clé ≥4 lettres entre 2 entrées du groupe, [REVIEW] sinon) pour rendre le rapport exploitable malgré ce bruit attendu. Option --include-weak (rapport séparé, plus bruyant : même translation_fr normalisée, form_ar différent) et --output-csv
  • ·Testé sur la base prod (2035 lignes dialect_forms) : 25 groupes de doublons forts détectés, dont 4 [HIGH] confirmés comme vrais doublons (ex: مِتْمَرِّس/مْتْمَرِّس, يِشْلِق/يِشْلَق) — aucune écriture, script strictement lecture seule
v1.8.11 — Juin 2026
  • ·Fix perf autocomplete SearchBar (toujours lent après v1.8.10 — confirmé par l'utilisateur : "Chercher" rapide, mais les suggestions sous la barre toujours longues) : autocompleteSearch() (/api/search) enchaînait jusqu'à 4 allers-retours réseau séquentiels (passe RPC fuzzy → passe variantes phonétiques → passe fallback JS fuzzy sur 200 lignes → champs secondaires), chacun awaité seulement après résolution du précédent et quasi-systématiquement déclenché pour une requête courte/partielle (typique pendant la frappe). Vérifié via EXPLAIN ANALYZE que chaque requête individuelle ne coûte que 1-35ms côté Postgres — le goulot d'étranglement était le nombre de round-trips, pas l'exécution SQL (même diagnostic que v1.8.10, mais sur la route autocomplete plutôt que searchWords())
  • ·Toutes les passes sont désormais lancées en parallèle dès le départ (Promise.all) au lieu d'être gated séquentiellement sur bucket.length après chaque étape — les passes "fallback" sont toujours récupérées mais leurs résultats ne sont appliqués que si les passes primaires n'ont pas suffi (seul le fetch devient inconditionnel, pas son utilisation)
  • ·2 index GIN trigram supplémentaires sur expression (immutable_unaccent(lower(translation_fr)), immutable_unaccent(lower(form_latin))) pour la RPC search_df_trad (pill "français" de l'autocomplete), non couverte par les index ajoutés en v1.8.10 (index sur colonne brute, pas sur l'expression unaccent) — Seq Scan 35ms → Bitmap Index Scan (BitmapOr) 2ms
  • ·Testé en local après fix : /api/search?mode=autocomplete warm à 80-150ms sur les 3 modes (latin/fr/arab), contre plusieurs secondes avant (la chaîne de 4 round-trips séquentiels, chacun ~150-300ms d'overhead réseau/auth/planning PostgREST, expliquait la lenteur perçue malgré des requêtes individuellement rapides)
v1.8.10 — Juin 2026
  • ·Fix perf recherche /dictionnaire (3-4s par recherche signalées) : EXPLAIN ANALYZE a confirmé des Seq Scan sur form_ar/translation_fr (40-80ms par requête) — ces colonnes n'avaient que des index BTREE classiques (idx_df_translation_fr, idx_dialect_forms_lower_translation_fr), inutilisables pour ILIKE '%...%' (BTREE ne sert qu'au préfixe/égalité, pas "contient"). 5 index GIN trigram ajoutés (form_ar, example, example_fr, fouss7a, translation_fr sur dialect_forms) — même requête re-testée après fix : Bitmap Index Scan, 0.3ms (~130x plus rapide)
  • ·Note honnête : le Seq Scan lui-même ne prenait que 40-80ms côté Postgres, donc les index seuls n'expliquent pas entièrement 3-4s perçues — searchWords() (db.ts) lance 3 requêtes Supabase par recherche (searchDf+searchMsa+searchDecl), dont searchDecl() enchaîne 2 ILIKE puis une 3ᵉ requête dépendante, et /dictionnaire ajoute un 2ᵉ appel HTTP (?limit=1) au premier chargement — l'empilement de round-trips réseau Supabase est probablement le facteur dominant, les index aident surtout à la scalabilité future
  • ·Debounce SearchBar.tsx 300ms → 380ms (était déjà à 300, pas 200 comme supposé)
  • ·Vérifié : pas de scan ILIKE multi-colonnes en OR dans le chemin principal — field (arab/fr/latin) restreint déjà à une seule colonne primaire, et secondarySearch() (autocomplete) n'étend déjà aux champs secondaires que si résultats insuffisants. Aucun changement nécessaire sur ce point
v1.8.9 — Juin 2026
  • ·Fix critique découvert au premier run production de import_msa_frequency.py : Insérés en base : 0 malgré 210 mots correctement enrichis — dialect_forms_source_check n'autorisait que admin|contribution|import_sheets|api|correction, pas ai_enriched (ni ai_generated, déjà utilisé par gemini_generate_msa.py, enrich-words.mjs, generate-msa-words.mjs sans qu'aucune ligne n'ait jamais cette valeur en base — ces scripts n'avaient donc probablement jamais réussi à insérer non plus). Contrainte étendue (migration add_ai_source_values_to_dialect_forms_source_check) pour inclure les deux valeurs. Aucune donnée corrompue : l'échec était atomique par ligne, 0 écriture partielle avant le fix
  • ·Quota journalier Groq (on_demand, 100k tokens/jour) épuisé par inadvertance : le premier run (avant le fix ci-dessus) a consommé ~97k tokens pour enrichir 210 mots qui ont ensuite tous échoué à l'insertion — le coût IA est payé dès l'appel, indépendamment du succès de l'écriture DB. Fenêtre de quota glissante (pas de reset net observé), récupération lente. Un test de validation à 5 mots a confirmé le fix (5 lignes réellement insérées, vérifiées en base) mais a aussi épuisé le reliquat de quota du jour — le run de production suivant (--limit 500) a échoué intégralement (0/500, 34 batches tous en 429 TPD dès le batch 1)
  • ·État au 2026-06-19 fin de journée : 5 mots MSA importés (source='ai_enriched') sur 1947 candidats restants (intersection wordfreq ∩ CAMeL, après exclusion des déjà-en-base). Reprise prévue le lendemain avec --provider groq (quota journalier Groq attendu reconstitué)
v1.8.8 — Juin 2026
  • ·Nouveau script scripts/import_msa_frequency.py : importe des mots MSA fiables croisés depuis deux sources de fréquence indépendantes — wordfreq (pip, top 3000) et CAMeL Arabic Frequency Lists MSA (top 3000) — puis enrichit l'intersection via Gemini/Groq (form_latin, translation_fr, pos_type, pattern, pluriel_ar, feminin_ar, example, example_fr) et insère dans dialect_forms (dialect='MSA', source='ai_enriched', source_detail='frequency_crossvalidated', status='validé', structure calquée sur gemini_generate_msa.py)
  • ·Correction de route vs la source annoncée : le repo CAMeL-Lab/Camel_Arabic_Frequency_Lists ne contient aucun fichier .tsv brut en arborescence (l'URL raw.githubusercontent.com/.../MSA/MSA_freqlist.tsv n'existe pas) — la liste est distribuée via GitHub Releases (MSA_freq_lists.tsv.zip, 69 Mo compressé / 228 Mo / 11.4M lignes). Le script télécharge ce zip une fois (caché dans scripts/.cache/, gitignored) et lit uniquement les 3000 premières lignes du .tsv interne sans jamais l'extraire en entier sur disque
  • ·Dédoublonnage contre la base (words.word_ar + dialect_forms.form_ar WHERE dialect='MSA') insensible aux diacritiques — les entrées générées par gemini_generate_msa.py portent souvent un tashkeel que les sources de fréquence externes n'ont jamais, un match exact aurait laissé passer de vrais doublons
  • ·Garde-fou arabe-only sur pattern/pluriel_ar/feminin_ar (même classe de bug que fouss7a v1.8.3) : détecté en dry-run — Groq (llama-3.3) renvoyait parfois pattern en latin (maf3oul au lieu de مفعول) malgré une consigne textuelle ; corrigé en ajoutant un exemple JSON concret en arabe dans le prompt + un rejet automatique (None, loggué) de toute valeur contenant une lettre latine
  • ·form_ar n'est JAMAIS pris depuis la réponse IA (appariement par index avec la liste source originale uniquement) — le mot est déjà confirmé par double source, on ne fait pas confiance au modèle pour le reproduire fidèlement
  • ·--limit (défaut 50), --provider groq|gemini (défaut groq — évite les 503 fréquents de Gemini), --dry-run (lit + enrichit pour prévisualisation, n'écrit rien)
  • ·Premier dry-run (2026-06-19, --limit 20) : 2229 mots à l'intersection des deux listes de 3000, 277 déjà en base exclus, 1952 candidats restants, 20/20 enrichis sans erreur — écriture en base non lancée, en attente de validation
v1.8.7 — Juin 2026
  • ·Nouveau script scripts/reconcile_racine_hash.py : réconcilie les racines consonantiques contenant un symbole # (lettre faible élidée non résolue par CamelTools, cf. extract_racine_batch.py) en propageant la version déjà résolue trouvée ailleurs en base pour la même racine (mêmes lettres aux positions non-#, n'importe quelle lettre acceptée à la position #) — débloque le regroupement "Famille de mots" entre mots qui partagent en réalité la même racine
  • ·Décision par racine # unique (dédupliquée) : 0 candidat résolu → inchangé, 1 candidat → réconcilié, 2+ candidats différents → ambigu, laissé pour revue manuelle (la plupart des cas ambigus ont 2 positions # simultanées, ex: #.#.ل, donc mécaniquement beaucoup de candidats possibles)
  • ·Recherche de candidats faite côté Python (un seul fetch de toutes les racines déjà résolues, comparées segment par segment) plutôt que par une requête regex Postgres par racine # rencontrée — strictement équivalent fonctionnellement, un seul aller-retour réseau au lieu de 200+
  • ·Dry-run par défaut (lecture seule), --apply + confirmation explicite (input()) requise avant toute écriture, écriture par lots de 50 avec logs de progression
  • ·État au premier dry-run (2026-06-19) sur 334 mots avec racine # (225 racines # distinctes) : 45 racines réconciliables (69 mots impactés), 140 ambiguës, 40 sans correspondance
  • ·Exécuté en base le 2026-06-19 (--apply, confirmation explicite) : 69 lignes mises à jour, 0 erreur — vérifié (334 → 265 mots restants avec #, écart exact de 69 ; spot-check زينز.ي.ن, فاشف.ر.ش)
v1.8.6 — Juin 2026
  • ·Fix régression v1.8.5 : /publier <id> (et /rejeter, /accepter, /refuser, /valider, /annuler) tapé depuis la notif Telegram répondait avec l'aide générique au lieu d'exécuter la commande. Cause : v1.8.5 avait changé le format des notifs de /publier_5a04a (underscore) à /publier 5a04a (espace) — mais l'entité "bot_command" de Telegram ne couvre que [a-zA-Z0-9_]+ et s'arrête au premier espace rencontré. Taper sur le lien n'envoyait donc que /publier (ID tronqué/perdu), qui ne matche aucune regex de routage → fallback aide. Reproduit et confirmé par test direct de la regex de routage ("/publier" → NO MATCH) + vérification SQL (la ligne contrib-...-5a04a existait bien, pending, ILIKE la trouvait correctement — donc le bug était bien côté format de notif/tap, pas côté requête DB)
  • ·src/lib/telegram.ts revient au format underscore (/publier_${shortId}) pour les 3 fonctions de notification concernées — seul format qui survit intact au tap Telegram. Le webhook acceptait déjà les deux formats ([_\s]+), donc la saisie manuelle au clavier avec espace continue de fonctionner
  • ·Logging de diagnostic ajouté côté src/app/api/telegram/webhook/route.ts : chaque branche de routage loggue désormais la commande matchée + l'ID extrait (console.info), et les échecs de requête Supabase dans /publier et /rejeter sont maintenant loggués en console.error au lieu d'être silencieusement avalés
v1.8.5 — Juin 2026
  • ·Fix critique : analyzeMorphology() (extraction pattern/racine post-soumission, v1.8.4) ne produisait jamais de résultat — pattern_suggested/racine_suggested restaient NULL en base sans erreur visible. Cause confirmée par reproduction directe de l'appel Gemini : gemini-2.5-flash réserve par défaut un budget de "thinking" interne qui consommait la quasi-totalité du maxOutputTokens: 256 (thoughtsTokenCount: 242/256 observé), tronquant la réponse en plein milieu du JSON (finishReason: MAX_TOKENS) — extractJson() échouait silencieusement (console.warn seulement, jamais remonté). Fix : thinkingConfig: { thinkingBudget: 0 } (tâche d'extraction simple, pas besoin de raisonnement étendu) + maxOutputTokens relevé à 512 — testé et confirmé en local (finishReason: STOP, JSON complet)
  • ·Tous les console.warn silencieux de analyzeMorphology.ts remontés en console.error (clé API manquante, HTTP non-OK, échec de parsing JSON avec texte brut + finishReason loggés, racine/pattern rejetés) pour rester visibles dans les logs Vercel
  • ·Normalisation de racine : Gemini espace parfois les points séparateurs ("ص . ي . ف") — désormais normalisé en "ص.ي.ف" avant stockage, cohérent avec le format utilisé par scripts/extract_racine_batch.py
  • ·Fix format des commandes Telegram actionnables (/publier, /rejeter, /valider, /annuler, /accepter, /refuser) : src/lib/telegram.ts envoyait /publier_5a04a (underscore), désormais /publier 5a04a (espace) — le webhook (src/app/api/telegram/webhook/route.ts) acceptait déjà les deux formats via regex [_\s]+, seul le texte des notifications a changé
v1.8.4 — Juin 2026
  • ·Extraction automatique de pattern + racine pour chaque nouvelle contribution via /ajouter, en arrière-plan et non-bloquant pour le contributeur — analyzeMorphology() (Gemini 2.5 Flash, prompt dédié) déclenché via after() (Next.js 15+, exécuté après l'envoi de la réponse, contrairement à un fire-and-forget classique que Vercel peut tuer prématurément)
  • ·Garde-fou strict (suite au bug fouss7a/translittération de v1.8.3) : tout résultat contenant une lettre latine (/[a-zA-Z]/) dans pattern ou racine est intégralement rejeté (aucune écriture en base, aucune notification) — même règle appliquée à l'édition admin de ces champs
  • ·Nouvelles colonnes contributions.pattern_suggested / racine_suggested (TEXT) / ia_confidence (FLOAT) — migration add_morphology_suggestion_columns_to_contributions
  • ·Notification Telegram complémentaire silencieuse en cas d'échec/rejet : "🧬 Analyse morphologique automatique" avec schème + racine + % confiance, seulement si l'analyse aboutit
  • ·/admin onglet "En attente" : badge 🧬 Analyse IA (schème + racine + confiance) affiché si présent ; éditable en mode édition avant publication (mêmes garde-fous arabe-only)
  • ·Publication (publishContribution.ts) : pattern_suggested copié dans dialect_forms.pattern seulement si aucun pattern manuel déjà renseigné (ex: import scan OCR prioritaire) ; racine_suggested + ia_confidence copiés dans dialect_forms.racine / racine_confidence (mêmes colonnes que scripts/extract_racine_batch.py, script inchangé)
  • ·/mot/[slug] : racine désormais affichée dans le panneau Morphologie juste après le schème (format lisible "ك - ت - ب"), lecture seule — pas encore de flux de correction dédié (FieldProposal ne supporte pas ce champ)
v1.8.3 — Juin 2026
  • ·Suppression de l'enrichissement IA automatique (debounce 1500ms) sur /ajouter : coûteux en tokens et source d'un bug de mapping de champ confirmé — fouss7a recevait parfois la translittération latine au lieu de l'arabe classique (ex: fouss7a="akala" pour "klaa"), le prompt Gemini ne précisant pas explicitement que ce champ doit rester en écriture arabe alors qu'il est injecté juste après le TRANSLIT_BLOCK (entièrement dédié aux règles de romanisation)
  • ·Tous les champs du formulaire (mot arabe, traduction, translittération, type grammatical, exemple) repassent en saisie 100% manuelle ; mot arabe et traduction française désormais toujours requis (le serveur l'exigeait déjà côté /api/submit, ce n'était plus reflété côté client une fois l'IA retirée)
  • ·Champs pattern/fouss7a/pluriel_ar/feminin_ar retirés du formulaire (jamais éditables manuellement, uniquement remplis par l'IA) — restent nullable côté /api/submit
  • ·Route /api/enrich-word conservée intacte (Gemini + fallback Groq) — sera réutilisée pour l'extraction racine/pattern au moment de la publication admin plutôt qu'en saisie utilisateur
v1.8.2 — Juin 2026
  • ·Nouvelle section "🌳 Famille de mots" sur /mot/[slug] (distincte de "Même schème", les deux coexistent) : jusqu'à 10 mots partageant exactement la même racine consonantique, triés pattern renseigné d'abord puis dialecte/translittération — getWordsByRacine() dans db.ts, composant WordFamily.tsx. Racine affichée sous forme lisible ("ك - ت - ب"). Tri "pattern non-null d'abord" fait en JS (stable) car PostgREST ne supporte pas de CASE direct dans .order()
  • ·racine / racine_confidence ajoutés au type Word et à normalizeDialectForm() (absents jusqu'ici malgré la migration v1.8.1)
  • ·Fix scripts/extract_racine_batch.py : la validation des racines reposait sur une liste noire incomplète (FOREIGN/NOAN/na) — le placeholder CamelTools "NTWS" ("not a triliteral/templatic stem", typique des emprunts/noms propres) passait au travers, plus quelques racines mal formées sans séparateur (حل, ل, م, مزل). Remplacé par une validation de forme (regex 2-4 groupes de lettres arabes séparés par des points) — 19 racines invalides corrigées sur re-run complet (1551 → 1532 racines valides, 0 erreur)
v1.8.1 — Juin 2026
  • ·Extraction de racine consonantique (3-4 lettres) sur dialect_forms via CamelTools : nouvelles colonnes racine (déjà migrée) et racine_confidence FLOAT (migration appliquée), scripts/extract_racine_test.py (Phase 1, échantillon 30 mots, lecture seule) puis scripts/extract_racine_batch.py (Phase 2, batch complet)
  • ·Résolution automatique du symbole "#" (lettre faible élidée détectée par CamelTools) par alignement avec le champ pattern existant — 177 lettres résolues sur 2022 mots
  • ·form_ar utilisé en priorité (fouss7a en fallback) — corrige les cas où un fouss7a mal renseigné en base aurait fait dériver une racine depuis le mauvais texte (détecté en Phase 1 sur 2 mots-tests)
  • ·Résultat batch final (après fix v1.8.2) : 1532 racines trouvées (1209 confiance 0.9, 323 confiance 0.6), 490 introuvables (racine=NULL), 0 erreur d'écriture
v1.8.0 — Juin 2026
  • ·Nouvelle section "Autres mots avec ce schème" sur /mot/[slug] : affiche jusqu'à 8 mots dialectaux partageant exactement le même pattern (schème وزن), cliquables vers leur fiche — getWordsByPattern() dans db.ts, composant PatternMatches.tsx (style cohérent avec SynonymList)
  • ·Section masquée automatiquement si le mot n'a pas de pattern ou si aucun autre mot ne le partage
v1.7.16 — Juin 2026
  • ·Fix : suggérer une correction du mot arabe renvoyait "Erreur serveur." — la contrainte CHECK Postgres suggestions_edit_field_check n'autorisait pas la valeur 'form_ar' (oubli de migration lors de l'ajout du champ en v1.7.15). Contrainte mise à jour en base (translation_fr|pos_type|example|example_fr|form_latin|form_ar)
v1.7.15 — Juin 2026
  • ·Mot arabe (form_ar/word_ar) modifiable via le crayon ✏️ sur /mot/[slug] au même titre que la translittération/traduction/exemple/pos_type — nouveau field='form_ar' dans SuggestEdit, /api/suggest-edit, resolveSuggestion.ts
  • ·Suggestions de correction du mot arabe acceptables/refusables depuis /admin (onglet Suggestions, affichage RTL dédié) ET depuis Telegram via /accepter_<id> et /refuser_<id> (mécanisme générique déjà en place, aucun changement webhook nécessaire)
  • ·Slug jamais régénéré même si le mot arabe change (liens stables, cohérent avec form_latin)
v1.7.14 — Juin 2026
  • ·Fix /dictionnaire : le scroll automatique après recherche atterrissait sur les filtres (eux-mêmes sous le clavier mobile resté ouvert) au lieu des résultats. SearchBar.submit() ferme désormais le clavier (blur du champ) à la soumission, et la cible du scroll est déplacée de la section filtres vers la zone de résultats elle-même (bannière d'erreur + grille/état vide/loading)
v1.7.13 — Juin 2026
  • ·/dictionnaire : scroll automatique fluide vers la section filtres/résultats à chaque recherche soumise (icône recherche ou Enter), avec scroll-mt pour compenser la Navbar fixe
v1.7.12 — Juin 2026
  • ·SearchBar pill (/dictionnaire) : remplacement du bouton "Chercher" texte par une icône Search seule (18px, #C9A84C) dans un bouton rond 36px (tap target 44px), hover rgba(201,168,76,0.15), positionné avant la croix clear
  • ·Vérifié : /dictionnaire n'a aucun état hasSearched/isSearched/searchDone — la SearchBar est déjà rendue de façon permanente (acquis depuis v1.7.7), aucune régression trouvée
  • ·SearchBar non-pill (page d'accueil) : bouton "Chercher" texte inchangé
v1.7.11 — Juin 2026
  • ·SearchBar pill (/dictionnaire) : bouton "Chercher" doré (#C9A84C) à l'intérieur de la barre, visible uniquement quand la query n'est pas vide — texte sur écrans ≥640px, icône Search en dessous
  • ·Suppression du tooltip "Astuce : utilisez les chiffres Maghreb" sous la SearchBar (état showHint, helper looksLikeStandardLatin, useEffect associé retirés)
v1.7.10 — Juin 2026
  • ·Bot Telegram : commandes /valider et /annuler pour traiter les propositions morphologiques (table corrections — pattern, pluriel_ar, feminin_ar, fouss7a, origine_mot, variants) directement depuis Telegram
  • ·Notification proposition morphologique : affiche désormais l'ID court avec les raccourcis /valider et /annuler inline
  • ·Logique accept/reject extraite dans src/lib/resolveFieldProposal.ts et partagée entre /api/admin/corrections/[id] et la commande Telegram
v1.7.9 — Juin 2026
  • ·Bot Telegram : commandes /accepter et /refuser pour traiter les suggestions d'édition (suggestions_edit) directement depuis Telegram, même mécanique que /publier et /rejeter
  • ·Notification suggestion d'édition : affiche désormais l'ID court (6 derniers chars de l'UUID) avec les raccourcis /accepter et /refuser inline
  • ·Logique accept/reject des suggestions extraite dans src/lib/resolveSuggestion.ts et partagée entre /api/admin/suggestions/[id] et la commande Telegram
  • ·/api/suggest-edit : capture l'id de la ligne insérée (.select('id').single()) pour pouvoir le transmettre à la notification
v1.7.8 — Juin 2026
  • ·Bot Telegram : commandes /publier <id> et /rejeter <id> pour modérer les contributions directement depuis Telegram
  • ·Notification nouvelle contribution : affiche désormais l'ID court (5 chars) avec les raccourcis /publier et /rejeter inline
  • ·Bot Telegram : aide /help mise à jour avec les nouvelles commandes
  • ·Fix : tap sur /rejeter <id> dans Telegram n'envoyait que "/rejeter" sans l'ID (l'entité commande Telegram coupe à l'espace) — format /rejeter_<id> sans espace + regex de routage acceptant les deux syntaxes
  • ·Fix critique : /publier ne faisait que contributions.status = 'published' sans jamais insérer dans dialect_forms — le mot n'apparaissait donc jamais dans le dictionnaire. Logique de publication extraite dans src/lib/publishContribution.ts (slug, doublon, lien concept, revalidation ISR) et partagée entre /api/admin/publish et la commande Telegram /publier
v1.7.7 — Juin 2026
  • ·Suppression CAPTCHA arithmétique ("Combien font X + Y ?") de tous les formulaires (SuggestEdit, FieldProposal, VersionForm, UtiliseForm)
  • ·Formulaires secondaires (VersionForm, UtiliseForm) : ajout Turnstile avec réutilisation du token de session (hook useTurnstileSession)
  • ·/dictionnaire : mise en page permanente — SearchBar pill toujours visible, filtres toujours affichés, suppression de la bascule hero/résultats
  • ·Suppression de SearchContext.tsx et de la SearchBar compacte en Navbar
  • ·SearchBar : nouvelles props pill (pill-shaped) et dialect (filtre autocomplete par dialecte actif)
v1.7.6 — Juin 2026
  • ·Recherche étendue aux champs secondaires : example, example_fr, fouss7a, pluriel_ar, feminin_ar, pattern
  • ·Nouveau type MatchField + matchExcerpt dans la réponse autocomplete (extrait 60 chars autour du match)
  • ·6 requêtes ILIKE parallèles pour les champs secondaires, max 3 résultats secondaires par appel
  • ·Dropdown SearchBar : extrait souligné en doré (#C9A84C) sous la ligne principale si match secondaire
  • ·Mobile first : pills scroll horizontal (overflow-x-auto), suggestions min-h-48px, clear button 36px
  • ·Hero dictionnaire : px-4 mobile pour éviter tout débordement sur 390px
v1.7.5 — Juin 2026
  • ·Recherche style Google sur /dictionnaire : hero centré au départ, Navbar compacte après recherche
  • ·SearchContext (React context) pour synchroniser l'état hero/résultats entre la page et la Navbar
  • ·Une seule SearchBar active à la fois : hero (lg, max-w-620px) ou Navbar (sm) selon hasSearched
  • ·La Navbar masque la SearchBar sur /dictionnaire tant qu'aucune recherche n'est lancée
  • ·Mode par défaut "fr" sur le hero, initField fallback corrigé latin → fr
v1.7.4 — Juin 2026
  • ·Double protection anti-bot sur le formulaire /ajouter : Cloudflare Turnstile (principal) + honeypot "website" (complémentaire)
  • ·Turnstile affiché une seule fois par session (token stocké en sessionStorage, rechargé en arrière-plan après chaque soumission)
  • ·Vérification Turnstile côté serveur (/api/submit) avant toute validation métier
v1.7.3 — Juin 2026
  • ·Script de backfill des concepts (scripts/backfill_concepts.ts)
  • ·Relie les mots publiés sans concept_id aux concepts existants par mots-clés communs
  • ·Modes --dry-run et --limit N ; dry-run sur 50 candidats → 12 liens trouvés (527 ancrages)
v1.7.2 — Juin 2026
  • ·Liaison inter-dialectes améliorée à la publication (tryLinkToConcept)
  • ·Matching par mots-clés communs au lieu de la traduction exacte (ex : "Énervé, fâché" ↔ "Énervé, fâché, triste, vexé")
  • ·Insensible aux accents, scoring par nombre de mots-clés partagés
v1.7.1 — Juin 2026
  • ·Fix image OG (Open Graph) : metadataBase pointait sur localhost en production
  • ·Les fiches partagées (WhatsApp / Twitter) affichent désormais bien l'image (domaine zeednalearn.com)
v1.7.0 — Juin 2026
  • ·Section "Dernières nouveautés" sur la page À propos : timeline des 8 dernières versions
  • ·Historique CHANGELOG.md synthétisé (lecture serveur + parsing) + lien vers l'historique complet
v1.6.7 — Juin 2026
  • ·Migration des pluriels depuis "Formes du mot" vers la section Morphologie
  • ·Suppression du doublon pluriel entre les deux sections
v1.6.6 — Juin 2026
  • ·Correction bug HTML entities dans les exemples (apostrophes et guillemets encodés)
  • ·Fix racine dans sanitizeText() — plus d'encodage ' en base
v1.6.5 — Juin 2026
  • ·Ajout dir="rtl" explicite sur les équivalents inter-dialectes
  • ·Garde-fou si form_ar vide sur les synonymes
v1.6.4 — Juin 2026
  • ·Revalidation ISR immédiate après actions admin
  • ·Les fiches mots sont mises à jour instantanément sans attendre le cache
v1.6.3 — Juin 2026
  • ·Crayon contributeurs sur la translittération (form_latin)
  • ·Disponible pour tous les dialectes y compris MSA
v1.6.2 — Juin 2026
  • ·Fix affichage navbar : pills de mode de recherche déplacées dans le dropdown
v1.6.1 — Juin 2026
  • ·Crayon contributeurs sur l'exemple d'utilisation
  • ·Formulaire double champ (exemple arabe + traduction FR)
v1.6.0 — Juin 2026
  • ·Suggestions d'édition par les contributeurs (traduction, genre grammatical)
  • ·Nouveau système de validation admin (onglet 💡 Suggestions)
  • ·Notifications Telegram à l'acceptation
v1.5.6 — Juin 2026
  • ·Refonte moteur de recherche : generateVariants() produit cartésien
  • ·Mapping phonétique : habibi→7abibi, khouya→5ouya, ch↔sh, y↔i, e↔a
  • ·Filtre de mode de recherche (Translittéré / Arabe / Français)
v1.5.5 — Juin 2026
  • ·Fix recherche : résultats conservés sans sélection de suggestion
  • ·Fix filtre dialecte : recherche globale par défaut (plus de TN forcé)
  • ·Fix navigation clavier dans les suggestions (flèches haut/bas)
  • ·Fix mapping phonétique h→7, kh→5
v1.5.4 — Mai 2026
  • ·Bloc morphologique collaboratif (WordMorphology)
  • ·Onglet corrections morphologie dans l'admin
  • ·Féminin et pluriel visibles sur toutes les fiches (tous pos_type)
v1.5.0 — Mai 2026
  • ·Refonte moteur de recherche (normalisation, fuzzy Levenshtein)
  • ·Composant SearchBar avec suggestions intelligentes
  • ·Index SQL unaccent + pg_trgm
v1.4.5 — Avril 2026
  • ·fix(search-bar): onMouseDown={(e) => e.preventDefault()} sur les boutons suggestion — corrige la race condition onBlur/onClick qui empêchait le clic de firer ; timeout onBlur 150→250ms par sécurité
  • ·fix(api/search): HTTP 500 sur query contenant une virgule — la virgule était interprétée comme séparateur de conditions dans la syntaxe .or() de PostgREST ; remplacé par des appels .ilike() parallèles dans searchDecl(), searchDf() (branche arabe) et searchMsa() (branche arabe)
  • ·fix(api/search): le catch global retourne désormais HTTP 200 { suggestions:[], words:[], total:0, error:'search_failed' } au lieu de 500
v1.4.3 — Avril 2026
  • ·feat(search): Recherche floue bidirectionnelle Maghreb chiffré ↔ latin standard
    • ·Fonction SQL IMMUTABLE public.normalize_search(text) : 7/8→h, 9→q, 5→kh, 3→a, 2→'', aa→a, ou→o, ii→i, Dh→d
    • ·Index GIN trigram expression sur dialect_forms.form_latin et words.word_latin
    • ·4 RPCs : search_df_fuzzy, count_df_fuzzy, search_words_fuzzy, count_words_fuzzy
    • ·db.ts : searchDf() et searchMsa() utilisent les RPCs pour les queries latines ; ILIKE direct conservé pour l'arabe et le français
    • ·SearchBar.tsx : hint Maghreb chiffré (affiché une seule fois par session si query latin sans chiffres)
    • ·scripts/test_search.py : smoke test 7 cas (normalize_search local + RPC live)
    • ·npm run test:search ajouté dans package.json
v1.4.2 — Avril 2026
  • ·fix(search): SearchBar utilise word.slug comme source de vérité pour les liens suggestions (jamais transliteration ni form_latin) — fix bug clic résultat → mauvaise URL post-migration Maghreb
  • ·Logique Enter/ArrowUp/ArrowDown refactorisée : mutuellement exclusifs, plus de double-submit
  • ·feat(guardrail): check_translit.py couvre désormais 4 sections : words, form_latin, variants JSONB, synonyms JSONB (champ transliteration uniquement — les slugs synonymes sont exclus)
  • ·CLAUDE.md : note explicite slugs ≠ form_latin + règle frontend
v1.4.1 — Avril 2026
  • ·Guardrail translittération Maghreb chiffré — 0 régression possible sur les nouveaux inserts
  • ·scripts/maghreb_translit.py : module partagé normalize_translit(text, form_ar) + is_conforming()
  • ·scripts/check_translit.py : vérification continue (exit 0 si propre, exit 1 + liste si violations)
  • ·src/lib/transliteration.ts : ء → '2' (hamza dialectale) ; ة → 'a' déjà conforme
  • ·src/lib/translit.test.ts : +14 cas translateToLatin() couvrant ح/ه/ق/ص/ط/ض/ظ/ة/ء/shadda
  • ·src/app/api/enrich-word/route.ts : bloc TRANSLITTÉRATION MAGHREB CHIFFRÉ dans prompts Gemini + Groq
  • ·src/app/api/submit/route.ts : guard regex /[āīūṣḥṭḍẓĀĪŪṢḤṬḌẒʿʾ'qQ]/ → 400 si non-conforme
  • ·src/app/api/admin/publish/route.ts : idem avant INSERT dialect_forms
  • ·scripts/gemini_generate_msa.py : import normalize_translit + fix lowercasing form_latin (préserve S/D/T/Dh)
  • ·scripts/gemini_enrich_batch.py : PROMPT_TEMPLATE + Groq system enrichis avec la table Maghreb complète
  • ·npm run check:translit + npm run test:translit ajoutés dans package.json
v1.4.0 — Avril 2026
  • ·Enrichissement IA via Gemini à la saisie des contributions (/ajouter)
  • ·Nouveau branding 3amia · AI — hero deux colonnes, calligraphie عامية
  • ·Système d'articles avec séries/parties (SerieNav, db enrichi, admin form)
  • ·Script batch enrichissement Gemini (scripts/gemini_enrich_batch.py)
  • ·Versioning visible dans le footer
v1.3.0 — Mars 2026
  • ·Back-office admin complet (contributions, mots publiés, historique)
  • ·Notifications Telegram (webhook + import de mots + /stats)
  • ·Système de synonymes inter-dialectes (concept_id, table concepts)
  • ·Icônes SVG custom (IconSensArtistique, IconPoesie, IconLanguesVivantes…)
  • ·Bot Telegram import de mots multi-lignes
v1.2.0 — Mars 2026
  • ·Formulaire contribution refonte (pays → région, translittération auto)
  • ·Détection doublons temps réel
  • ·Déclinaisons morphologiques (table declensions, DeclinaisonsSection)
  • ·Enrichissement CamelTools (scripts/enrich_cameltools.py)
  • ·Type grammatical pos_type (7 valeurs : ism/fi3l/sifa…)
v1.1.0 — Mars 2026
  • ·Migration Google Sheets → Supabase (1 434 mots MSA + 1 771 formes dialectales)
  • ·Back-office /admin v1 (authentification Bearer, modération contributions)
  • ·SEO generateMetadata sur toutes les pages
  • ·Page /a-propos avec stats live Supabase (ISR 1h)
  • ·Audit log (table audit_log, logAction helper)
v1.0.0 — Mars 2026
  • ·Lancement initial du dictionnaire
  • ·3 000+ mots dialectaux (TN, MA, DZ, EG, LB, SY…)
  • ·Recherche multilingue (arabe, français, translittération)
  • ·Guide phonétique
  • ·Formulaire contribution visiteur