La plateforme consolide trois sources de données — l'API Exeve (catalogue de ventes), Airtable (demandes commerciales) et des CSV ponctuels — dans une base Supabase. Le dashboard Next.jsque vous utilisez lit cette base. Les imports tournent automatiquement chaque matin à 09h30 ou manuellement via l'onglet Admin.
1. Architecture
Le flux de données est unidirectionnel : les sources amont alimentent Supabase, et le dashboard lit Supabase. Aucun écran ne tape directement sur Exeve ou Airtable en lecture.
┌──────────────┐ ┌──────────────┐
│ Exeve API │ │ Airtable │
│ (ventes, │ │ (demandes, │
│ contacts) │ │ sources) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌────────────────────────────────────┐
│ Scripts Python (Mac local, .venv) │
│ sync_contacts │
│ sync_sales │
│ sync_airtable_sources │
└────────────────┬───────────────────┘
│
▼
┌───────────────┐ ┌───────────┐
│ Supabase │◄──────│ Make.com │
│ PostgreSQL │ │ (token) │
└───────┬───────┘ └───────────┘
│ lecture (anon key, RLS)
▼
┌────────────────┐ ┌──────────────────────┐
│ Next.js 16 │ ◄────── │ Vercel (Hobby plan) │
│ frontend/ │ deploy │ build auto sur push │
│ React 19 │ push │ → bi.leducq-encheres │
└────────────────┘ main │ .com │
└──────────────────────┘
2. Bases Supabase
Supabase est notre base PostgreSQL hébergée. Elle contient deux types d'objets : des tables (alimentées par les scripts Python) et des vues SQL (calculs en temps réel sur les tables, consommées par le dashboard).
Tables principales
ventes | Une ligne par vente Exeve. Clés : PK id_supabase · clé externe id_exeve Champs : nom_vente, date_vente Source : sync_sales.py |
items | Une ligne par lot. Cœur de la base. Clés : PK id_supabase · FK vente_id → ventes.id_exeve · FK vendeur_id / acheteur_id → contacts.id_supabase · FK expert_id → experts.id_supabase Champs : description, adjudication, estimation_basse/haute, thumbnail_url, ref_vendeur, ref_acheteur Source : sync_sales.py |
contacts | Une ligne par personne (vendeur ou acheteur). Clés : PK ref_contact · aussi id_exeve, id_supabase Champs : nom, email, ville, telephone, source_acquisition Source : sync_contacts.py + sync_airtable_sources.py |
experts | Une ligne par expert (commissaires-priseurs externes, spécialistes). Clés : PK id_supabase · UNIQUE ref_expert · UNIQUE id_exeve Champs : prenom, nom, email, expertise, commission, ref_expert, id_exeve Source : sync_experts_csv.py (import manuel CSV) |
Vues BI (lecture seule, calculées par Supabase)
bi_dashboard_master | Superset des items avec jointures ventes + contacts. Lu par le dashboard, les filtres et l'onglet Ventes. Clés : FK vente_id (= ventes.id_exeve) Champs : tout items + nom_vente, date_vente, vendeur_source Source : vue SQL |
bi_ventes_stats | Une ligne par vente, agrégats prêts à afficher. Clés : id_supabase, id_exeve Champs : nom_vente, date_vente, nb_lots_total, nb_lots_vendus, total_adjudication, ca_total, taux_de_vente Source : vue SQL |
bi_stats_mensuelles | Une ligne par mois. Alimente le graph « Performance mensuelle ». Clés : mois (date) Champs : adjudication_totale, ca_total, nb_lots_total, nb_lots_vendus, adjudication_moyenne, taux_invendu_pourcentage Source : vue SQL |
core_contact_stats | Vue CRM Contacts : profil + KPIs vendeur/acheteur pré-agrégés. Lue par la modal de détail contact pour un affichage instantané (1 ligne SQL au lieu de jusqu'à 500 lignes côté JS). Clés : ref_contact (= contacts.ref_contact) Champs : profil (nom, prenom, email, telephone, ville, code_postal, pays, source_acquisition, civilite) · flags (is_vendeur_real, is_acheteur_real) · stats vendeur (nb_lots_mis_en_vente, nb_lots_vendus, nb_invendus, taux_invendu, ca_total_vendeur) · stats acheteur (nb_lots_achetes, total_adjudication_achete) Source : vue SQL (refactor CTE — un seul scan d'items au lieu de subqueries corrélées) |
Table technique
app_secrets | Stocke les secrets qui rotatent souvent. Évite d'éditer .env tous les jours. Clés : PK key (text) Champs : value (text), updated_at Source : Make.com (upsert quotidien du EXEVE_TOKEN) |
3. Scripts Python
Trois scripts vivent à la racine du projet et sont exécutés avec l'interpréteur local .venv/bin/python. Ils peuvent être lancés manuellement (terminal ou onglet Admin) ou automatiquement par le cron.
sync_contacts.pyImporter / mettre à jour tous les contacts depuis l'API Exeve (paginé).
- Inputs
- Aucun. Lit .env (Supabase) + table app_secrets (token Exeve).
- Sortie
- Table contacts à jour. Log : nb ajoutés, nb mis à jour, nb total.
- Déclencheur
- Bouton « Synchroniser les contacts » dans Admin · cron 09h30
sync_sales.pyImporter une vente Exeve avec ses lots, en croisant un CSV pour récupérer les références vendeur, acheteur et expert.
- Inputs
--sale-id(ID Exeve de la vente, ex.4744) ·--csv-file(export CSV Exeve correspondant, doit contenirRef. Vendeur,Ref. AcheteuretRef. Expert)- Sortie
- Lignes ajoutées/upsertées dans
ventesetitems. Les FKvendeur_id/acheteur_id/expert_idsont résolues par lookup des refs CSV danscontacts.ref_contactetexperts.ref_expert— importez donc contacts et experts d'abord. - Déclencheur
- Bouton « Ajouter une vente » dans Admin uniquement. Manuel, une vente à la fois.
sync_experts_csv.pyImporter une liste d'experts depuis un CSV (Nom, Prénom, Email — ref_expert / expertise / commission optionnels).
- Inputs
--csv-file(CSV experts). Colonnes mappées :Nom→nom,Prénom→prenom,Email→email.- Sortie
- Lignes insérées dans experts. Dédoublonnage idempotent sur le triplet (nom, prenom, email) en lower-case.
- Déclencheur
- Bouton « Importer un CSV » dans Admin uniquement. Manuel, à la demande.
sync_airtable_sources.pyPour chaque vendeur réel, identifier la première demande Airtable et écrire son champ Source dans contacts.source_acquisition.
- Inputs
- Aucun. Lit .env + app_secrets pour Supabase et AIRTABLE_TOKEN / AIRTABLE_BASE_ID pour Airtable.
- Sortie
- Colonne source_acquisition des contacts vendeurs renseignée.
- Déclencheur
- Bouton « Synchroniser les sources » dans Admin · cron 09h30 (après sync_contacts)
4. Automatisations
Cron local — Mac
Chaque jour à 09h30, une tâche programmée enchaîne :
- sync_contacts.py
- sync_airtable_sources.py (uniquement si l'étape 1 a réussi)
Sortie + erreurs dans /tmp/leducq-cron.log. Pour vérifier que la tâche est bien planifiée :
crontab -l
Make.com — rotation du token Exeve
Le token Exeve expire toutes les ~24h. Un scénario Make capture la nouvelle valeur chaque matin et fait un upsert dans la table app_secrets avec la clé EXEVE_TOKEN. Les scripts Python lisent cette table à chaque exécution — plus besoin de toucher au .env chaque jour.
app_secrets.EXEVE_TOKEN contient une valeur fraîche (colonne updated_at du jour).5. Sources externes
API Exeve
- Base URL :
https://api.exeve.fr/v1/public/ - Authentification : Bearer JWT, rotation ~quotidienne (cf. Make ci-dessus)
- Endpoints utilisés :
/contacts(paginé),/sale/{id},/sale/{id}/items
Airtable
- Table
Demandeurs— un record par personne ayant fait une demande. Champemailest la clé de jointure avec Supabase. - Table
Demande— un record par demande commerciale. Le champDemandeurest un linked record versDemandeurs. Le champSourcecontient l'origine (Artvaleur - Formulaire, Leducq - Inventaire, etc.). - Auth : Personal Access Token (PAT) avec scope
data.records:readsur la base.
6. Frontend
Application Next.js 16 (App Router) + React 19, charts par Tremor, thème Tailwind v4 « Midnight Luxury ».
Pages
/ | Dashboard : KPI banner, filtres (période avec DateRangePicker Tremor, vente, expert, source), courbe d'évolution, histogramme mensuel, donut sources/experts. Clés : lit bi_dashboard_master + bi_stats_mensuelles |
/ventes | Catalog : liste de toutes les ventes avec thumbnails + adjudication totale. Clés : lit bi_ventes_stats + bi_dashboard_master |
/ventes/[saleKey] | Détail d'une vente avec 2 tabs (KPIs / Lots) côte à côte du titre. Tab KPIs : podiums Lots/Vendeurs/Acheteurs (bouton « Voir plus » → modal liste complète) + donuts Source/Expert (expert capé à 6 slices, « Voir plus de détails » pour déplier). Tab Lots : tableau triable. Le bandeau KPI haut reste visible sur les 2 tabs. Click sur un contact d'un podium ou d'une liste → modal détails contact (stats pré-agrégées de core_contact_stats, lots en lazy-load). Clés : lit bi_dashboard_master.eq('vente_id', …) + core_contact_stats pour la modal contact |
/admin | Boutons pour déclencher les scripts Python. Local-only — sur Vercel, les routes /api/sync-* renvoient 503. Clés : POST sur /api/sync-* |
/documentation | Cette page. Clés : — |
Routes API (spawn de scripts Python)
POST /api/sync-sale | Reçoit saleId + CSV en multipart, spawne sync_sales.py, renvoie stdout/stderr. |
POST /api/sync-contacts | Spawne sync_contacts.py, sans input. |
POST /api/sync-airtable-sources | Spawne sync_airtable_sources.py. |
POST /api/sync-experts-csv | Reçoit un CSV en multipart, spawne sync_experts_csv.py. |
/api/sync-*spawnent du Python local et ne marchent qu'en dev (localhost:3000). En production sur Vercel, elles renvoient un 503 via un check process.env.VERCELau début du handler — pour éviter qu'un appel accidentel essaie d'exécuter Python qui n'existe pas. Voir la section Déploiement pour plus de détails.7. Déploiement
L'app est en production depuis le 20 mai 2026. Le code vit sur GitHub, l'hébergement et le build tournent chez Vercel. Aucune commande manuelle à lancer pour déployer : un git push sur main suffit.
URLs
https://bi.leducq-encheres.com | URL publique de production. Sous-domaine pointé vers Vercel via un CNAME dans la zone DNS OVH de leducq-encheres.com. |
https://leducq-bi.vercel.app | Alias Vercel auto-généré. Toujours fonctionnel, redirige vers la prod. Utile pour debug ou si OVH a un souci DNS. |
https://github.com/gregoire-lq/leducq-bi | Repo source — compte GitHub pro (gregoire-lq). L'ancien repo perso gregoiregagnaux/leducq-bi n'est plus utilisé. |
Workflow Git → Vercel
git pullavant de commencer (récupère ce que l'autre dev a poussé).- Coder, tester en local avec
npm run dev. git add -A && git commit -m "…"pour sauvegarder l'étape.git push→ Vercel détecte le commit et redéploie automatiquement (~1 min).- Branche autre que main : Vercel crée une URL de preview dédiée (
leducq-bi-git-<branche>-leducq-s-projects.vercel.app) — utile pour tester sans toucher à la prod.
Configuration Vercel à connaître
Root Directory | frontend (le monorepo a un dossier frontend/ pour Next.js et le code Python à la racine). |
Framework Preset | Next.js — ⚠️ ne JAMAIS passer à « Other », sinon Vercel sert le projet en static générique et toutes les routes renvoient 404. |
Build Command | npm run build (auto-detected) → exécute next build --webpack dans package.json. Le flag --webpack est nécessaire car l'output Turbopack par défaut de Next 16 a un bug d'intégration avec l'adapter serverless Vercel (résultat : déploiement Ready mais 404 sur toutes les routes). |
frontend/.npmrc | legacy-peer-deps=true — nécessaire car Tremor v3.18.7 déclare encore react@^18 comme peer dependency, alors qu'on tourne en React 19. npm strict refuse l'install sans ce flag. Le jour où Tremor passe à React 19 officiellement, on pourra retirer ce fichier. |
Environment Variables | NEXT_PUBLIC_SUPABASE_URL + NEXT_PUBLIC_SUPABASE_ANON_KEY (clé sb_publishable_…). PAS la clé sb_secret_… (service_role) — celle-ci ne va que dans le .env racine pour les scripts Python. |
Identité Git | Les commits doivent être signés par un membre de la team Vercel (gregoire@leducq-encheres.com). Si tu fais un commit avec une autre identité, Vercel rejette en « Blocked ». Vérifier avec git config user.email dans le repo. |
Routes admin (/admin et /api/sync-*) sur Vercel
Ces routes spawn du Python local — impossible sur Vercel (pas de Python, pas de .venv). Les routes API embarquent un check process.env.VERCEL qui renvoie 503 en production. Pour synchroniser depuis chez vous, lancer npm run dev localement et utiliser /admin sur localhost:3000, ou exécuter les scripts Python en CLI directement.
8. En cas de pépin
Quelques scénarios fréquents et le bon réflexe associé.
Les chiffres du dashboard n'ont pas bougé depuis hier.
Causes probables
Mac en veille pendant le cron de 09h30 · Make n'a pas rafraîchi le token Exeve · Une vue Supabase a été cassée.
Quoi faire
/adminet cliquer « Synchroniser les contacts ». Regarder le log — si erreur 403, c'est le token.2. Vérifier dans Supabase Table Editor que
app_secrets a bien une ligne EXEVE_TOKEN récente.3. Sinon, vérifier le scénario Make.
Une vente affiche zéro lot alors qu'elle existe dans Supabase.
Causes probables
La colonne vente_id de bi_dashboard_master ne matche pas l'id_exeve attendu · Ou le lot n'a pas été importé.
Quoi faire
select count(*) from bi_dashboard_master where vente_id = '<id_exeve>';2. Si zéro, l'import n'a pas tourné. Relancer
sync_sales.py avec le bon sale_id et le CSV correspondant via /admin.Le filtre Source du dashboard ne montre aucune option.
Causes probables
La colonne vendeur_source de bi_dashboard_master est vide pour tous les rows.
Quoi faire
contacts.source_acquisitionjoint via le vendeur. Si vide partout, c'est que sync_airtable_sources.py n'a pas tourné. Le relancer via /admin.Un lot a expert_id NULL alors qu'un expert est censé être assigné.
Causes probables
L'expert n'est pas (encore) importé dans la table experts · La Ref. Expert du CSV ne matche aucun ref_expert en base · Le CSV n'a pas de colonne Ref. Expert.
Quoi faire
select * from experts where ref_expert = '<ref>';2. Si absent, importer/mettre à jour la liste experts via
/admin → « Importer un CSV ».3. Relancer ensuite
sync_sales.py pour cette vente — le lookup résoudra expert_id au prochain upsert.Un lot a un mauvais thumbnail / pas de thumbnail.
Causes probables
Le lot n'a pas de thumbnail_url côté Exeve · Ou son adjudication est NULL et la vente n'a aucun lot adjugé.
Quoi faire
Erreur sur localhost:3000 (« Site inaccessible »).
Causes probables
Le serveur dev Next.js s'est arrêté.
Quoi faire
cd ~/Documents/leducq-bi/frontend && npm run dev
L'app live (bi.leducq-encheres.com) renvoie 404 NOT_FOUND sur toutes les URLs, alors que Vercel dit que le déploiement est « Ready ».
Causes probables
Le Framework Preset Vercel est passé à « Other » au lieu de « Next.js » (typiquement après un re-import du projet où Vercel n'a pas auto-détecté Next.js). L'adapter serverless n'est plus activé, donc les routes ne sont pas servies.
Quoi faire
2. Vérifier « Framework Preset » — doit être
Next.js. Si c'est « Other », le repasser à Next.js et Save.3. Pour forcer Vercel à réécrire le cache de détection : sélectionner « Other » → Save → re-sélectionner « Next.js » → Save.
4. Redéployer (Deployments → dernier deploy → ... → Redeploy, sans cache).
Un déploiement Vercel reste « Blocked » avec un email « not a member of the team ».
Causes probables
L'identité Git locale ne correspond pas à un membre de la team Vercel. Typiquement, git config user.email est encore l'email perso au lieu du compte pro.
Quoi faire
git config user.email doit retourner gregoire@leducq-encheres.com.2. Sinon, le corriger :
git config user.email "gregoire@leducq-encheres.com" git config user.name "gregoire-lq"3. Faire un commit (au pire empty :
git commit --allow-empty -m "retrigger") puis git push. Vercel acceptera le nouveau build.