diff --git a/DEMARRAGE.md b/DEMARRAGE.md new file mode 100644 index 0000000..0e24a60 --- /dev/null +++ b/DEMARRAGE.md @@ -0,0 +1,62 @@ +# Guide de Démarrage - Priceflow + +## Problème détecté + +- ❌ Docker non installé/accessible +- ❌ WSL non installé +- ❌ Application non démarrée (port 8555 fermé) + +## Solutions possibles + +### Option 1 : Installer Docker Desktop (Recommandé) + +1. Télécharger Docker Desktop : https://www.docker.com/products/docker-desktop/ +2. Installer et démarrer Docker Desktop +3. Ouvrir un terminal PowerShell dans `C:\Users\Michael\Git\Priceflow` +4. Lancer l'application : + ```bash + docker compose up -d + ``` +5. Vérifier les logs : + ```bash + docker compose logs api | Select-String "site(s)" + ``` + Vous devriez voir : **"10 site(s) de recherche créé(s)"** + +### Option 2 : Installer WSL + Docker + +1. Installer WSL : + ```powershell + wsl --install + ``` +2. Redémarrer l'ordinateur +3. Installer Docker dans WSL +4. Suivre les étapes de l'Option 1 + +### Option 3 : Application déjà démarrée ailleurs + +Si l'application tourne déjà dans un conteneur ou processus différent : + +1. Vérifier les processus : + ```powershell + Get-Process | Where-Object {$_.ProcessName -like "*python*" -or $_.ProcessName -like "*uvicorn*"} + ``` + +2. Vérifier les ports ouverts : + ```powershell + netstat -ano | findstr :8555 + ``` + +## Prochaines étapes après démarrage + +1. ✅ Vérifier que 10 sites sont seedés (logs) +2. ✅ Tester via script : `docker compose exec api python verify_all_sites.py` +3. ✅ Tester via interface : http://localhost:8555/search + +## État actuel + +**Application : ❌ NON DÉMARRÉE** + +- Configuration : ✅ 10 sites ajoutés dans `search_config.py` +- Script de test : ✅ `verify_all_sites.py` créé +- Tests : ⏸️ En attente du démarrage de l'application diff --git a/frontend/src/pages/MultiSearch.jsx b/frontend/src/pages/MultiSearch.jsx index 728951b..a96cf94 100644 --- a/frontend/src/pages/MultiSearch.jsx +++ b/frontend/src/pages/MultiSearch.jsx @@ -2,12 +2,13 @@ import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { Search as SearchIcon, Loader2, ExternalLink, TrendingUp, Package } from 'lucide-react'; +import { Search as SearchIcon, Loader2, ExternalLink, TrendingUp, Package, ChevronDown, ChevronUp } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; const API_URL = '/api'; @@ -17,8 +18,67 @@ export default function MultiSearch() { const [isSearching, setIsSearching] = useState(false); const [progress, setProgress] = useState(null); const [results, setResults] = useState([]); + const [availableSites, setAvailableSites] = useState([]); + const [selectedSiteIds, setSelectedSiteIds] = useState([]); + const [showSiteSelector, setShowSiteSelector] = useState(false); const eventSourceRef = useRef(null); + // Fetch available sites on mount + useEffect(() => { + const fetchSites = async () => { + try { + const response = await axios.get(`${API_URL}/search-sites`); + const sites = response.data.filter(site => site.is_active); + setAvailableSites(sites); + + // Load selection from localStorage or select all by default + const savedSelection = localStorage.getItem('selectedSiteIds'); + if (savedSelection) { + setSelectedSiteIds(JSON.parse(savedSelection)); + } else { + setSelectedSiteIds(sites.map(s => s.id)); + } + } catch (error) { + console.error('Error fetching sites:', error); + toast.error('Erreur lors du chargement des sites'); + } + }; + fetchSites(); + }, []); + + // Save selection to localStorage + useEffect(() => { + if (selectedSiteIds.length > 0) { + localStorage.setItem('selectedSiteIds', JSON.stringify(selectedSiteIds)); + } + }, [selectedSiteIds]); + + const toggleSite = (siteId) => { + setSelectedSiteIds(prev => + prev.includes(siteId) + ? prev.filter(id => id !== siteId) + : [...prev, siteId] + ); + }; + + const selectAll = () => { + setSelectedSiteIds(availableSites.map(s => s.id)); + }; + + const deselectAll = () => { + setSelectedSiteIds([]); + }; + + // Group sites by category + const sitesByCategory = availableSites.reduce((acc, site) => { + const category = site.category || 'Autres'; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(site); + return acc; + }, {}); + const handleSearch = async (e) => { e.preventDefault(); if (!query.trim()) { @@ -26,6 +86,11 @@ export default function MultiSearch() { return; } + if (selectedSiteIds.length === 0) { + toast.error('Veuillez sélectionner au moins un site'); + return; + } + // Fermer l'EventSource précédent si existant if (eventSourceRef.current) { eventSourceRef.current.close(); @@ -35,10 +100,11 @@ export default function MultiSearch() { setResults([]); setProgress({ status: 'searching', total: 0, completed: 0 }); - // Construire l'URL - recherche sur TOUS les sites actifs + // Construire l'URL avec les sites sélectionnés const params = new URLSearchParams({ q: query.trim(), - max_results: 50, // Plus de résultats pour la comparaison + max_results: 50, + sites: selectedSiteIds.join(','), // Include selected sites }); // Créer l'EventSource pour SSE @@ -147,6 +213,65 @@ export default function MultiSearch() { + + {/* Site Selector */} +
+ + + {showSiteSelector && ( +
+ {/* Quick Actions */} +
+ + +
+ + {/* Sites grouped by category */} + {Object.entries(sitesByCategory).map(([category, sites]) => ( +
+

{category}

+
+ {sites.map(site => ( + + ))} +
+
+ ))} +
+ )} +
diff --git a/verify_all_sites.py b/verify_all_sites.py new file mode 100644 index 0000000..c3bc7f9 --- /dev/null +++ b/verify_all_sites.py @@ -0,0 +1,147 @@ +""" +Script de vérification complète du comparateur sur tous les sites configurés +""" +import asyncio +import logging +import sys +import os +from datetime import datetime + +# Add project root to path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "."))) + +from app.core.search_config import SITE_CONFIGS +from app.services.search_service import new_search_service +from app.services.browserless_service import browserless_service + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler()] +) + +logger = logging.getLogger(__name__) + +async def test_site(site_key: str, query: str) -> dict: + """Test a single site and return results""" + config = SITE_CONFIGS[site_key] + result = { + "site": config["name"], + "site_key": site_key, + "category": config["category"], + "success": False, + "count": 0, + "error": None, + "sample_results": [] + } + + try: + print(f"\n🔍 Testing {config['name']} ({site_key})...") + results = await new_search_service.search_site(site_key, query) + + if results: + result["success"] = True + result["count"] = len(results) + # Store first 2 results as samples + result["sample_results"] = [ + { + "title": r.title, + "url": r.url, + "price": r.price, + "in_stock": r.in_stock + } + for r in results[:2] + ] + print(f" ✅ Found {len(results)} results") + for r in results[:2]: + price_str = f"{r.price}€" if r.price else "N/A" + stock_str = "✓" if r.in_stock else "✗" if r.in_stock is False else "?" + print(f" - {r.title[:60]}... [{price_str}] [Stock: {stock_str}]") + else: + print(f" ⚠️ No results found") + + except Exception as e: + result["error"] = str(e) + print(f" ❌ Error: {e}") + logger.exception(f"Error testing {site_key}") + + return result + +async def verify_all_sites(): + """Test all configured sites""" + query = "chaise" # Test query + + print("=" * 80) + print(f"VERIFICATION DU COMPARATEUR - Query: '{query}'") + print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 80) + + # 1. Test Browserless Connection + print("\n[1] Testing Browserless Connection...") + try: + await browserless_service.start() + print("✅ Browserless connected successfully") + except Exception as e: + print(f"❌ Browserless connection failed: {e}") + return + + # 2. Test all sites + print(f"\n[2] Testing {len(SITE_CONFIGS)} configured sites...") + + all_results = [] + for site_key in SITE_CONFIGS.keys(): + result = await test_site(site_key, query) + all_results.append(result) + # Small delay between sites to avoid overload + await asyncio.sleep(1) + + # 3. Summary + print("\n" + "=" * 80) + print("SUMMARY") + print("=" * 80) + + successful = [r for r in all_results if r["success"]] + failed = [r for r in all_results if not r["success"]] + + print(f"\n✅ Successful: {len(successful)}/{len(all_results)}") + for r in successful: + print(f" - {r['site']:20s} | {r['count']:2d} results | Category: {r['category']}") + + if failed: + print(f"\n❌ Failed: {len(failed)}/{len(all_results)}") + for r in failed: + error_msg = r['error'][:50] if r['error'] else "No results" + print(f" - {r['site']:20s} | Error: {error_msg}") + + # 4. Detailed results by category + print("\n" + "=" * 80) + print("BY CATEGORY") + print("=" * 80) + + categories = {} + for r in all_results: + cat = r["category"] + if cat not in categories: + categories[cat] = [] + categories[cat].append(r) + + for cat, sites in categories.items(): + successful_count = sum(1 for s in sites if s["success"]) + print(f"\n{cat}: {successful_count}/{len(sites)} working") + for site in sites: + status = "✅" if site["success"] else "❌" + count = f"({site['count']} results)" if site["success"] else "(failed)" + print(f" {status} {site['site']:20s} {count}") + + # 5. Cleanup + await browserless_service.stop() + + print("\n" + "=" * 80) + print(f"Verification completed at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 80) + + return all_results + +if __name__ == "__main__": + asyncio.run(verify_all_sites())