Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8397ec9793 | |||
| 9a6448facc |
@@ -0,0 +1,327 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commandes Essentielles
|
||||
|
||||
### Backend (Poetry)
|
||||
```bash
|
||||
poetry install # Installation des dépendances
|
||||
poetry run pytest # Tests unitaires
|
||||
poetry run ruff check backend/ # Linting
|
||||
poetry run mypy backend/ # Vérification de types
|
||||
poetry run coverage run -m pytest && poetry run coverage report -m # Couverture
|
||||
```
|
||||
|
||||
### Scraper CLI (test manuel)
|
||||
```bash
|
||||
SCRAPE_TEST_MAX=0 poetry run python backend/app/scraper/run_scrape_tests.py
|
||||
# Avec navigateur visible si blocage Amazon:
|
||||
SCRAPE_TEST_HEADFUL_ON_BLOCK=1 SCRAPE_TEST_MAX=0 poetry run python backend/app/scraper/run_scrape_tests.py
|
||||
```
|
||||
|
||||
### Serveur de développement
|
||||
```bash
|
||||
poetry run uvicorn backend.app.main:app --reload # Backend (port 8008)
|
||||
cd frontend && npm run dev # Frontend (port 5173)
|
||||
```
|
||||
|
||||
### Frontend (npm)
|
||||
```bash
|
||||
cd frontend && npm install && npm run dev # Dev
|
||||
cd frontend && npm run build # Build production
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Stack technique
|
||||
- **Backend**: FastAPI + SQLAlchemy (SQLite) + APScheduler + Loguru
|
||||
- **Scraper**: Playwright (Chromium) + BeautifulSoup4, contexte fr-FR
|
||||
- **Frontend**: React 18 + Vite + Zustand + SCSS Gruvbox dark
|
||||
|
||||
### Structure clé
|
||||
```
|
||||
├── agent.md
|
||||
├── backend
|
||||
│ ├── app
|
||||
│ │ ├── api
|
||||
│ │ │ ├── deps.py
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── routes_config.py
|
||||
│ │ │ ├── routes_products.py
|
||||
│ │ │ └── routes_scrape.py
|
||||
│ │ ├── core
|
||||
│ │ │ ├── config.py
|
||||
│ │ │ ├── logging.py
|
||||
│ │ │ ├── __pycache__
|
||||
│ │ │ └── scheduler.py
|
||||
│ │ ├── db
|
||||
│ │ │ ├── crud.py
|
||||
│ │ │ ├── database.py
|
||||
│ │ │ ├── models.py
|
||||
│ │ │ └── schemas.py
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── main.py
|
||||
│ │ ├── __pycache__
|
||||
│ │ │ └── __init__.cpython-313.pyc
|
||||
│ │ ├── samples
|
||||
│ │ │ ├── amazon_product.html
|
||||
│ │ │ ├── debug
|
||||
│ │ │ ├── scrape_fields.json
|
||||
│ │ │ ├── scrape_test.json
|
||||
│ │ │ ├── scrap_result.json
|
||||
│ │ │ └── storage_state.json
|
||||
│ │ ├── scraper
|
||||
│ │ │ ├── amazon
|
||||
│ │ │ ├── browser.py
|
||||
│ │ │ ├── normalize.py
|
||||
│ │ │ ├── __pycache__
|
||||
│ │ │ ├── runner.py
|
||||
│ │ │ └── run_scrape_tests.py
|
||||
│ │ ├── services
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── pricing.py
|
||||
│ │ │ └── __pycache__
|
||||
│ │ └── tests
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── __pycache__
|
||||
│ │ ├── test_normalize.py
|
||||
│ │ ├── test_parser_samples.py
|
||||
│ │ └── test_pricing.py
|
||||
│ ├── config_backend.json
|
||||
│ ├── data
|
||||
│ │ ├── raw
|
||||
│ │ └── screenshots
|
||||
│ ├── __init__.py
|
||||
│ ├── logs
|
||||
│ └── __pycache__
|
||||
│ └── __init__.cpython-313.pyc
|
||||
├── CHANGELOG.md
|
||||
├── CLAUDE.md
|
||||
├── consigne codex.md
|
||||
├── docker
|
||||
├── docs
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ ├── db_structure.md
|
||||
│ ├── frontend_structure.md
|
||||
│ ├── scrap.md
|
||||
│ └── tools.md
|
||||
├── frontend
|
||||
│ ├── config_frontend.json
|
||||
│ ├── package.json
|
||||
│ ├── public
|
||||
│ │ └── index.html
|
||||
│ ├── src
|
||||
│ │ ├── api
|
||||
│ │ │ └── client.js
|
||||
│ │ ├── app
|
||||
│ │ ├── App.jsx
|
||||
│ │ ├── components
|
||||
│ │ │ └── ProductCard.jsx
|
||||
│ │ ├── main.jsx
|
||||
│ │ └── styles
|
||||
│ │ └── global.scss
|
||||
│ └── vite.config.js
|
||||
├── kanban.md
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
└── TODO.md
|
||||
```
|
||||
|
||||
### Flux de scraping
|
||||
1. Playwright charge la page avec délais aléatoires (1-3s)
|
||||
2. Détection blocage (captcha/robot) → artéfacts debug (screenshot + HTML)
|
||||
3. Extraction via CSS selectors → normalisation (prix, stock, ratings)
|
||||
4. Sauvegarde: snapshot DB + raw JSON dans `backend/data/raw/`
|
||||
|
||||
### Base de données (SQLite)
|
||||
- **products**: URL, ASIN, titre, catégorie, statut actif
|
||||
- **product_snapshots**: prix, stock, ratings, badges, status scrape, lien vers raw JSON
|
||||
- **scrape_runs**: métadonnées de chaque exécution (success/fail counts)
|
||||
|
||||
### Configuration
|
||||
- `backend/config_backend.json`: intervalle scrape, timeout, browser settings
|
||||
- `frontend/config_frontend.json`: thème, layout grille, champs visibles
|
||||
- `.env`: variables d'environnement (copier `.env.example`)
|
||||
|
||||
## Règles de développement
|
||||
|
||||
### Libertés accordées
|
||||
- Créer/modifier fichiers backend, frontend, docs
|
||||
- Refactorer pour lisibilité/robustesse
|
||||
- Lancer tests et scripts de validation
|
||||
|
||||
### Actions nécessitant approbation
|
||||
- Refonte architecture (stack, framework)
|
||||
- Suppression/reset données utilisateur (tables, raw JSON, logs)
|
||||
- Migrations DB irréversibles
|
||||
|
||||
### Qualité code
|
||||
- Chaque scrape produit un log clair dans `backend/logs/scrap.log` (rotation 10MB, 7j)
|
||||
- Erreurs scraping = résilience (log + artéfacts) sans crash global
|
||||
- Logique métier critique accompagnée de tests
|
||||
|
||||
### Commits
|
||||
- Messages courts explicites: `feat: add scraper runner`
|
||||
- Un commit = une unité cohérente
|
||||
- Pas de commits fourre-tout
|
||||
|
||||
## Notes importantes
|
||||
|
||||
- Le scraping est volontairement lent (15-20 produits/jour) pour éviter blocage Amazon
|
||||
- Raw JSON conservé 30 jours dans `backend/data/raw/`
|
||||
- Pre-commit hooks configurés (Ruff + MyPy): `pre-commit install`
|
||||
|
||||
|
||||
# Schéma ASCII (UI globale)
|
||||
Objectif : reproduire l’esprit de la capture (vignette + section prix + graphe), en améliorant la lisibilité.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ suivi_produits [Add Product] [Refresh] [Settings] FE vX BE vY │
|
||||
│ (header fixed) [debug] (⋯) │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ Grid (cols = config_frontend.json) │
|
||||
│ │
|
||||
│ ┌──────────────────────────── Card Produit ──────────────────────────┐ │
|
||||
│ │ Boutique + Titre (2 lignes) │ │
|
||||
│ │ Amazon │ │
|
||||
│ │ Samsung SSD Interne 9100 Pro… │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────┐ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ Image │ │ ACTUEL 249€99 │ │ │
|
||||
│ │ │ (non rognée) │ │ PRIX CONSEILLÉ 329€99 (si présent) │ │ │
|
||||
│ │ │ │ │ RÉDUCTION -24% (si présent) │ │ │
|
||||
│ │ └───────────────┘ │ STOCK Disponible │ │ │
|
||||
│ │ │ NOTE 4,7 (967) │ │ │
|
||||
│ │ │ CHOIX AMAZON Oui/Non │ │ │
|
||||
│ │ │ PRIME Oui/Non │ │ │
|
||||
│ │ │ DEAL Oui/Non │ │ │
|
||||
│ │ │ Ref: ASIN [Lien produit] │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────── Graph 30j (clair) ─────────────────────┐ │ │
|
||||
│ │ │ ligne + points, axes lisibles, tooltip │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ Min 249€99 Max 249€99 Tendance → stable +0.0% Dernier: now │ │
|
||||
│ │ Catégorie: SSD Type: NVMe │ │
|
||||
│ │ │ │
|
||||
│ │ [Scrap] [Edit] [Delete] [Détail] │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
page de debug et log qui affiche le contenue des differentes tables sqlite dans des section distincte, une section log qui affiche les log json de scrap
|
||||
|
||||
## Vignette produit : exigences UI
|
||||
- Image **non tronquée** : object-fit `contain`, fond neutre, padding.
|
||||
- Section prix alignée au niveau de l’image (descendue comme sur la capture).
|
||||
- “Boutique + titre” sur 2 lignes, pleine largeur, icône boutique.
|
||||
- Badges : Amazon Choice / Prime / Deal / Exclusive (chips).
|
||||
- Graph 30j :
|
||||
- axes + labels lisibles
|
||||
- points visibles
|
||||
- min/max/tendance affichés sous le graphe
|
||||
- couleurs + flèches :
|
||||
- baisse : vert + flèche ↓
|
||||
- stable : jaune/orange + →
|
||||
- hausse : rouge + ↑
|
||||
- Responsive :
|
||||
- `columns` paramétrable (desktop)
|
||||
- mobile : 1 colonne + layout empilé
|
||||
|
||||
|
||||
Contraintes clés :
|
||||
- Scraping : **Python + Playwright** (robuste, peu de produits : 15–20/jour).
|
||||
- Stockage : **JSON** (raw scrap) + persistance en **SQLite** (historique prix + métriques).
|
||||
- Config :
|
||||
- `config_backend.json` (paramètres scraping + backend + catégories/types)
|
||||
- `config_frontend.json` (paramètres UI + colonnes + thème + bouton mode text/icon)
|
||||
- Logs : fichier de log des scrapes (rotation simple).
|
||||
- UI : **Gruvbox vintage dark**, moderne (ombres, arrondis, typo lisible), responsive. icon fa
|
||||
- Déploiement : test en mode .env puis deploiement final dans **docker-compose** (backend + frontend) + cron/worker pour scrapes périodiques.
|
||||
- Repo : sur mon serveur Gitea, nom : **suivi_produits** : https://gitea.maison43.duckdns.org/gilles/suivi_produit
|
||||
|
||||
---
|
||||
|
||||
## Objectif produit (fonctionnel)
|
||||
Application self-hosted pour suivre l’évolution de produits Amazon.fr (puis autres stores). L’utilisateur ajoute des URLs de produits, l’app :
|
||||
- scrape les données clés (prix, stock, note, badges, image, etc.)
|
||||
- stocke un **snapshot** à chaque scraping
|
||||
- affiche des **vignettes produit** + **graphique historique** clair (tendance, min/max, %)
|
||||
- propose actions : **Scrap**, **Edit**, **Delete**, **Détail**
|
||||
- lance un **scraping planifié** (cron) sur tous les produits à intervalle défini.
|
||||
|
||||
---
|
||||
|
||||
## Données à capturer (Amazon.fr)
|
||||
### Champs de base (toujours)
|
||||
- `url` (canonique) + `asin`
|
||||
- `title` (nom produit)
|
||||
- `image_main_url` (image principale)
|
||||
- `price_current` (valeur numérique + devise)
|
||||
- `stock_status` (texte) + `in_stock` (bool)
|
||||
- `rating_value` (float) + `rating_count` (int)
|
||||
|
||||
### Champs conditionnels (si présents)
|
||||
- `price_list` / prix conseillé / prix barré (si affiché)
|
||||
- `discount_percent` (si affiché ou calculé)
|
||||
- `lowest_30d_price` (si mention “prix le plus bas des 30 derniers jours”)
|
||||
- `amazon_choice` (badge)
|
||||
- `limited_time_deal` (offre à durée limitée)
|
||||
- `prime_eligible` (badge prime / livraison prime)
|
||||
- `amazon_exclusive` (mention exclusivité)
|
||||
|
||||
### Calculs à faire côté app (pas “inventer”)
|
||||
> Important : **ne pas “calculer une réduction” si le champ source n’existe pas**. on ne calcule rien sauf demande explicite de ma part ( peut etre tendance dans courbe historique)
|
||||
|
||||
---
|
||||
|
||||
## Méthode de scraping (sûre/efficace)
|
||||
### Stratégie Playwright
|
||||
- Navigateur Chromium.
|
||||
- Context :
|
||||
- locale `fr-FR`
|
||||
- timezone `Europe/Paris`
|
||||
- viewport réaliste (ex. 1366×768 ou 1920×1080)
|
||||
- user-agent récent
|
||||
- Rythme : faible (15–20 produits/jour) + **delays aléatoires** 1–3s entre pages.
|
||||
- Détection blocages :
|
||||
- si page contient captcha / robot-check → marquer scrap “blocked” + screenshot + html dump pour debug.
|
||||
- Résilience :
|
||||
- retry 1–2 fois max avec backoff, sinon échec contrôlé.
|
||||
|
||||
### Sélecteurs (approche robuste)
|
||||
- Priorité : **IDs stables** (ex : `#productTitle`, `#acrCustomerReviewText`, `#availability`)
|
||||
- Prix : gérer variantes (prix fractionné, promo, etc.)
|
||||
- Fallback : si sélecteur absent, log “missing field”, ne pas planter.
|
||||
|
||||
### Artifacts de debug
|
||||
À chaque scrap :
|
||||
- sauvegarder un JSON “raw” normalisé
|
||||
- en cas d’échec : `page.screenshot()` + `page.content()` dans un dossier `debug/` horodaté.
|
||||
|
||||
---
|
||||
|
||||
## Architecture cible
|
||||
### Backend
|
||||
- API HTTP (proposé : **FastAPI**) :
|
||||
- CRUD produits
|
||||
- déclenchement scrap (produit / tous)
|
||||
- lecture historique + agrégats (min/max/tendance)
|
||||
- lecture/écriture configs frontend/backend
|
||||
- Worker de scraping (Playwright) séparé en module “scraper”
|
||||
- Scheduler (cron interne ou cron container) qui appelle `scrape_all`
|
||||
|
||||
### Frontend
|
||||
- SPA simple (proposé : **Vite + React** ou **Svelte**) ou HTML server-side minimal (selon simplicité).
|
||||
- Thème **Gruvbox vintage dark** :
|
||||
- fond #282828, cartes #3c3836, texte #ebdbb2
|
||||
- accent orange #fe8019, jaune #fabd2f, vert #b8bb26
|
||||
- Responsive : nombre de colonnes configurable.
|
||||
- utilisation de popup lors de l'ajout de produit ou acces a setting
|
||||
### Stockage
|
||||
- le stockage se fais uniquement lors de l'enregistrement du produit
|
||||
- SQLite : tables normalisées (produits, snapshots, tags/catégories/types)
|
||||
- JSON “raw” : archivage optionnel (dossier `data/raw/YYYY-MM/...json`)
|
||||
@@ -1,7 +1,22 @@
|
||||
# TODO
|
||||
- [ ] init repo pour backend FastAPI + logging + config
|
||||
- [ ] créer scraper Amazon via Playwright avec parser robuste
|
||||
- [ ] définir UI Gruvbox + carte produit + graphique 30j
|
||||
- [ ] intégrer scheduler APScheduler pour `scrape_all`
|
||||
|
||||
## Phase 1 - Backend & Scraper (TERMINÉ ✓)
|
||||
- [x] init repo pour backend FastAPI + logging + config
|
||||
- [x] créer scraper Amazon via Playwright avec parser robuste
|
||||
- [x] modèle SQLAlchemy (products, product_snapshots, scrape_runs)
|
||||
- [x] API CRUD produits + endpoints scraping
|
||||
- [x] tests unitaires parser, normalisation, pricing
|
||||
- [x] intégrer scheduler APScheduler pour `scrape_all`
|
||||
|
||||
## Phase 2 - Frontend (EN COURS)
|
||||
- [ ] connecter App.jsx à l'API backend (fetch produits)
|
||||
- [ ] implémenter ProductCard avec données réelles
|
||||
- [ ] ajouter formulaire d'ajout de produit (URL Amazon)
|
||||
- [ ] graphique Chart.js historique 30j
|
||||
- [ ] store Zustand pour état global
|
||||
|
||||
## Phase 3 - Industrialisation
|
||||
- [ ] dockeriser backend + frontend + scheduler
|
||||
- [ ] ajouter page debug/logs affichant tables SQLite
|
||||
- [ ] docker-compose avec volumes persistants
|
||||
- [ ] page debug/logs affichant tables SQLite
|
||||
- [ ] tests E2E frontend
|
||||
|
||||
@@ -2,3 +2,9 @@
|
||||
from .routes_config import router as config_router
|
||||
from .routes_products import router as products_router
|
||||
from .routes_scrape import router as scrape_router
|
||||
|
||||
__all__ = [
|
||||
"config_router",
|
||||
"products_router",
|
||||
"scrape_router",
|
||||
]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
|
||||
from backend.app.core.config import BackendConfig, CONFIG_PATH, load_config
|
||||
|
||||
@@ -31,6 +31,6 @@
|
||||
<div id="acBadge_feature_div">Choix d'Amazon</div>
|
||||
<div id="dealBadge_feature_div">Offre a duree limitee</div>
|
||||
|
||||
<div>Exclusivite Amazon</div>
|
||||
<div>Exclusivité Amazon</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 940 KiB After Width: | Height: | Size: 959 KiB |
@@ -14,12 +14,12 @@
|
||||
"prix_actuel": 1259.99,
|
||||
"prix_conseille": 1699.99,
|
||||
"prix_min_30j": null,
|
||||
"prix_conseille_reduction": null,
|
||||
"prix_min_30j_reduction": -26,
|
||||
"prix_conseille_reduction": -26,
|
||||
"prix_min_30j_reduction": null,
|
||||
"etat_stock": "En stock",
|
||||
"en_stock": true,
|
||||
"note": 4.7,
|
||||
"nombre_avis": 7,
|
||||
"nombre_avis": 8,
|
||||
"choix_amazon": null,
|
||||
"offre_limitee": null,
|
||||
"prime": null,
|
||||
@@ -110,8 +110,8 @@
|
||||
"Disponibilité des pièces détachées": "5 Ans",
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"ASIN": "B0DQ8M74KL",
|
||||
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (7) 4,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "4 884 en Informatique ( Voir les 100 premiers en Informatique ) 127 en Ordinateurs portables classiques",
|
||||
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (8) 4,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "1 977 en Informatique ( Voir les 100 premiers en Informatique ) 47 en Ordinateurs portables classiques",
|
||||
"Date de mise en ligne sur Amazon.fr": "1 juillet 2025"
|
||||
}
|
||||
},
|
||||
@@ -119,7 +119,7 @@
|
||||
"statut": "succes",
|
||||
"erreurs": [],
|
||||
"notes": [
|
||||
"Optionnels manquants: prix_conseille_reduction, prix_min_30j, prime, choix_amazon, offre_limitee",
|
||||
"Optionnels manquants: prix_min_30j, prix_min_30j_reduction, prime, choix_amazon, offre_limitee",
|
||||
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-001_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-001_capture.html'}"
|
||||
]
|
||||
}
|
||||
@@ -197,7 +197,7 @@
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"ASIN": "B0F32N1ZGH",
|
||||
"Moyenne des commentaires client": "4,6 4,6 sur 5 étoiles (211) 4,6 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "480 en Informatique ( Voir les 100 premiers en Informatique ) 6 en Cartes graphiques",
|
||||
"Classement des meilleures ventes d'Amazon": "417 en Informatique ( Voir les 100 premiers en Informatique ) 7 en Cartes graphiques",
|
||||
"Date de mise en ligne sur Amazon.fr": "16 avril 2025"
|
||||
}
|
||||
},
|
||||
@@ -224,12 +224,12 @@
|
||||
"prix_actuel": 249.99,
|
||||
"prix_conseille": 329.99,
|
||||
"prix_min_30j": null,
|
||||
"prix_conseille_reduction": null,
|
||||
"prix_min_30j_reduction": -24,
|
||||
"prix_conseille_reduction": -24,
|
||||
"prix_min_30j_reduction": null,
|
||||
"etat_stock": null,
|
||||
"en_stock": null,
|
||||
"note": 4.7,
|
||||
"nombre_avis": 967,
|
||||
"nombre_avis": 969,
|
||||
"choix_amazon": null,
|
||||
"offre_limitee": null,
|
||||
"prime": null,
|
||||
@@ -282,8 +282,8 @@
|
||||
"Disponibilité des pièces détachées": "Information indisponible sur les pièces détachées",
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"ASIN": "B0DWFLPMM5",
|
||||
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (967) 4,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "42 en Informatique ( Voir les 100 premiers en Informatique ) 2 en SSD internes",
|
||||
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (969) 4,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "44 en Informatique ( Voir les 100 premiers en Informatique ) 2 en SSD internes",
|
||||
"Date de mise en ligne sur Amazon.fr": "10 mars 2025"
|
||||
}
|
||||
},
|
||||
@@ -291,7 +291,7 @@
|
||||
"statut": "succes",
|
||||
"erreurs": [],
|
||||
"notes": [
|
||||
"Optionnels manquants: prix_conseille_reduction, prix_min_30j, etat_stock, en_stock, prime, choix_amazon, offre_limitee, exclusivite_amazon",
|
||||
"Optionnels manquants: prix_min_30j, prix_min_30j_reduction, etat_stock, en_stock, prime, choix_amazon, offre_limitee, exclusivite_amazon",
|
||||
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-003_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-003_capture.html'}"
|
||||
]
|
||||
}
|
||||
@@ -315,7 +315,7 @@
|
||||
"etat_stock": "En stock",
|
||||
"en_stock": true,
|
||||
"note": 4.8,
|
||||
"nombre_avis": 28247,
|
||||
"nombre_avis": 28260,
|
||||
"choix_amazon": true,
|
||||
"offre_limitee": null,
|
||||
"prime": null,
|
||||
@@ -364,8 +364,8 @@
|
||||
"Disponibilité des pièces détachées": "2 Ans",
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"ASIN": "B07RW6Z692",
|
||||
"Moyenne des commentaires client": "4,8 4,8 sur 5 étoiles (28 247) 4,8 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "210 en Informatique ( Voir les 100 premiers en Informatique ) 3 en Mémoire RAM",
|
||||
"Moyenne des commentaires client": "4,8 4,8 sur 5 étoiles (28 260) 4,8 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "258 en Informatique ( Voir les 100 premiers en Informatique ) 4 en Mémoire RAM",
|
||||
"Date de mise en ligne sur Amazon.fr": "8 novembre 2018"
|
||||
}
|
||||
},
|
||||
@@ -397,7 +397,7 @@
|
||||
"etat_stock": null,
|
||||
"en_stock": null,
|
||||
"note": 4.7,
|
||||
"nombre_avis": 8626,
|
||||
"nombre_avis": 8629,
|
||||
"choix_amazon": null,
|
||||
"offre_limitee": null,
|
||||
"prime": null,
|
||||
@@ -438,8 +438,8 @@
|
||||
"Disponibilité des pièces détachées": "2 Ans",
|
||||
"Mises à jour logicielles garanties jusqu’à": "13 avril 2030",
|
||||
"ASIN": "B08GSTF5NJ",
|
||||
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (8 626) 4,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "2 130 en Informatique ( Voir les 100 premiers en Informatique ) 41 en Mémoire RAM",
|
||||
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (8 629) 4,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "3 086 en Informatique ( Voir les 100 premiers en Informatique ) 61 en Mémoire RAM",
|
||||
"Date de mise en ligne sur Amazon.fr": "25 août 2020"
|
||||
}
|
||||
},
|
||||
@@ -464,14 +464,14 @@
|
||||
"titre": "MSI Modern MD2412P Écran Bureautique 23.8\" Full HD - Dalle IPS 1920x1080, 100Hz, Confort Oculaire, Montage VESA, Haut-Parleurs Intégrés, Display Kit, Réglable 4 Directions - HDMI 1.4b, USB Type-C",
|
||||
"url_image_principale": "https://m.media-amazon.com/images/I/71bjTFOkcDL._AC_SX679_.jpg",
|
||||
"prix_actuel": 119.99,
|
||||
"prix_conseille": 149.99,
|
||||
"prix_min_30j": null,
|
||||
"prix_conseille": null,
|
||||
"prix_min_30j": 149.99,
|
||||
"prix_conseille_reduction": null,
|
||||
"prix_min_30j_reduction": -20,
|
||||
"etat_stock": "En stock",
|
||||
"en_stock": true,
|
||||
"note": 4.5,
|
||||
"nombre_avis": 3872,
|
||||
"nombre_avis": 3878,
|
||||
"choix_amazon": true,
|
||||
"offre_limitee": true,
|
||||
"prime": null,
|
||||
@@ -526,8 +526,8 @@
|
||||
"Disponibilité des pièces détachées": "Information indisponible sur les pièces détachées",
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"ASIN": "B0CB4FDJT5",
|
||||
"Moyenne des commentaires client": "4,5 4,5 sur 5 étoiles (3 872) 4,5 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "43 en Informatique ( Voir les 100 premiers en Informatique ) 1 en Écrans PC",
|
||||
"Moyenne des commentaires client": "4,5 4,5 sur 5 étoiles (3 878) 4,5 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "40 en Informatique ( Voir les 100 premiers en Informatique ) 1 en Écrans PC",
|
||||
"Date de mise en ligne sur Amazon.fr": "4 juillet 2023"
|
||||
}
|
||||
},
|
||||
@@ -535,7 +535,7 @@
|
||||
"statut": "succes",
|
||||
"erreurs": [],
|
||||
"notes": [
|
||||
"Optionnels manquants: prix_conseille_reduction, prix_min_30j, prime, exclusivite_amazon",
|
||||
"Optionnels manquants: prix_conseille, prix_conseille_reduction, prime, exclusivite_amazon",
|
||||
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-006_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-006_capture.html'}"
|
||||
]
|
||||
}
|
||||
@@ -552,8 +552,8 @@
|
||||
"titre": "UGREEN Revodok Pro Docking Station USB C 2 DisplayPort 4K 120Hz et 1 HDMI 4K 60Hz Triple Écran Extension 14 en 1 Hub Station d'Accueil avec Multi USB 10Gbps Ethernet Gigabit 100W PD Charge SD TF 3.5mm",
|
||||
"url_image_principale": "https://m.media-amazon.com/images/I/61AuBDPwDvL._AC_SY355_.jpg",
|
||||
"prix_actuel": 118.99,
|
||||
"prix_conseille": 127.49,
|
||||
"prix_min_30j": null,
|
||||
"prix_conseille": 169.99,
|
||||
"prix_min_30j": 127.49,
|
||||
"prix_conseille_reduction": -30,
|
||||
"prix_min_30j_reduction": -7,
|
||||
"etat_stock": "En stock",
|
||||
@@ -588,7 +588,7 @@
|
||||
"Moyenne des commentaires client": "4,6 4,6 sur 5 étoiles (46) 4,6 sur 5 étoiles",
|
||||
"Numéro du modèle de l'article": "CM843",
|
||||
"ASIN": "B0FGHX59G2",
|
||||
"Classement des meilleures ventes d'Amazon": "32 en Stations d'accueil pour ordinateur portable",
|
||||
"Classement des meilleures ventes d'Amazon": "35 en Stations d'accueil pour ordinateur portable",
|
||||
"Date de mise en ligne sur Amazon.fr": "25 août 2025"
|
||||
}
|
||||
},
|
||||
@@ -596,7 +596,7 @@
|
||||
"statut": "succes",
|
||||
"erreurs": [],
|
||||
"notes": [
|
||||
"Optionnels manquants: description, prix_min_30j, prime, choix_amazon, exclusivite_amazon",
|
||||
"Optionnels manquants: description, prime, choix_amazon, exclusivite_amazon",
|
||||
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-007_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-007_capture.html'}"
|
||||
]
|
||||
}
|
||||
@@ -620,7 +620,7 @@
|
||||
"etat_stock": "En stock",
|
||||
"en_stock": true,
|
||||
"note": 4.2,
|
||||
"nombre_avis": 3713,
|
||||
"nombre_avis": 3716,
|
||||
"choix_amazon": null,
|
||||
"offre_limitee": null,
|
||||
"prime": null,
|
||||
@@ -656,10 +656,10 @@
|
||||
"Garantie constructeur": "2 ans constructeur",
|
||||
"Disponibilité des pièces détachées": "Information indisponible sur les pièces détachées",
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"Moyenne des commentaires client": "4,2 4,2 sur 5 étoiles (3 713) 4,2 sur 5 étoiles",
|
||||
"Moyenne des commentaires client": "4,2 4,2 sur 5 étoiles (3 716) 4,2 sur 5 étoiles",
|
||||
"Numéro du modèle de l'article": "A1336",
|
||||
"ASIN": "B0BYNZXFM2",
|
||||
"Classement des meilleures ventes d'Amazon": "16 846 en High-Tech ( Voir les 100 premiers en High-Tech ) 347 en Blocs d'alimentation portatifs pour téléphone portable",
|
||||
"Classement des meilleures ventes d'Amazon": "16 306 en High-Tech ( Voir les 100 premiers en High-Tech ) 333 en Blocs d'alimentation portatifs pour téléphone portable",
|
||||
"Date de mise en ligne sur Amazon.fr": "31 juillet 2023"
|
||||
}
|
||||
},
|
||||
@@ -731,7 +731,7 @@
|
||||
"Mises à jour logicielles garanties jusqu’à": "Information non disponible",
|
||||
"ASIN": "B0CWLSQ8FS",
|
||||
"Moyenne des commentaires client": "3,7 3,7 sur 5 étoiles (27) 3,7 sur 5 étoiles",
|
||||
"Classement des meilleures ventes d'Amazon": "7 941 en Informatique ( Voir les 100 premiers en Informatique ) 176 en Mémoire RAM",
|
||||
"Classement des meilleures ventes d'Amazon": "10 729 en Informatique ( Voir les 100 premiers en Informatique ) 218 en Mémoire RAM",
|
||||
"Date de mise en ligne sur Amazon.fr": "30 juillet 2024"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -214,15 +214,17 @@ def extract_product_data_from_html(html: str, url: str) -> dict[str, Any]:
|
||||
if not price_text:
|
||||
price_text = _safe_attr_soup(soup, "#twister-plus-price-data-price", "value")
|
||||
|
||||
price_list_text = _safe_text_soup(
|
||||
soup, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen"
|
||||
)
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text_soup(soup, "#priceblock_strikeprice")
|
||||
# prix conseillé (srpPriceBlock = "Prix conseillé : XXX €")
|
||||
price_list_text = _safe_text_soup(soup, ".srpPriceBlock .srpPriceBlockAUI .a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text_soup(soup, ".srpPriceBlock .a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text_soup(soup, ".srpPriceBlockAUI .a-offscreen")
|
||||
price_list_text = _safe_text_soup(soup, "#priceblock_strikeprice")
|
||||
# fallback sur corePriceDisplay (prix barré) si pas de srpPriceBlock
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text_soup(
|
||||
soup, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen"
|
||||
)
|
||||
|
||||
stock_text = _safe_text_soup(soup, "#availability span")
|
||||
if not stock_text:
|
||||
@@ -252,25 +254,33 @@ def extract_product_data_from_html(html: str, url: str) -> dict[str, Any]:
|
||||
prime_eligible = True
|
||||
amazon_exclusive = "Exclusivité Amazon" if "Exclusivité Amazon" in soup.get_text() else None
|
||||
|
||||
# prix plus bas 30 jours (basisPrice avec mention "30 jours")
|
||||
lowest_30d_text = _extract_lowest_30d_text_soup(soup)
|
||||
lowest_30d_price = None
|
||||
if lowest_30d_text:
|
||||
lowest_30d_price = parse_price_fr(lowest_30d_text)
|
||||
if lowest_30d_price is not None:
|
||||
candidate_list = parse_price_fr(price_list_text)
|
||||
if candidate_list == lowest_30d_price:
|
||||
price_list_text = None
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text_soup(soup, ".srpPriceBlock .a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text_soup(soup, ".srpPriceBlockAUI .a-offscreen")
|
||||
|
||||
# si le prix conseillé == prix min 30j, c'est une erreur de détection
|
||||
# (le prix barré dans corePriceDisplay est en fait le prix min 30j, pas le conseillé)
|
||||
price_list_value = parse_price_fr(price_list_text)
|
||||
if price_list_value is not None and lowest_30d_price is not None and price_list_value == lowest_30d_price:
|
||||
price_list_text = None
|
||||
price_list_value = None
|
||||
|
||||
# réductions
|
||||
reduction_savings_text = _safe_text_soup(
|
||||
soup, "#corePriceDisplay_desktop_feature_div .savingsPercentage"
|
||||
)
|
||||
reduction_conseille_text = _safe_text_soup(soup, ".srpSavingsPercentageBlock")
|
||||
reduction_min_30j = _parse_percent(reduction_savings_text)
|
||||
reduction_conseille = _parse_percent(reduction_conseille_text)
|
||||
|
||||
# attribuer correctement les réductions selon ce qui est présent
|
||||
# - si prix min 30j présent, savingsPercentage = réduction par rapport au min 30j
|
||||
# - si prix conseillé présent (srpPriceBlock), srpSavingsPercentageBlock = réduction par rapport au conseillé
|
||||
reduction_min_30j = _parse_percent(reduction_savings_text) if lowest_30d_price is not None else None
|
||||
reduction_conseille = _parse_percent(reduction_conseille_text) if price_list_value is not None else None
|
||||
# si pas de srpSavingsPercentageBlock mais un savingsPercentage et un prix conseillé (sans min 30j)
|
||||
if reduction_conseille is None and price_list_value is not None and lowest_30d_price is None:
|
||||
reduction_conseille = _parse_percent(reduction_savings_text)
|
||||
|
||||
a_propos = _extract_about_bullets(soup)
|
||||
description = _extract_description(soup)
|
||||
@@ -285,7 +295,7 @@ def extract_product_data_from_html(html: str, url: str) -> dict[str, Any]:
|
||||
"titre": title,
|
||||
"url_image_principale": image_main_url,
|
||||
"prix_actuel": parse_price_fr(price_text),
|
||||
"prix_conseille": parse_price_fr(price_list_text),
|
||||
"prix_conseille": price_list_value,
|
||||
"prix_min_30j": lowest_30d_price,
|
||||
"prix_conseille_reduction": reduction_conseille,
|
||||
"prix_min_30j_reduction": reduction_min_30j,
|
||||
@@ -333,14 +343,15 @@ def extract_product_data(page: Page, url: str) -> dict[str, Any]:
|
||||
if not price_text:
|
||||
price_text = _safe_attr(page, "#twister-plus-price-data-price", "value")
|
||||
|
||||
# prix barré / conseillé
|
||||
price_list_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text(page, "#priceblock_strikeprice")
|
||||
# prix conseillé (srpPriceBlock = "Prix conseillé : XXX €")
|
||||
price_list_text = _safe_text(page, ".srpPriceBlock .srpPriceBlockAUI .a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text(page, ".srpPriceBlock .a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text(page, ".srpPriceBlockAUI .a-offscreen")
|
||||
price_list_text = _safe_text(page, "#priceblock_strikeprice")
|
||||
# fallback sur corePriceDisplay (prix barré) si pas de srpPriceBlock
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen")
|
||||
|
||||
# stock
|
||||
stock_text = _safe_text(page, "#availability span")
|
||||
@@ -374,34 +385,44 @@ def extract_product_data(page: Page, url: str) -> dict[str, Any]:
|
||||
|
||||
amazon_exclusive = _safe_text(page, "text=Exclusivité Amazon")
|
||||
|
||||
# prix plus bas 30 jours
|
||||
# prix plus bas 30 jours (basisPrice ou corePriceDisplay avec mention "30 jours")
|
||||
lowest_30d_text = None
|
||||
lowest_30d_price = None
|
||||
if page.locator(".basisPrice").count() > 0:
|
||||
basis_text = page.locator(".basisPrice").first.inner_text()
|
||||
if basis_text and re.search(r"prix.+(30|trente).+jour", basis_text.lower()):
|
||||
lowest_30d_text = _safe_text(page, ".basisPrice .a-offscreen") or basis_text
|
||||
if not lowest_30d_text and page.locator("#priceBadging_feature_div").count() > 0:
|
||||
lowest_30d_text = _safe_text(page, ".basisPrice .a-price .a-offscreen") or basis_text
|
||||
lowest_30d_price = parse_price_fr(lowest_30d_text)
|
||||
# fallback sur corePriceDisplay si contient mention 30 jours
|
||||
if lowest_30d_price is None and page.locator("#corePriceDisplay_desktop_feature_div .a-text-price").count() > 0:
|
||||
core_text = page.locator("#corePriceDisplay_desktop_feature_div").first.inner_text()
|
||||
if core_text and re.search(r"prix.+(30|trente).+jour", core_text.lower()):
|
||||
lowest_30d_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .a-text-price .a-offscreen")
|
||||
lowest_30d_price = parse_price_fr(lowest_30d_text)
|
||||
if not lowest_30d_price and page.locator("#priceBadging_feature_div").count() > 0:
|
||||
badging_text = page.locator("#priceBadging_feature_div").first.inner_text()
|
||||
if badging_text and re.search(r"prix.+(30|trente).+jour", badging_text.lower()):
|
||||
lowest_30d_text = _safe_text(page, "#priceBadging_feature_div .a-offscreen") or badging_text
|
||||
if lowest_30d_text and not re.search(r"prix.+(30|trente).+jour", lowest_30d_text.lower()):
|
||||
lowest_30d_text = None
|
||||
lowest_30d_price = None
|
||||
if lowest_30d_text and "prix" in lowest_30d_text.lower():
|
||||
lowest_30d_price = parse_price_fr(lowest_30d_text)
|
||||
if lowest_30d_price is not None:
|
||||
candidate_list = parse_price_fr(price_list_text)
|
||||
if candidate_list == lowest_30d_price:
|
||||
price_list_text = None
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text(page, ".srpPriceBlock .a-offscreen")
|
||||
if not price_list_text:
|
||||
price_list_text = _safe_text(page, ".srpPriceBlockAUI .a-offscreen")
|
||||
lowest_30d_price = parse_price_fr(lowest_30d_text)
|
||||
|
||||
# si le prix conseillé == prix min 30j, c'est une erreur de détection
|
||||
price_list_value = parse_price_fr(price_list_text)
|
||||
if price_list_value is not None and lowest_30d_price is not None and price_list_value == lowest_30d_price:
|
||||
price_list_text = None
|
||||
price_list_value = None
|
||||
|
||||
# réductions
|
||||
# savingsPercentage dans corePriceDisplay = réduction par rapport au prix min 30j (si présent)
|
||||
# srpSavingsPercentageBlock = réduction par rapport au prix conseillé
|
||||
reduction_savings_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .savingsPercentage")
|
||||
reduction_conseille_text = _safe_text(page, ".srpSavingsPercentageBlock")
|
||||
reduction_min_30j = _parse_percent(reduction_savings_text)
|
||||
reduction_conseille = _parse_percent(reduction_conseille_text)
|
||||
|
||||
# attribuer correctement les réductions selon ce qui est présent
|
||||
reduction_min_30j = _parse_percent(reduction_savings_text) if lowest_30d_price is not None else None
|
||||
reduction_conseille = _parse_percent(reduction_conseille_text) if price_list_value is not None else None
|
||||
# si pas de srpSavingsPercentageBlock mais un savingsPercentage et un prix conseillé (sans min 30j)
|
||||
if reduction_conseille is None and price_list_value is not None and lowest_30d_price is None:
|
||||
reduction_conseille = _parse_percent(reduction_savings_text)
|
||||
|
||||
asin = _safe_attr(page, "input#ASIN", "value") or _extract_asin_from_url(url)
|
||||
|
||||
@@ -417,7 +438,7 @@ def extract_product_data(page: Page, url: str) -> dict[str, Any]:
|
||||
"titre": title,
|
||||
"url_image_principale": image_main_url,
|
||||
"prix_actuel": parse_price_fr(price_text),
|
||||
"prix_conseille": parse_price_fr(price_list_text),
|
||||
"prix_conseille": price_list_value,
|
||||
"prix_min_30j": lowest_30d_price,
|
||||
"prix_conseille_reduction": reduction_conseille,
|
||||
"prix_min_30j_reduction": reduction_min_30j,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.async_api import async_playwright, Browser, BrowserContext
|
||||
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from loguru import logger
|
||||
from playwright.sync_api import sync_playwright
|
||||
from loguru import logger # noqa: E402
|
||||
from playwright.sync_api import sync_playwright # noqa: E402
|
||||
|
||||
from backend.app.core.config import load_config
|
||||
from backend.app.scraper.amazon.parser import extract_product_data
|
||||
from backend.app.core.config import load_config # noqa: E402
|
||||
from backend.app.scraper.amazon.parser import extract_product_data # noqa: E402
|
||||
|
||||
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "samples"
|
||||
TESTS_PATH = SAMPLES_DIR / "scrape_test.json"
|
||||
|
||||
@@ -153,7 +153,7 @@ def scrape_product(product_id: int) -> None:
|
||||
# fermeture propre du navigateur
|
||||
context.close()
|
||||
browser.close()
|
||||
except Exception as exc: # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
logger.exception("Erreur pendant le scraping de %s", product_id)
|
||||
_finalize_run(run, session, "erreur")
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Plan Frontend Phase 2 - suivi_produits
|
||||
|
||||
**Date** : 2026-01-18
|
||||
**Statut** : Validé
|
||||
**Priorité** : Fonctionnel d'abord, puis visualisation
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
Le backend (Phase 1) est terminé :
|
||||
- Scraper Playwright fonctionnel (9/9 produits OK)
|
||||
- API FastAPI avec CRUD produits complet
|
||||
- Modèles SQLAlchemy (products, snapshots, scrape_runs)
|
||||
- Scheduler APScheduler
|
||||
- Tests unitaires (7 tests OK)
|
||||
|
||||
Le frontend est squelettique : App.jsx basique, ProductCard vide, pas de store.
|
||||
|
||||
---
|
||||
|
||||
## Décisions de design
|
||||
|
||||
| Question | Choix |
|
||||
|----------|-------|
|
||||
| Priorité | Fonctionnel d'abord |
|
||||
| Ajout produit | URL seule (backend extrait ASIN + scrape immédiat) |
|
||||
| State management | Zustand |
|
||||
| Page debug | Dès le début (utile pendant le dev) |
|
||||
| Routing | React Router (évolutivité multi-stores) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Frontend
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── api/
|
||||
│ └── client.js # API calls (extensible par store)
|
||||
├── components/
|
||||
│ ├── common/ # Composants réutilisables
|
||||
│ │ ├── Modal.jsx
|
||||
│ │ ├── Badge.jsx
|
||||
│ │ └── PriceDisplay.jsx
|
||||
│ ├── products/
|
||||
│ │ ├── ProductCard.jsx
|
||||
│ │ ├── ProductGrid.jsx
|
||||
│ │ └── AddProductForm.jsx
|
||||
│ └── debug/
|
||||
│ ├── DbTable.jsx
|
||||
│ └── LogViewer.jsx
|
||||
├── pages/
|
||||
│ ├── HomePage.jsx # Grille produits
|
||||
│ ├── DebugPage.jsx # Tables + logs
|
||||
│ └── SettingsPage.jsx # (futur) Config frontend/backend
|
||||
├── stores/
|
||||
│ └── useProductStore.js
|
||||
├── hooks/ # (futur) Custom hooks
|
||||
├── styles/
|
||||
│ ├── _variables.scss # Couleurs Gruvbox
|
||||
│ └── global.scss
|
||||
├── App.jsx # React Router setup
|
||||
└── main.jsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Endpoint | Méthode | Statut | Description |
|
||||
|----------|---------|--------|-------------|
|
||||
| `/products` | GET | Existe | Liste paginée |
|
||||
| `/products` | POST | Existe | Créer produit |
|
||||
| `/products/{id}` | GET/PUT/DELETE | Existe | CRUD |
|
||||
| `/products/{id}/snapshots` | GET | À créer | Historique prix |
|
||||
| `/products/{id}/scrape` | POST | À vérifier | Scrape un produit |
|
||||
| `/scrape/all` | POST | À vérifier | Scrape tous |
|
||||
| `/debug/tables` | GET | À créer | Dump tables SQLite |
|
||||
| `/debug/logs` | GET | À créer | Derniers logs scrap |
|
||||
|
||||
---
|
||||
|
||||
## Plan d'implémentation
|
||||
|
||||
### Étape 1 : Page Debug (fondation)
|
||||
**Objectif** : Avoir une vue sur les données pendant le développement
|
||||
|
||||
Backend :
|
||||
- [ ] Créer endpoint `GET /debug/tables` (dump products, snapshots, scrape_runs)
|
||||
- [ ] Créer endpoint `GET /debug/logs` (lecture `backend/logs/scrap.log`)
|
||||
|
||||
Frontend :
|
||||
- [ ] Installer react-router-dom
|
||||
- [ ] Setup React Router avec 2 routes (/, /debug)
|
||||
- [ ] Créer `DebugPage.jsx` avec composants DbTable et LogViewer
|
||||
- [ ] Afficher les 3 tables SQLite dans des sections distinctes
|
||||
- [ ] Afficher les logs JSON de scraping
|
||||
|
||||
---
|
||||
|
||||
### Étape 2 : Store Zustand + Liste produits
|
||||
**Objectif** : Connexion frontend-backend fonctionnelle
|
||||
|
||||
Frontend :
|
||||
- [ ] Installer zustand
|
||||
- [ ] Créer `useProductStore.js` avec actions (fetch, add, delete, scrape)
|
||||
- [ ] Enrichir `client.js` avec toutes les fonctions fetch
|
||||
- [ ] Connecter `HomePage.jsx` au store
|
||||
- [ ] Afficher les produits existants (version basique)
|
||||
|
||||
---
|
||||
|
||||
### Étape 3 : Ajout de produit
|
||||
**Objectif** : Permettre d'ajouter un produit via URL Amazon
|
||||
|
||||
Backend :
|
||||
- [ ] Modifier `POST /products` pour accepter juste l'URL
|
||||
- [ ] Extraire automatiquement l'ASIN de l'URL
|
||||
- [ ] Déclencher scrape automatique après création
|
||||
|
||||
Frontend :
|
||||
- [ ] Créer `Modal.jsx` (composant réutilisable)
|
||||
- [ ] Créer `AddProductForm.jsx` (input URL + validation)
|
||||
- [ ] Intégrer dans Header avec bouton "Add Product"
|
||||
- [ ] Gestion loading/error dans l'UI
|
||||
- [ ] Refresh automatique après ajout
|
||||
|
||||
---
|
||||
|
||||
### Étape 4 : Actions sur produit
|
||||
**Objectif** : Pouvoir scraper et supprimer un produit
|
||||
|
||||
Backend :
|
||||
- [ ] Vérifier/créer endpoint `POST /products/{id}/scrape`
|
||||
|
||||
Frontend :
|
||||
- [ ] Ajouter boutons Scrap/Delete sur ProductCard
|
||||
- [ ] Modal de confirmation avant suppression
|
||||
- [ ] Feedback visuel pendant le scraping (spinner)
|
||||
- [ ] Refresh automatique après action
|
||||
|
||||
---
|
||||
|
||||
### Étape 5 : Amélioration visuelle ProductCard
|
||||
**Objectif** : Vignette produit complète selon le schéma CLAUDE.md
|
||||
|
||||
Frontend :
|
||||
- [ ] Image non tronquée (object-fit: contain)
|
||||
- [ ] Section prix (actuel, conseillé, réduction)
|
||||
- [ ] Badges (Prime, Choix Amazon, Deal, Exclusivité)
|
||||
- [ ] Note + nombre d'avis
|
||||
- [ ] Stock status
|
||||
- [ ] Responsive grid (colonnes configurables via config_frontend.json)
|
||||
|
||||
---
|
||||
|
||||
### Étape 6 : Graphique historique (Phase 2.5)
|
||||
**Objectif** : Visualiser l'évolution des prix sur 30 jours
|
||||
|
||||
Backend :
|
||||
- [ ] Créer endpoint `GET /products/{id}/snapshots`
|
||||
|
||||
Frontend :
|
||||
- [ ] Installer chart.js + react-chartjs-2
|
||||
- [ ] Créer composant PriceChart
|
||||
- [ ] Intégrer dans ProductCard
|
||||
- [ ] Afficher min/max/tendance sous le graphique
|
||||
- [ ] Couleurs selon tendance (vert baisse, orange stable, rouge hausse)
|
||||
|
||||
---
|
||||
|
||||
## Thème visuel
|
||||
|
||||
Gruvbox vintage dark (défini dans CLAUDE.md) :
|
||||
- Fond : #282828
|
||||
- Cartes : #3c3836
|
||||
- Texte : #ebdbb2
|
||||
- Accent orange : #fe8019
|
||||
- Accent jaune : #fabd2f
|
||||
- Accent vert : #b8bb26
|
||||
|
||||
Icônes : Font Awesome
|
||||
|
||||
---
|
||||
|
||||
## Critères de succès Phase 2
|
||||
|
||||
- [ ] Un utilisateur peut ajouter un produit via URL Amazon
|
||||
- [ ] Le scraping se déclenche automatiquement
|
||||
- [ ] Les produits s'affichent dans une grille responsive
|
||||
- [ ] On peut supprimer un produit
|
||||
- [ ] On peut déclencher un scrape manuel
|
||||
- [ ] La page debug montre l'état des tables et logs
|
||||
@@ -1,15 +1,22 @@
|
||||
# Kanban
|
||||
|
||||
## Backlog
|
||||
- Initialiser FastAPI + SQLite
|
||||
- Parser Amazon + tests
|
||||
- UI vignettes + graphique
|
||||
- Docker Compose setup
|
||||
- Page debug/logs SQLite
|
||||
- Tests E2E frontend
|
||||
|
||||
## Doing
|
||||
- En cours : structure repo
|
||||
- Frontend: connecter App.jsx à l'API
|
||||
- Frontend: ProductCard avec données réelles
|
||||
- Frontend: formulaire ajout produit
|
||||
|
||||
## Review
|
||||
-
|
||||
- Scheduler APScheduler (fonctionnel, à tester en charge)
|
||||
|
||||
## Done
|
||||
-
|
||||
- Backend FastAPI + SQLite + logging
|
||||
- Modèles SQLAlchemy (products, snapshots, runs)
|
||||
- API CRUD produits + endpoints scraping
|
||||
- Scraper Playwright + parser Amazon
|
||||
- Tests unitaires (7 tests OK)
|
||||
- Tests CLI scraper (9/9 produits OK)
|
||||
|
||||