Nouveautés

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

v1.38.9 — Août 2026
  • ·scripts/test_groq_vision.py — conclusion définitive : qwen/qwen3.6-27b non viable sur Groq on_demand : campagne de tests en 5 runs (HTTP 401 → 200 thinking tronqué → 413 TPM → 200 thinking tronqué → 400 "thinking unsupported"). Résultat : aucun mécanisme API ne permet de désactiver le thinking sur ce modèle via Groq — /no_think ignoré, "thinking": {"budget_tokens": 0} → HTTP 400, message système prohibitif → sans effet. Le thinking consomme les 4096 tokens entiers avant d'émettre le moindre JSON (finish_reason: "length" systématique). La limite TPM on_demand (8000) interdit par ailleurs max_completion_tokens > ~6000 (HTTP 413). Le script est conservé à titre documentaire avec les commentaires d'explication. Gemini reste le seul provider OCR viable. Parsing défensif <think>...</think> et strip avant { ajoutés au cas où un futur modèle émettrait un résidu de thinking.
v1.38.8 — Août 2026
  • ·scripts/test_groq_vision.py (script jetable) : test Groq Vision sur une seule image Drive (dossier Tounsi) avec le même PROMPT_VISION que le pipeline principal. Modèle qwen/qwen3.6-27b (Qwen 3.6 27B, multimodal vision, actif sur Groq août 2026 — llama-4-scout et llama-4-maverick dépréciés mars 2026). Auth Drive identique à scan_drive_import.py (priorité GOOGLE_SERVICE_ACCOUNT_JSON). Affiche la réponse JSON brute + parsing des entrées extraites + usage tokens. scan_drive_import.py inchangé.
  • ·.github/workflows/test-groq.yml : workflow GitHub Actions workflow_dispatch uniquement (pas de cron). Secrets : GROQ_API_KEY + GOOGLE_SERVICE_ACCOUNT_JSON. Options filename (image spécifique) et verbose (détails HTTP). Permet de déclencher le test depuis GitHub mobile sans clone local.
v1.38.7 — Août 2026
  • ·scan-import.yml — cron 06h → 09h UTC : le cron quotidien est décalé de 0 6 * * * à 0 9 * * * pour tourner après le reset du quota gratuit Gemini 2.5 Flash (~08h UTC / 00h PST). Le modèle utilisé (gemini-2.5-flash, ~1500 RPD sur le palier gratuit) était déjà le bon — aucun changement de modèle nécessaire.
v1.38.6 — Août 2026
  • ·scan_drive_import.py — prompt Gemini : traduction EN→FR obligatoire : section "TRADUCTION FRANÇAISE — OBLIGATOIRE" ajoutée au prompt Vision. Gemini traduit maintenant la glose anglaise (ex: "clan, tribe") en français ("clan, tribu") quand aucune traduction FR n'est directement visible. translation_fr ne doit jamais être null si un sens est lisible sur l'image. Même règle pour example_fr. Résout le problème de toutes les entrées classées low_confidence alors que Gemini extrayait bien les données (mais en anglais uniquement).
  • ·scan_drive_import.py — seuil low_confidence assoupli : has_required ne vérifie plus que word_ar et translation_fr. example_ar et pos_type absents ne classent plus une entrée en low_confidence — leur absence génère un warning mais la fiche reste extracted_new si les deux champs fondamentaux sont présents.
  • ·scan_drive_import.py — retry Gemini sur quota (429) : au lieu d'un exit immédiat sur 429, le script tente jusqu'à 2 retries avec un backoff progressif (65s, 130s) pour laisser le fenêtrage RPM se réinitialiser. Exit 2 seulement après la 3ème tentative échouée. Constante DELAY_QUOTA = 65 ajoutée.
  • ·Idempotence confirmée : save_processed_id() est appelé par image (pas en batch final) — si Gemini retourne un quota fatal à l'image N, les images 1..N-1 déjà traitées sont bien persistées et le run suivant reprend proprement depuis N.
v1.38.5 — Août 2026
  • ·scan_drive_import.py — fix TypeError insert_staging : row.get('translation_fr', '')[:40] crashait quand Gemini retourne None pour un champ (dict.get(key, default) ne retourne le défaut que si la clé est absente, pas si elle vaut None). Fix : (row.get(key) or '') pour tous les champs texte de l'affichage dry-run (translation_fr, pos_type, duplicate_of_word_ar, duplicate_match_type, status) + (row.get('confidence_score') or 0) pour le champ numérique. Un champ None de Gemini produit maintenant un champ vide (ou low_confidence) sans faire planter le run entier.
v1.38.4 — Août 2026
  • ·scan_drive_import.py — fix Gemini Vision (bloquant) : thinking_config retiré du GenerationConfig — champ non supporté par google-generativeai < 0.9 (CI). Toutes les images échouaient avec "Unknown field for GenerationConfig: thinking_config". max_output_tokens augmenté de 2048 à 8192 pour absorber le budget thinking interne de Gemini 2.5 Flash (cf. gotcha documenté dans CLAUDE.md). Modèle gemini-2.5-flash inchangé.
v1.38.3 — Août 2026
  • ·scan_drive_import.py — Bug 1 (bloquant) : fix syntaxe Drive API v3 dans list_drive_images()parentId='{id}' remplacé par '{id}' in parents (syntaxe v3 correcte). L'ancienne syntaxe générait HttpError 400 Invalid Value sur le paramètre q et empêchait tout listing de dossier.
  • ·scan_drive_import.py — Bug 2 (non bloquant) : la colonne staging_scan_words.drive_file_id n'existait pas en base — le script la lisait pour l'idempotence et échouait silencieusement. Migration SQL créée : supabase/migrations/20260829000001_add_drive_file_id_to_staging_scan_words.sql (ADD COLUMN IF NOT EXISTS drive_file_id TEXT + index). Le code d'insertion existant peuple déjà la colonne (aucun changement script nécessaire pour la partie écriture).
  • ·supabase/migrations/ : nouveau dossier de migrations SQL versionnées dans le repo.
v1.38.2 — Août 2026
  • ·.github/workflows/scan-import.yml : workflow GitHub Actions pour l'import Drive depuis le cloud (PC éteint, déclenchable depuis le téléphone). workflow_dispatch avec inputs dialect (défaut TN), limit (vide = tout), dry_run (défaut true) + cron quotidien 6h UTC (dry_run auto-désactivé). Secrets requis : SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, GOOGLE_AI_API_KEY, GOOGLE_SERVICE_ACCOUNT_JSON. load_dotenv tolère l'absence de .env.local en CI (guard if ENV_FILE.exists() déjà en place).
  • ·CLAUDE.md : section "Run cloud via GitHub Actions" avec tableau des déclencheurs, inputs, secrets requis.
v1.38.1 — Août 2026
  • ·scan_drive_import.py v1.38.1 : authentification Google Drive par compte de service (get_drive_service() réécrite). Ordre de priorité : 1. GOOGLE_SERVICE_ACCOUNT_JSON (env var JSON — container/CI) → 2. scripts/service_account.json ou GOOGLE_SERVICE_ACCOUNT_FILE (fichier local) → 3. OAuth2 interactif (fallback). Compte : zeedna-scan-import@modern-kiln-461906-p7.iam.gserviceaccount.com. Import google.oauth2.service_account ajouté.
  • ·.gitignore : ajout scripts/credentials.json et scripts/service_account.json (fichiers secrets jamais committés).
  • ·CLAUDE.md : section Auth Drive complète avec tableau des 3 niveaux, marche à suivre pour configurer GOOGLE_SERVICE_ACCOUNT_JSON en container remote.
v1.38.0 — Août 2026
  • ·scan_drive_import.py v1.38.0 : idempotence DB complète — au démarrage, les drive_file_id et image_filename déjà présents dans staging_scan_words sont chargés depuis Supabase (load_processed_ids_from_db()), fusionnés avec le cache JSON local. Couvre les 5 pilotes chat_extract_pilot et tout run précédent même dans un nouveau container (sans cache). Filtre par ID ET par nom de fichier.
  • ·CLAUDE.md : nouvelle section « Import Google Drive — scan_drive_import.py » avec règles d'extraction Derja.Ninja, conventions form_latin vs racine_latin, mapping idempotence, migration SQL drive_file_id.
v1.37.5 — Août 2026
  • ·Suppression du formulaire "Proposer une autre version" (src/components/WordContribution.tsx) : le tab version ("Autre version" / "Un autre mot pour la même idée ?") est retiré du composant WordContribution. Les sens s'ajoutent désormais directement via le crayon (SuggestEdit sur translation_fr) avec séparateur virgule, rendant ce formulaire contributeur redondant. Le composant passe de 4 tabs à 3 (je l'utilise, corriger, exemple). Imports orphelins nettoyés : MessageCirclePlus (lucide-react) et translateToLatin supprimés.
v1.37.4 — Août 2026
  • ·Fix CPU — suppression OG dynamique fiches mots : suppression de src/app/mot/[slug]/opengraph-image.tsx (route serverless Next.js déclenchée à chaque crawl des ~3000 fiches, 636 appels/12h, 88% d'erreurs 5XX, consommation CPU Fluid Vercel). Remplacée par une image OG statique unique public/og/mot-default.png (1200×630, fond #0D0A06, texte doré #C9A84C, branding Zeedna). generateMetadata de /mot/[slug]/page.tsx mis à jour : openGraph.images et twitter.images pointent vers /og/mot-default.png ; twitter.card passé de summary à summary_large_image.
v1.37.3 — Août 2026
  • ·Page QCM — refonte hero (src/app/qcm/page.tsx, src/locales/fr.json, src/locales/en.json, public/qcm/) : nouveau header full-width inspiré du Claude Design. Label "Exercices" + titre arabe اِختبر معرفتك + H1 "Teste tes connaissances" en DM Serif Display + description + 3 étapes numérotées en chiffres arabes (١ ٢ ٣) sur bordure dorée. Photo calames filtrée (saturate/brightness/sepia) avec filets d'angle dorés, côté droit desktop. 6 nouvelles clés i18n (qcm.heroLabel, qcm.step1Title/Desc, qcm.step2Title/Desc, qcm.step3Title/Desc), qcm.philosophy raccourci pour correspondre à la maquette.
v1.37.2 — Août 2026
  • ·Page QCM — pitch de la rubrique (src/app/qcm/page.tsx, src/locales/fr.json, src/locales/en.json) : l'en-tête de /qcm explique désormais clairement le but de la rubrique. Ajout d'un encadré doré sous le titre avec 3 nouvelles clés i18n : qcm.philosophy (conviction Zeedna sur la pratique vs. lecture seule), qcm.featureAccount (historique scores/QCM si compte créé), qcm.progressiveNote (transparence sur le module en cours de développement).
  • ·Règle zéro tiret cadratin étendue à tout le site : suppression de tous les dans fr.json et en.json (25 occurrences). Remplacés par virgule ou deux-points selon le contexte. Règle documentée dans CLAUDE.md comme site-wide (précédemment limitée aux articles).
v1.37.0 — Août 2026
  • ·Refonte direction artistique — 3 zones — Implémentation des maquettes Claude Design.
    • ·Hero homepage : système d'overlay "blindé" en 2 couches (couche 1 : backdrop-filter saturate(.5) brightness(.7) blur(2px) — neutralise le média ; couche 2 : dégradé cartouche diagonal sur desktop, vertical sur mobile). Titre refondu : ivoire uni #FDF6E8 sans ombre portée ni italique, filet doré #A47A18 + عامية en Amiri or sous le titre. Label avec tiret doré avant "N mots · N dialectes".
    • ·Footer : nouveau design 4 colonnes — logo encensoir SVG + "Zeedna عامية" (DM Serif Display + Amiri), bloc étymologie border-left ("ZEED + NA : augmente-nous"), colonnes Pages / Le projet / Dialectes avec chips cliquables, barre basse avec © + étoiles décoratives + version. Fond #0A0805, lueurs radiales, filet doré dégradé en tête. Remplace l'ancien footer 3 colonnes avec "3amia" en latin.
    • ·Articles : bouton "Proposer un article" — icône Qalam (calame قلم) SVG gravée remplace l'emoji ✍️. Traits or currentColor 1.6 px, courbe d'encre #A47A18, viewBox 32×32.
v1.36.1 — Août 2026
  • ·Dates de publication et de modification sur les fiches articles (src/app/articles/[slug]/page.tsx) :
    • ·En haut : date de publication dans la byline auteur rendue plus visible (--z-text-secondary au lieu de --z-text-muted).
    • ·En bas (footer article, après le contenu Markdown) : Publié le X toujours affiché ; ✎ Mis à jour le Y affiché uniquement quand updated_at est supérieur de plus d'1 heure à published_at (filtre les touches automatiques à la publication). Helper wasSignificantlyModified().
v1.36.0 — Août 2026
  • ·Double vue articles — Par série / Chronologique (src/app/articles/page.tsx, src/components/SeriesGroupCard.tsx, src/lib/db.ts) — toggle en haut de la liste /articles pour passer entre deux modes d'affichage :
    • ·📚 Par série (défaut) : regroupe tous les articles publiés par serie_slug. Chaque groupe affiche une carte série (image, titre FR/AR dérivé du 1er article, badge "X parties"). Cliquer sur une carte affiche les articles de la série triés par partie_num ASC. Les articles sans serie_slug sont regroupés en "Articles indépendants" (carte unique à la fin). Navigation propre : ?vue=series est le défaut (omis de l'URL), ?serie=<slug> pour le détail, bouton ← retour.
    • ·🕐 Chronologique : liste paginée existante enrichie d'un toggle tri "Plus récents / Plus anciens" (?tri=desc|asc). Filtres catégorie et dialecte uniquement visibles dans cette vue.
    • ·getAllPublishedArticlesSummary() dans db.ts — fetch lean (colonnes minimales) de tous les articles publiés pour le groupement côté JS, ordonné published_at ASC.
    • ·sort?: 'asc' | 'desc' ajouté à getPublishedArticles() — prop transmise au .order() Supabase.
    • ·SeriesGroupCard : variante carte série (180px image, gradient overlay, badge count, badge "Série", titre FR/AR, lien "Voir →").
    • ·URLs propres : vue=series et tri=desc omis (valeurs par défaut), page=1 omis. readSlugs uniquement fetchés quand des articles sont visibles.
v1.35.3 — Août 2026
  • ·Suivi de lecture articles — carte grisée (src/components/ArticleCard.tsx) — les articles déjà lus passent en opacity: 0.45 + filter: grayscale(0.7) au lieu du badge vert "✓ Lu". L'effet s'applique à la carte entière (wrapper <Link>), s'anime en 0.2s.
v1.35.2 — Août 2026
  • ·Fix commentaires : confirmation "en attente de modération" (CommentSection.tsx) — après envoi d'un commentaire (status='pending'), le composant affichait le commentaire dans la liste puis le faisait silencieusement disparaître au rechargement (le GET ne retourne que status='visible'). Fix : l'envoi affiche maintenant une bannière de confirmation dorée "✅ Commentaire envoyé ! Il sera visible après validation par l'équipe." La bannière se réinitialise quand l'utilisateur commence à taper un nouveau commentaire.
  • ·Notifications Telegram commentaires (/api/comments, telegram.ts) — notifyNewComment() notifie le bot à chaque nouveau commentaire soumis, avec un résumé du contenu + commandes /approuver_ID / /modérer_ID.
  • ·UX auth clarification (UserMenu.tsx, auth/login/page.tsx) — bouton "Connexion" renommé "Connexion / Inscription" et titre de la page mise à jour.
v1.35.1 — Août 2026
  • ·Copy : suppression du messaging IA — "enrichi par l'IA" retiré de toutes les pages publiques, remplacé par "enrichi par la communauté". Section "Ce que fait l'IA" supprimée de /a-propos. Metadata descriptions mises à jour en cohérence.
v1.35.0 — Août 2026
  • ·Onboarding wizard + CGU : expérience d'accueil pour les nouveaux utilisateurs et page Conditions d'utilisation.
    • ·src/app/onboarding/page.tsx (nouveau) — wizard de bienvenue en 1 étape : sélection des dialectes maîtrisés (chips avec vrais drapeaux flagcdn.com) + case à cocher CGU + bouton "Commencer l'aventure". Sauvegarde via PATCH /api/profil. Ignorer → redirect direct vers next.
    • ·src/app/api/auth/callback/route.ts — détection nouveaux inscrits : après exchangeCodeForSession/verifyOtp, appel getUser() et calcul de l'âge du compte. Si < 5 minutes → redirect vers /onboarding?next=<destination> au lieu de <destination> directement. Les reconnexions ne déclenchent plus l'onboarding.
    • ·src/app/cgu/page.tsx (nouveau) — page CGU & Politique de confidentialité conforme RGPD : données collectées (email, nom, dialectes, contributions, commentaires, hash IP), finalités, durée de conservation, partage (Supabase EU + Vercel), droits RGPD (accès, rectification, effacement, portabilité), cookies de session, licence CC BY 4.0 des contributions.
    • ·src/app/auth/login/page.tsx — lien CGU ajouté sous le formulaire (remplace la note "Ton email ne sera jamais partagé" → "En continuant, tu acceptes nos CGU & politique de confidentialité").
v1.34.0 — Août 2026
  • ·Notifications in-app + profil dialecte : système de notifications utilisateur et sélection des dialectes maîtrisés.
    • ·DB : table user_notifications (id UUID, user_id UUID, type TEXT, payload JSONB, read BOOL DEFAULT false, created_at) avec RLS. Table user_profiles (user_id UUID PK, dialect_expertise TEXT[], created_at, updated_at) avec RLS. Migration create_user_profiles_and_notifications. Statut pending ajouté au CHECK comments.status (migration add_pending_status_to_comments).
    • ·Modération commentaires : POST /api/comments insère désormais avec status: 'pending' (plus visible directement). PATCH /api/comments/[id] accepte pending|visible|hidden|flagged ; transition pending → visible crée automatiquement une user_notification de type comment_approved avec aperçu du contenu (120 car).
    • ·src/app/api/notifications/route.ts (nouveau) — GET ?count_only=true{ unread_count } ; GET complet → { notifications, unread_count } ; PATCH → marque tout comme lu.
    • ·src/app/api/profil/route.ts (nouveau) — GET profil dialecte ; PATCH sauvegarde dialect_expertise[] (validation contre VALID_DIALECTS).
    • ·src/components/NotificationsPanel.tsx (nouveau) — panneau client liste/temps relatif/badges non-lus/"Tout marquer comme lu" ; liens vers /articles/{id} ou /mot/{id}.
    • ·src/components/DialectEditor.tsx (réécrit) — vrais drapeaux flagcdn.com (<img> 24×18 + srcSet 2x) au lieu d'emojis. Composant DialectFlag avec fallback emoji 📖 pour MSA.
    • ·src/components/UserMenu.tsx — badge rouge non-lu sur avatar, fetchUnread() au montage.
    • ·src/app/profil/page.tsx (réécrit) — chargements parallèles (Promise.all) contributions + profil + notifications. Affiche <NotificationsPanel> puis <DialectEditor>.
    • ·src/app/admin/page.tsx — onglet Commentaires filtré pending par défaut, boutons contextuels "Approuver" / "Rejeter" vs "Rendre visible" / "Masquer", badge de fond distinct pour les commentaires en attente.
v1.33.0 — Août 2026
  • ·Système de commentaires (Phase 2) : commentaires authentifiés sur les articles et les fiches mot, avec modération admin.
    • ·DB : table public.comments (Supabase) — id UUID, user_id UUID REFERENCES auth.users, target_type CHECK(article|word), target_id TEXT, content TEXT CHECK(1–2000), status CHECK(visible|hidden|flagged), created_at, updated_at. RLS : SELECT visible pour tous, INSERT pour les connectés (user_id = auth.uid()), DELETE propriétaire ou admin, PATCH via service_role uniquement. Index sur (target_type, target_id, created_at DESC) et (user_id).
    • ·src/app/api/comments/route.tsGET /api/comments?target_type=X&target_id=Y (public, retourne les commentaires visible, enrichis avec le nom/avatar depuis auth.users) ; POST /api/comments (connecté requis, insère avec supabaseAdmin, retourne le commentaire enrichi).
    • ·src/app/api/comments/[id]/route.tsDELETE /api/comments/[id] (propriétaire ou admin Bearer) ; PATCH /api/comments/[id] (admin Bearer uniquement, change le status).
    • ·src/app/api/admin/comments/route.tsGET /api/admin/comments?status=all|visible|hidden|flagged&page=N (admin Bearer), retourne tous les commentaires enrichis avec pagination.
    • ·src/components/CommentSection.tsx — composant client réutilisable. Anonyme : CTA "Se connecter" avec lien vers /auth/login?next=<path>. Authentifié : formulaire textarea (2000 car max, compteur, envoi) + liste des commentaires avec avatar (photo Google ou initiale), nom, date relative, bouton supprimer (propriétaire).
    • ·src/app/articles/[slug]/page.tsx<CommentSection targetType="article" targetId={article.slug} /> ajouté après l'encart QCM, avec séparateur.
    • ·src/app/mot/[slug]/page.tsx<CommentSection targetType="word" targetId={word.id} /> ajouté avant la section "Articles similaires".
    • ·src/app/admin/page.tsx — onglet 💬 Commentaires (CommentsTab) : filtre par statut, liste des commentaires avec badges colorés (Visible/Masqué/Signalé), actions Rendre visible / Masquer / Signaler / Supprimer.
v1.32.0 — Août 2026
  • ·Authentification utilisateur — Google OAuth + Email Magic Link : couche auth complète basée sur Supabase Auth (@supabase/ssr) greffée sans casser les flux anonymes existants. Tous les champs contributed_by/reported_by restent null pour les anonymes — l'auth est additive.
    • ·middleware.ts (racine) — refresh automatique des cookies de session JWT à chaque requête (pattern obligatoire @supabase/ssr) ; matcher exclut les assets statiques, favicon, manifest.
    • ·src/lib/supabase-browser.tscreateBrowserClient() pour les composants client (auth operations).
    • ·src/lib/supabase-server.tscreateServerClient() async qui lit les cookies Next.js via next/headers ; pattern getAll/setAll pour la gestion des cookies.
    • ·src/lib/auth.ts — deux helpers : getServerUser() (server components, lit les cookies via next/headers) et getUserFromRequest(request) (API routes, parse l'en-tête cookie manuellement).
    • ·src/app/api/auth/callback/route.ts — handler unique pour Google OAuth (codeexchangeCodeForSession) et Email Magic Link (token_hash + typeverifyOtp) ; redirige vers le param next ou /.
    • ·src/components/UserProvider.tsx — context UserCtx { user, loading, signOut }, useUser() hook exporté ; écoute onAuthStateChange pour sync temps réel.
    • ·src/components/UserMenu.tsx — bouton "Connexion" (anonyme) ou avatar + dropdown "Mon profil / Se déconnecter" (authentifié) ; s'adapte au mode transparent de la homepage.
    • ·src/app/auth/login/page.tsx — page connexion : bouton Google OAuth + champ email Magic Link ; design Zeedna (fond --bg, doré --z-gold).
    • ·src/app/auth/confirm/page.tsx — page "Vérifie ta boîte mail" post-Magic-Link.
    • ·src/app/profil/page.tsx — profil utilisateur (force-dynamic) : avatar, nom, email, stats (publiés/total) + liste des contributions avec badges de statut colorés.
    • ·src/app/layout.tsx<UserProvider> ajouté autour de <Navbar> + <main> + <Footer>.
    • ·src/components/Navbar.tsx<UserMenu isTransparent={isTransp} /> inséré entre le toggle thème et le bouton "Ajouter".
    • ·src/app/api/submit/route.ts — bypass rate limit ET Turnstile pour les utilisateurs connectés (identité vérifiée côté Supabase) ; contributed_by: user?.id ?? null.
    • ·src/app/api/correction/route.ts — bypass rate limit pour les connectés ; reported_by: user?.id ?? null.
v1.31.0 — Juillet 2026
  • ·Cartes historiques interactives dans les articles : nouveau composant HistoricalMap (SVG data-driven, palette #0D0A06/#C9A84C/#E8C86A, rose des vents, rhumbs décoratifs, trajet animé via requestAnimationFrame + DOM direct, info panneau latéral, clic sur chaque ville). Token [[carte:ID]] dans le contenu Markdown → rendu <HistoricalMap>. parseBlocks() détecte le token en milieu d'accumulation raw, émet le bloc raw courant puis un bloc { kind: 'carte', id }. ArticleLocaleView propage article.cartes vers ArticleContent. Gazetteer src/lib/gazetteer-mediterranee.ts — 9 villes (Ibérie, Maroc, Algérie, Tunisie) dans le référentiel viewBox 1000×640. Nouveaux types Lieu, Trajet, CarteArticle dans src/types/index.ts + champ cartes?: CarteArticle[] | null sur Article. Colonne cartes JSONB déjà présente en base (pas de migration). prefers-reduced-motion : trajet dessiné immédiatement sans animation. Layout flex-row desktop / flex-column mobile via CSS class (pas inline — inline overriderait les media queries).
v1.30.6 — Juillet 2026
  • ·SEO articles — audit et corrections : (1) sitemap.ts — priority articles 0.70.8 ; (2) robots.tsdisallow: ['/admin', '/api/'] ajouté (était absent) ; (3) generateMetadata sur /articles/[slug] — ajout alternates.canonical, openGraph.url, openGraph.modifiedTime (branché sur updated_at), Twitter card (summary_large_image) ; (4) JSON-LD — ajout dateModified, inLanguage: 'fr', url, publisher.url. lastModified sitemap était déjà sur updated_at (pas de changement). Contenu article rendu côté client (ArticleLocaleView est 'use client') — Googlebot indexe via JS en deux vagues ; metadata et JSON-LD sont bien dans le HTML statique.
v1.30.5 — Juillet 2026
  • ·Fix navbar mobile recouvrant le contenu : le header fixed (64px) n'avait aucun espace réservé côté <main> — le fil d'Ariane et le h1 étaient invisibles au chargement sur petit écran. Fix : variable CSS --z-header-h: 64px dans :root (globals.css) ; padding-top: calc(var(--z-header-h) + env(safe-area-inset-top, 0px)) sur <main> dans layout.tsx (gère aussi la safe-area iOS). Toutes les pages qui compensaient manuellement la navbar (pt-[64px] ou pt-24) ont été mises à jour : pt-[64px] → supprimé, pt-24pt-8 (breathing room conservé). scroll-margin-top des items footnote mis à jour de 80px hardcodé → calc(var(--z-header-h) + 16px).
v1.30.4 — Juillet 2026
  • ·Fix numérotation et premier lien des footnotes : deux bugs dans extractFootnotes() (ArticleContent.tsx) :
    • ·Numérotation par ordre de définition au lieu d'apparition : la fonction assignait les numéros 1, 2, 3… dans l'ordre des blocs [^id]: en bas du document. Si [^4]: était la 4e définition, il s'affichait "4" même s'il était le 1er renvoi rencontré dans le texte. Fix : extractFootnotes passe en deux étapes — (1) collecter les définitions id→markdown sans num ; (2) scanner le contenu DÉPOUILLÉ des définitions pour les références [^id] dans leur ordre d'apparition et assigner les nums séquentiellement 1, 2, 3…
    • ·Premier renvoi sans lien (renvoi orphelin remark-gfm) : si injectFnRefs ne couvrait pas un id (rare edge case), remark-gfm voyait [^id] sans définition et le rendait en <sup>N</sup> sans <a>. Fix défensif : .replace(/\[\^[^\]]+\]/g, '') sur le contenu final après injection — tout [^id] non résolu est supprimé proprement avant le rendu ReactMarkdown.
v1.30.3 — Juillet 2026
  • ·Fix carets "^1^" visibles dans les renvois de footnotes : injectFnRefs() générait le texte du lien sous forme [^{num}^](#fn-{id}) — les carets restaient littéraux dans le rendu. Corrigé en [{num}](#fn-{id}) : le numéro nu est rendu dans le <sup> via AnchorComponent/renderTokens, aucun caret. Les renvois affichent désormais "1", "2", "3" en exposant doré sans caractère parasite.
v1.30.2 — Juillet 2026
  • ·Fix définitif footnotes — implémentation manuelle découplée de parseBlocks : les références [^1] s'affichaient encore en texte brut dans les blocs bilingual (lignes avec <br> rendues par renderInline/renderTokens, jamais par ReactMarkdown — remark-gfm n'y voyait donc rien). La v1.30.1 ne corrigeait que les blocs raw. Fix : abandon total du mécanisme remark-gfm pour les footnotes, implémentation manuelle en trois étapes dans ArticleContent.tsx :
    1. ·extractFootnotes() — extrait les définitions [^id]: texte du contenu COMPLET (avant parseBlocks) via exec en boucle (pas de regex /g partagée — cf. règle v1.22.10), construit une Map<id, {num, markdown}>, strip les définitions du texte source.
    2. ·injectFnRefs() — remplace [^id][n](#fn-id) via split/join (pas de regex /g) sur TOUT le texte avant split ; le lien Markdown standard [n](#fn-id) est reconnu aussi bien par tokenizeInline (blocs bilingual/section) que par ReactMarkdown (blocs raw).
    3. ·Section JSX finale après la boucle de blocs : <section data-footnotes> avec <ol> + <li id="fn-{id}"> + ReactMarkdown par définition + lien retour ↩.
    • ·renderTokens — cas link avec href.startsWith('#fn-')<sup><a id="fnref-{id}" className="article-fn-ref">.
    • ·AnchorComponent — même cas ajouté pour les blocs raw.
    • ·H2Component retiré (plus nécessaire sans remark-gfm footnotes).
    • ·globals.css — sélecteurs mis à jour : .article-fn-ref, .article-footnotes-section, .article-footnotes-list, .article-footnote-item, .article-fn-back.
v1.30.1 — Juillet 2026
  • ·Fix footnotes [^1] rendus en texte brut : parseBlocks() fragmentait le contenu Markdown en blocs indépendants, chacun rendu par une instance ReactMarkdown séparée. remark-gfm exige que la référence [^1] et sa définition [^1]: texte soient dans le même document pour générer un <sup> cliquable — éclatés dans deux blocs différents, ils restaient littéraux. Fix dans ArticleContent.tsx : extraction des définitions de footnotes (regex /^\[\^[^\]]+\]:.*/gm) avant parseBlocks, retrait du contenu pour éviter les doublons, réinjection en queue de chaque bloc raw qui contient au moins une référence [^N]. Les articles sans footnotes sont inchangés (chemin contentForParsing === normalizedContenu).
v1.30.0 — Juillet 2026
  • ·Footnotes / Sources cliquables dans les articles : les marqueurs [^1] GFM sont désormais rendus en exposants cliquables qui scrollent vers la section Sources en bas de l'article, avec lien retour ↩.
    • ·ArticleContent.tsxAnchorComponent : les ancres #fn-… / #fnref-… ne s'ouvrent plus dans un nouvel onglet (fix target="_blank" sur les liens internes à la page). Nouveau composant H2Component : renomme le titre auto "Footnotes" de remark-gfm en "Sources / المصادر" (style label discret, pas de serif géant). Composant h2 branché sur le ReactMarkdown.
    • ·globals.css — styles footnotes : exposant .article-content sup a (couleur --z-gold, taille 0.7em) ; section .article-content [data-footnotes] (filet doré en haut, texte 0.85rem, scroll-margin-top: 80px sur les <li> pour compenser la navbar fixe) ; lien retour [data-footnote-backref] ; heading .article-footnotes-heading (label uppercase discret).
    • ·Fonctionne pour les marqueurs dans les blocs raw (paragraphes français). Limitation connue : marqueurs dans les lignes bilingual (avec <br>) non rendus en superscript (tokenizer inline custom — pas de support [^N]).
    • ·Aucun plugin supplémentaire requis : remark-gfm (déjà installé) gère nativement les footnotes GFM. Pas de rehype-sanitize → les id d'ancre sont préservés sans configuration.
v1.29.2 — Juillet 2026
  • ·Fix texte illisible en thème clair sur les pages article : les couleurs de texte du contenu article étaient hardcodées pour le mode sombre uniquement — illisibles sur fond crème en mode clair.
    • ·globals.css.line-ar, .line-fr, .section-ar, .section-fr : remplacement des valeurs littérales (#FDF6E8, #8A9E78, #E8C460) par les variables CSS de thème --z-text-primary, --z-text-secondary, --z-gold-light. Ajout de --z-gold-light: #7A5A10 dans le bloc .light (en dark il reste #E8C460).
    • ·ArticleLocaleView.tsx — titre arabe (color: '#E8C460'var(--z-gold-light)) et h1 (color: 'rgba(253,246,232,.82)'var(--z-text-primary)) : mêmes couleurs hardcodées converties en variables thème.
    • ·Mode sombre : comportement visuel inchangé (même rendu qu'avant).
    • ·Mode clair : texte article maintenant lisible (quasi-noir #100D08 sur fond crème, doré foncé #7A5A10 pour les titres arabes).
v1.29.1 — Juillet 2026
  • ·Fix tableaux et fusion de lignes (ArticleContent) :
    • ·Fix tableau non rendu (root cause) : parseBlocks() consommait la ligne header du tableau (| Mot | Sens |) comme traduction française d'un heading arabe — la condition nextIsPlainText ne vérifiait pas que la ligne suivante est une structure Markdown. La ligne | Mot vocalisé | Translittération | Sens | passait tous les tests (pas #, pas <br>, pas arabe) et était absorbée dans le bloc section. Le raw block suivant commençait donc par |---|---| sans header row → remark-gfm ne reconnaissait pas de tableau. Fix : nextIsPlainText vérifie maintenant que nextLine ne commence PAS par | (table), > (blockquote), `(code block), - /* /+ (liste non ordonnée), ni \d+. (liste ordonnée). Si la ligne suivante est l'une de ces structures, le heading arabe tombe dans le path "raw" et l'ensemble est passé d'un tenant à ReactMarkdown qui peut rendre le tableau correctement.
    • ·Fix fusion de lignes : ajout du plugin remark-breaks — convertit les sauts de ligne simples (\n) en <br> dans les paragraphes. Les lignes séparées par \n (sans ligne vide) s'affichent maintenant sur des lignes visuellement distinctes. N'affecte pas les tableaux, listes, headings (éléments de bloc — remark-breaks n'agit que dans les paragraphes). Format bilingue AR/FR non affecté (les blocs section/bilingual ne passent pas par ReactMarkdown).
v1.29.0 — Juillet 2026
  • ·Rendu articles amélioré (3 fixes ArticleContent) :
    • ·Tableaux Markdown rendus : les tableaux GFM (| col | col |) s'affichaient en texte brut. Ajout des composants table/th/td dans ReactMarkdown — le tableau est wrappé dans <div style="overflow-x:auto"> pour le responsive mobile, et la direction RTL est appliquée cellule par cellule si le contenu arabe est détecté (isArabicText). Le CSS .article-content table/th/td existant (globals.css) est préservé.
    • ·Apostrophes correctes : s''effondrents'effondrent — micromark (sous react-markdown) convertissait '' en " (smart typography). Fix : normalisation contenu.replace(/''/g, "'") avant parseBlocks, en amont de tout traitement.
    • ·Images inline dans les articles : la syntaxe ![alt](url) dans le corps des articles est désormais rendue (composant img ajouté à ReactMarkdown). Responsive (max-width: 100%, height: auto), lazy-loading natif, border-radius: 8px. La couverture (image_url) et les cartes article étaient déjà gérées (inchangé).
v1.28.0 — Juillet 2026
  • ·Auto-traduction EN à la validation : quand un mot (dialect_forms) est publié depuis l'admin sans translation_en, une traduction anglaise est générée automatiquement via IA juste après la validation.
    • ·Nouvelle lib src/lib/translateWordToEn.ts : translateWordToEn() (fonction pure réutilisable — batch/articles) + autoTranslateDialectFormToEn(dialectFormId) (fetch + update Supabase). Gemini 2.5 Flash principal, fallback Groq llama-3.3-70b sur 503. thinkingBudget: 0 (évite la troncature JSON). Timeout 8s. Idempotent : si translation_en déjà rempli → skip immédiat.
    • ·Non-bloquant : after() dans /api/admin/publish/route.ts et /api/telegram/webhook/route.ts — la réponse admin est envoyée AVANT que l'IA commence. Un échec IA n'annule jamais la validation.
    • ·Contexte IA : form_ar, form_latin, dialect, translation_fr, example, example_fr — la traduction s'appuie sur le FR et l'arabe pour lever l'ambiguïté dialectale. Convention Maghreb chiffré explicitée dans le prompt (ع→3, ح→7, ق→9, خ→5).
    • ·en_auto = true positionné lors de l'écriture (badge "Traduit par IA" admin déjà existant v1.25.2).
    • ·PublishResult enrichi de dialectFormId (champ status: 'published').
v1.27.0 — Juillet 2026
  • ·Sitemap complet + robots.txt :
    • ·src/app/sitemap.ts réécrit : inclut désormais toutes les fiches mot (/mot/[slug], tables dialect_forms + words) ET tous les articles publiés (/articles/[slug]), ainsi que les pages statiques (/, /dictionnaire, /articles, /ajouter, /guide-phonetique). ISR revalidate = 3600 conservé.
    • ·Fix egress : remplace getAllWords() (select('*') sur toute la base) par deux nouvelles fonctions getSitemapWords() / getSitemapArticles() dans db.ts — sélectionnent uniquement slug, updated_at, wrappées dans unstable_cache (revalidation 1h), lancées en parallèle via Promise.all. lastModified basé sur updated_at réel.
    • ·Build fallback préservé : erreur Supabase au build → tableau vide + console.warn ; re-throw au runtime (ISR stale intact).
    • ·src/app/robots.ts créé : autorise tous les crawlers (userAgent: '*', allow: '/'), pointe vers https://zeednalearn.com/sitemap.xml.
    • ·Note : le site utilise une locale cookie (NEXT_LOCALE) sans sous-chemin d'URL (/en/), donc une seule URL existe par page — les articles sans contenu_en ne génèrent pas d'URL EN distincte (il n'y en a pas). Aucune balise hreflang dans le sitemap.
v1.26.1 — Juillet 2026
  • ·Fix articles EN locale — 2 régressions v1.26.0 :
    • ·Fix 500 "Event handlers cannot be passed to Client Component props" : SerieNav était défini inline dans articles/[slug]/page.tsx (server component) et passait onMouseEnter/onMouseLeave à <Link> (client component Next.js) — violation RSC. Extrait en src/components/SerieNav.tsx ('use client') ; les handlers hover restent fonctionnels côté client.
    • ·Fix masquage EN incorrect : suppression du bandeau ambre "not available in English + contenu FR en dessous" introduit en v1.26.0. ArticleLocaleView adopte désormais la règle stricte : locale EN + contenu_en absent → return null + router.replace('/articles') (redirect client-side, zéro contenu FR affiché). locale EN + contenu_en présent → titre EN + contenu EN uniquement. locale FR → comportement inchangé.
    • ·Robustesse filtre enOnly : .gt('contenu_en', '') remplace le double .not().neq() — exclut NULL et chaîne vide en une seule condition PostgreSQL (NULL comparisons → false, '' > '' → false, tout contenu réel → true). Cookie lu avec .trim() pour éviter les espaces parasites.
v1.26.0 — Juillet 2026
  • ·Articles en locale EN : les colonnes titre_en, excerpt_en, contenu_en de la table articles sont désormais exploitées côté front.
    • ·Fiche article (/articles/[slug]) : nouveau composant client ArticleLocaleView — quand la locale est EN et contenu_en est non vide, affiche le titre EN (h1) + le corps EN via le même moteur ArticleContent. Quand contenu_en est NULL/vide en locale EN, affiche le contenu FR avec une bannière visible « This article is not yet available in English — showing the French version. » — jamais de fallback FR silencieux. La page reste ISR (revalidate=3600) : ArticleLocaleView est un client component qui lit le cookie NEXT_LOCALE sans forcer le mode dynamique.
    • ·Liste articles (/articles) : en locale EN, seuls les articles ayant un contenu_en non NULL et non vide sont affichés (filtrage DB via .not('contenu_en','is',null).neq('contenu_en','')). Le total de pagination est exact côté serveur. La page est déjà force-dynamic ; le cookie NEXT_LOCALE est lu via cookies() de next/headers.
    • ·Cartes articles (ArticleCard) : passage en 'use client' + useLocale() — le titre et l'excerpt affichés switchent sur titre_en/excerpt_en quand la locale est EN et ces champs sont non vides.
    • ·Types : Article enrichi de titre_en?, excerpt_en?, contenu_en? (nullable). getPublishedArticles() dans db.ts accepte un nouveau paramètre enOnly?: boolean.
v1.25.4 — Juillet 2026
  • ·Fix bannière "sans traduction EN" masquée sur mobile : le badge countNoEn était à l'intérieur du <header sticky> — sur Safari/WebKit mobile, le backdropFilter: blur d'un élément sticky clippe les lignes supplémentaires créées par flex-wrap, ce qui coupait la bannière. Déplacé dans <main> (flux normal, toujours sous le sticky) sous forme d'une bannière pleine largeur (w-full, min-h-[44px], px-4 py-3, rounded-xl). Le header ne contient plus que "Zeedna Admin" + le badge "en attente" + les actions — garantit qu'il reste sur une seule ligne même à 390px et n'a plus besoin de flex-wrap.
v1.25.3 — Juillet 2026
  • ·Fix responsive mobile admin — aucune logique touchée, uniquement layout/CSS.
    • ·Header : passage en flex-wrap + gap-y-2 — le badge "N mots sans traduction EN" passe à la ligne sur mobile plutôt que de déborder ; py-3 au lieu de py-4 pour récupérer de l'espace vertical. Rafraîchir + Déconnexion regroupés dans un conteneur shrink-0 séparé.
    • ·Boutons d'action des cartes (PublishedWordCard) : py-2 + min-h-[40px] pour une zone tappable correcte sur mobile ; flex-wrap déjà présent sur le conteneur garantit le passage à la ligne si l'espace manque.
    • ·Onglets : remplacé flex-wrap par overflow-x-auto + shrink-0 sur chaque bouton — scroll horizontal natif sur mobile, disposition identique sur desktop.
v1.25.2 — Juillet 2026
  • ·Admin — traduction EN dans l'onglet Mots publiés : 4 améliorations sans changement de schéma.
    • ·Champs de traduction séparés et labellisés (PublishedWordCard) : le formulaire d'édition expose désormais « Traduction FR » et « Traduction EN » avec leurs labels distincts. Badge rouge « FR manquante » si translation_fr vide, badge rouge « EN manquante » si translation_en NULL/vide. En mode lecture, les badges sont affichés sous la ligne principale (form_ar / form_latin / traduction).
    • ·Badge « Traduit par IA — à revoir » (défensif) : s'affiche uniquement si la future colonne en_auto existe ET est true — sans crash si la colonne est absente (optional chaining). La colonne n'est pas créée.
    • ·Filtre « Sans traduction EN » : bouton toggle sous la barre de recherche de l'onglet Mots publiés ; paramètre ?noEnOnly=true transmis à /api/admin/words ; filtre OR translation_en IS NULL OR translation_en = '' côté Supabase ; cumulable avec la recherche textuelle. Le compteur de résultats s'affiche à côté du bouton actif.
    • ·Compteur EN dans l'en-tête admin : « N mots sans traduction EN » — badge rouge cliquable qui bascule directement sur l'onglet Mots publiés avec le filtre Sans-EN activé. Chargé via GET /api/admin/words/count-no-en (nouveau, unstable_cache revalidate 1h) — un seul appel au premier affichage de l'onglet, jamais au render.
    • ·/api/admin/words (GET) : sélection étendue à translation_en et pos_type ; filtre status='validé' ajouté ; nouveau paramètre ?noEnOnly=true.
    • ·/api/admin/words/[id] (PATCH) : accepte désormais translation_en et pos_type en mise à jour.
v1.25.1 — Juillet 2026
  • ·Fix EN : placeholder pour translation_en manquante — quand la langue d'affichage = EN et translation_en est NULL ou vide, la traduction française n'est plus affichée en fallback silencieux.
    • ·Fiche mot (/mot/[slug]) : placeholder cliquable « ✎ No English translation yet — contribute one » qui ouvre directement le formulaire SuggestEdit existant (field='translation_fr'). Nouveau composant TranslationSection.tsx ('use client') avec état partagé entre le bouton et le SuggestEdit.
    • ·Cartes (listes /dictionnaire, résultats de recherche) : placeholder italique atténué (non interactif — la carte entière est un lien vers la fiche détaillée où l'interaction complète est disponible).
    • ·FR mode : comportement inchangé dans les deux contextes.
    • ·EN mode + translation_en renseignée : comportement inchangé.
    • ·Vérification NULL ET chaîne vide (?.trim()).
    • ·Clé i18n : word.noEnTranslation (en.json / fr.json).
    • ·SuggestEdit.tsx : ajout de props optionnelles open / onOpenChange pour contrôle externe de l'état ouvert (rétrocompatible — comportement interne par défaut si props absentes).
v1.25.0 — Juillet 2026
  • ·scripts/scan_drive_import.py — pipeline d'import des ~600 captures Derja.Ninja depuis Google Drive vers staging_scan_words.
    • ·Lecture Drive via API OAuth2 (google-api-python-client) : énumère les sous-dossiers par dialecte (IDs Drive réels récupérés au 2026-07-13), télécharge chaque image en mémoire.
    • ·Gemini 2.5 Flash Vision — prompt dédié qui extrait un TABLEAU d'entrées par capture (2-4 mots par page de résultats), avec consigne explicite de restituer ال exactement tel qu'affiché (problème confirmé avec l'OCR générique Drive).
    • ·Dédup obligatoire sur form_ar_norm (colonne dévocalisée) via RPC find_duplicate_dialect_form + ILIKE sur form_latin ; jamais sur form_ar brut (évite les faux positifs harakat).
    • ·Idempotence : scripts/.cache/drive_processed_ids.json — relancer le script ne re-traite pas les images déjà vues.
    • ·Traçabilité : source_detail = 'Drive/SCAN <dossier> — <nom_fichier>' + drive_file_id (nouvelles colonnes, migration SQL fournie dans le script).
    • ·Flags CLI : --dialect TN --limit 20 --dry-run --verbose.
    • ·Rattachement concept_id tentative (normalisation translation_fr → concepts.id).
    • ·Migrations Supabase requises (documentées dans le script) : ADD COLUMN IF NOT EXISTS source_detail TEXT + drive_file_id TEXT sur staging_scan_words.
v1.24.5 — Juillet 2026
  • ·Build résilient aux pannes Supabase — une base indisponible (402, 500, réseau, timeout) ne bloque plus le déploiement Vercel. Mécanisme : .catch() gated sur process.env.NEXT_PHASE === 'phase-production-build' — au build, retourne un tableau vide valide + console.warn('[BUILD FALLBACK] ...') ; au runtime ISR, re-throw pour que Next.js serve la page stale mise en cache (comportement inchangé).
  • ·src/app/page.tsx — helper withBuildFallback<T>() local appliqué à getAllWords() et getRecentPublishedArticles(3) ; homepage s'affiche vide au build si Supabase est down, plutôt que de crasher.
  • ·src/app/sitemap.ts — même pattern inliné sur getAllWords() ; sitemap réduit au lieu d'une erreur de build.
  • ·Test validé : build avec NEXT_PUBLIC_SUPABASE_URL=https://fake.supabase.co (fetch réseau impossible) → 41 pages générées, 2 lignes [BUILD FALLBACK], build vert.
v1.24.4 — Juillet 2026
  • ·Fix egress Supabase (~95% de réduction)getAllWords() faisait un select('*') sur les tables dialect_forms et words (3 200+ lignes, toutes colonnes) à chaque render de /mot/[slug] et /articles/[slug], sans aucun cache — cause des 15 GB d'egress (305% du quota).
  • ·getAllWordLinks() (nouveau, db.ts) — remplace getAllWords() dans les contextes d'auto-linking : ne sélectionne que slug, form_ar, dialect (dialect_forms) + slug, word_ar (words), wrappé par unstable_cache(['autolink-word-index'], { revalidate: 86400 })1 seule requête DB par 24h partagée entre toutes les pages.
  • ·getRelatedWords(dialectCode, excludeId) (nouveau, db.ts) — requête ciblée (3 mots max, même dialecte, colonnes minimales) pour la section "Autres mots" sur /mot/[slug], qui lisait auparavant toute la table pour filtrer en mémoire.
  • ·/mot/[slug]/page.tsxgetAllWords()getAllWordLinks() (wordLinks) + getRelatedWords() (related). La section "Autres mots" conserve son comportement ; LinkedExamplePhrase reçoit le même tableau WordLink[].
  • ·/articles/[slug]/page.tsxgetAllWords()getAllWordLinks(). Seul w.slug était utilisé (slugArray) — aucun impact fonctionnel.
  • ·getAllWords() conservé intact pour la homepage (1 page, ISR 1h) et le sitemap.
v1.24.3 — Juillet 2026
  • ·Rate limit desserré sur /api/submit + bypass adminsrc/lib/rateLimit.ts : MAX_REQUESTS relevé de 5 → 30 soumissions par heure par IP. src/app/api/submit/route.ts : si la requête porte un header Authorization: Bearer {ADMIN_SECRET}, le rate limit est court-circuité avant tout comptage — un admin peut soumettre sans restriction. Le message d'erreur 429 (Limite atteinte. Réessayez après HH:MM:SS) et le Turnstile restent inchangés pour les utilisateurs anonymes.
v1.24.2 — Juillet 2026
  • ·Auto-bascule en mode Arabe à la saisie de caractères arabesSearchBar.tsx : le handler onChange de l'input détecte la présence d'un caractère arabe (blocs Unicode ؀-ۿ, ݐ-ݿ, ࢠ-ࣿ, ﭐ-﷿, ﹰ-) et passe automatiquement mode à 'arab' si le mode courant n'est pas déjà 'arab'. Bascule à sens unique (pas de retour forcé vers latin). Le nouveau mode est immédiatement utilisé par le debounce d'autocomplétion et propagé via onSearch(q, mode) vers /dictionnaire (le field du résultat page suit).
v1.24.1 — Juillet 2026
  • ·Lien de révision si score < 50% sur l'écran de résultat du QCMQuizPlayerArticle.tsx : en phase résultat, si correctCount / questions.length < 0.5 et que article_slug est renseigné sur le quiz, affiche un encart doré sous le score avec un message d'encouragement (locale quiz.reviewPrompt) et un bouton « Relire l'article » (locale quiz.reviewCta) vers /articles/{article_slug} via next/link. Si score ≥ 50% : comportement inchangé, aucun encart.
  • ·Locales fr/enquiz.reviewPrompt et quiz.reviewCta ajoutés.
v1.24.0 — Juillet 2026
  • ·Module QCM relié aux articles via article_slug/qcm/[articleSlug] charge les quiz publiés liés à un article (fr + ar) et affiche un player complet avec toggle de langue, validation par question et correction détaillée à la fin.
  • ·hasArticleQuiz(articleSlug) dans db.ts — requête count légère pour détecter si un quiz existe pour un article, utilisée par /articles/[slug] (CTA conditionnel).
  • ·getQuizzesByArticleSlug(articleSlug) dans db.ts — charge tous les quiz publiés pour un article_slug donné (fr + ar) avec leurs questions et réponses imbriquées (order_index).
  • ·types/index.tsarticle_slug?: string | null ajouté à l'interface Quiz.
  • ·/articles/[slug]/page.tsx — CTA QCM relie maintenant via article_slug (non plus related_quiz_category). hasArticleQuiz() en Promise.all avec getRelatedArticles. Lien vers /qcm/${article.slug}.
  • ·/qcm/[slug]/page.tsx — double routing : essaie getQuizzesByArticleSlug (article_slug) en parallèle avec getQuizBySlug (slug quiz) ; si des quiz d'article trouvés → QuizPlayerArticle ; sinon si quiz standalone → QuizPlayer (rétrocompat).
  • ·QuizPlayerArticle.tsx (nouveau) — composant client : toggle FR/العربية (masqué si une seule langue), rendu question par question (radio=single, checkbox=multiple, mention "(plusieurs réponses possibles)" si multiple), validation avec feedback immédiat par réponse (vert/rouge), stockage de toutes les sélections, phase résultat : score X/N + % + correction par question (réponses surlignées vert/rouge + explication), bouton "Recommencer".
  • ·Locales fr/en — ajout de quiz.multiple et quiz.correctionTitle.
v1.23.3 — Juillet 2026
  • ·Fix recherche française accent-insensible sur la page de résultats — taper proteger (sans accent) ne trouvait pas Protéger... ni sécuriser sur la page /dictionnaire (mode "Français/Anglais"). L'autocomplétion fonctionnait déjà (via la RPC search_df_trad qui utilise unaccent()), mais les résultats complets (searchDf/searchMsa) faisaient un ILIKE accent-sensible sur translation_fr brut — 40% des traductions accentuées manquaient.
  • ·normalizeFr() ajoutée à src/lib/search-utils.ts — reproduit EXACTEMENT la normalisation SQL des colonnes générées translation_fr_norm / translation_en_norm (déjà en base) : str.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/œ/g, 'oe').replace(/æ/g, 'ae').trim(). PAS de mapping phonétique ni autre transformation (distincte de normalizeSearch()).
  • ·ILIKE parallèle sur colonnes normalisées dans src/lib/db.tsdfIlike et msaIlike acceptent un 2ᵉ paramètre val (défaut : q). Branches field='fr' de searchDf et searchMsa : 2 ILIKE supplémentaires sur translation_fr_norm + translation_en_norm avec normalizeFr(q), en Promise.all(), colonnes normalisées placées EN TÊTE du merge (Set dédup existant). Jamais de .or() avec variable (règle PostgREST/virgule).
  • ·scoreWordRelevance() field='fr' — comparaisons sur formes normalisées normalizeFr (au lieu de normalizeSearch) : fFr === fq est EXACT même si les accents diffèrent. Garantit que proteger classe Protéger en 1.0 et sécuriser en 1.0.
  • ·Autocomplétion route.ts (field='fr') — 2 ILIKE supplémentaires sur translation_fr_norm + translation_en_norm avec normalizeFr(q) en Promise.all() aux côtés de la RPC search_df_trad existante.
  • ·Tests (npm run test:arabic-search) — 23 assertions (13 arabes v1.23.2 + 10 nouvelles fr) : normalizeFr('Protéger')==='proteger', normalizeFr('sécuriser')==='securiser', scoreWordRelevance('proteger',...,'Protéger','fr')===1.0, exact par sens (0.97), préfixe d'un sens (0.94), exact > contains.
v1.23.2 — Juillet 2026
  • ·Fix recherche arabe harakat-insensible — taper قدم (sans voyelles) ne trouvait pas قَدّمْ (stocké avec tachkeel/harakat) parce que le ILIKE comparait form_ar brut (avec diacritiques) à la query (sans diacritiques) — 75% de la base étant stockée avec harakat, c'était un bug majeur pour la recherche en mode arabe.
  • ·normalizeArabic() ajoutée à src/lib/search-utils.ts — reproduit EXACTEMENT la normalisation SQL des colonnes générées dialect_forms.form_ar_norm / words.word_ar_norm (déjà en base) : strip tashkeel étendu (ؐ–ؚ, ً–ٟ, ٰ, ۖ–ۭ) + strip tatweel (ـ) + أإآٱ→ا + ى→ي + ة→ه. PAS de lowercase ni de mapping phonétique (ق reste ق — distincte de normalizeSearch() qui mappe ق→9). Le JS est identique à l'expression SQL pour garantir que normalizeArabic(query) génère le même texte que la colonne indexée → chaque ILIKE sur form_ar_norm trouve ses matchs.
  • ·scoreWordRelevance() (src/lib/search-utils.ts) — nouveau mode field='arab' — compare sur les formes normalisées (normalizeArabic(query) vs normalizeArabic(form_ar)) : (1.0) exact normalisé — taper قدم vs قَدّمْ → score 1.0 ; (0.9) préfixe normalisé ; (0.8) sous-chaîne normalisée. Modes latin/fr/legacy strictement inchangés.
  • ·ILIKE parallèle sur colonne normalisée dans src/lib/db.ts (searchDf + searchMsa) et src/app/api/search/route.ts (autocomplete) — 4 branches touchées : field='arab' (dialect_forms/words) + legacy isArabic (dialect_forms/words). Chaque branche ajoute un 3ᵉ ILIKE parallèle sur form_ar_norm/word_ar_norm avec normalizeArabic(q), en Promise.all() avec les existants. Résultats normalisés placés EN TÊTE du merge (Set dédup existant). Jamais de .or() avec variable (règle PostgREST/virgule). RPCs non touchées (évite le bug overloads dupliqués RETURNS TABLE, cf. CLAUDE.md v1.14.2).
  • ·Nouveau test src/lib/arabicSearch.test.ts (npm run test:arabic-search) — 13 assertions : normalizeArabic('قَدّمْ') === 'قدم', normalizeArabic('مِتْقَدِّمْ') === 'متقدم', score exact en mode arab (1.0), préfixe (0.9), contains (0.8), exact > contains.
v1.23.1 — Juillet 2026
  • ·Fix recherche Français (field='fr') — exact traduction noyé sous des matches partiels — en mode "Français/Anglais" de la barre de recherche, taper "si" donnait des résultats absurdes : siassa (form_latin commençant par "si") remontait avant إذَا (translation_fr exactement = "si") parce que scoreWordRelevance() attribuait 0.8 aux matches form_latin contenant la query et seulement 0.77 à l'exact traduction — la hiérarchie arabic/latin > traduction était universelle même quand l'utilisateur cherchait explicitement en français.
  • ·scoreWordRelevance() (src/lib/search-utils.ts) — nouveau paramètre optionnel field — quand field='fr', la fonction applique une hiérarchie mode-aware : (1.0) exact plein champ translation_fr/en ; (0.97) exact par sens — la traduction est découpée sur [,;/،] pour que "si, seulement" → sens "si" matche exactement ; (0.94) préfixe d'un sens (requête ≥ 3 chars seulement — évite que "si" donne 0.94 à tout mot dont la trad commence par "simple") ; (0.91) correspondance sur frontière de mot \b dans la traduction ; puis arabic/latin comme champ secondaire (0.70/0.65/0.60), en-dessous de la sous-chaîne traduction (0.75). Les modes field='latin'/'arab'/undefined gardent strictement le comportement original (aucun changement).
  • ·Propagation de field aux consommateurs : scoreRow() + secondarySearch() + dfRowToSuggestion() dans src/app/api/search/route.ts (autocomplete) ; tri final dans src/lib/db.ts (searchWords() — résultats /dictionnaire). matchType dans scoreRow mis à jour pour le mode fr (translation exact → badge 'exact' au lieu de 'trad').
  • ·Nouveau test src/lib/searchScoring.test.ts (npm run test:search-scoring) — 23 assertions : query "si" vérifie إذَا (1.0) > إلا/كان (0.97, exact-par-sens) > bssit/derwich (0.75, sous-chaîne trad) > siassa (0.65, latin prefix) ; query "politique" vérifie siassa en #1 (1.0 exact) ; word-boundary testé séparément (0.91) ; modes latin et legacy confirmés inchangés (siassa=0.9 préfixe latin, إذَا=0.77 traduction contains).
v1.23.0 — Juillet 2026
  • ·Retour de la vidéo en fond du Hero de la landingHeroSlideshow (diaporama d'images) remplacé par HeroVideo (3 mp4 de /public/videos/ en lecture aléatoire, fondu enchaîné) dans src/app/page.tsx. HeroSlideshow reste dans le repo (non supprimé) mais n'est plus importé dans le hero. Layout du contenu du hero (colonne gauche texte/recherche/CTA, عامية décoratif à droite) non touché — seul le fond change.
  • ·HeroVideo.tsx — nouveau prop optionnel poster (attribut natif poster sur <video>, évite un hero noir pendant le chargement) : /a-propos continue de l'appeler sans ce prop (s'appuie sur le background CSS existant de sa section, comportement inchangé) ; la landing passe poster="/hero/desert-2-sunset-dunes.jpg". preload="metadata" ajouté sur les 3 vidéos (au lieu du défaut navigateur) pour ne pas plomber le chargement initial. object-cover object-center explicite. loop={false} conservé intentionnellement : le "bouclage" perçu est la playlist des 3 clips (avance via onEnded), pas l'attribut natif — loop={true} figerait la lecture sur un seul clip et empêcherait la rotation.
  • ·Overlay hero — dégradé clair→sombre au lieu d'un voile uni opaquelinear-gradient(to bottom, rgba(10,7,3,0.35) 0%, rgba(10,7,3,0.45) 45%, rgba(10,7,3,0.65) 100%) : la vidéo reste bien visible en haut, s'assombrit progressivement vers le bas où se trouvent le h1/la barre de recherche pour garder la lisibilité. L'ancien dégradé avait un point médian PLUS clair que le haut (non-monotone, incohérent) et montait jusqu'à 0.88 en bas — remplacé par une progression monotone 0.35→0.65. Second calque décoratif (teinte dorée 135deg) conservé à l'identique.
  • ·Hauteur du hero responsivemin-h-[62vh] sm:min-h-[68vh] md:min-h-screen (au lieu de min-h-screen fixe) : une vidéo paysage dans un hero plein écran mobile se retrouvait très zoomée/recadrée agressivement ; réduire la hauteur minimale sur mobile respecte mieux le ratio de la vidéo tout en gardant object-cover/object-position: center. min-h-* (pas h-*) : le hero grandit toujours si le contenu l'exige, aucun risque de clipping de texte.
  • ·Vérification : rendu réel testé via Chromium headless (Playwright) sur une page HTML reproduisant exactement le markup/CSS du nouveau hero, aux deux tailles (1280×850 desktop, 420×900 mobile) — dégradé et cadrage confirmés visuellement corrects avec l'image poster comme proxy. Limite connue de l'environnement sandbox : le Chromium fourni par Playwright dans ce conteneur n'a pas de support H.264 (DEMUXER_ERROR_NO_SUPPORTED_STREAMS), donc les frames réelles des 3 .mp4 n'ont pas pu être prévisualisées ici (seul le poster s'affichait) — mécanique CSS (overlay, hauteurs, object-fit) validée, contenu vidéo réel à vérifier visuellement en environnement normal (Chrome/Safari réels supportent H.264).
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