mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-22 22:58:11 +02:00
Merge pull request #109 from R0m1k3/antigravity
feat: add script to verify search service functionality across all co…
This commit is contained in:
3 files changed
+337
-3
No files matched your search
@@ -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
|
||||||
@@ -2,12 +2,13 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -17,8 +18,67 @@ export default function MultiSearch() {
|
|||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [progress, setProgress] = useState(null);
|
const [progress, setProgress] = useState(null);
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
|
const [availableSites, setAvailableSites] = useState([]);
|
||||||
|
const [selectedSiteIds, setSelectedSiteIds] = useState([]);
|
||||||
|
const [showSiteSelector, setShowSiteSelector] = useState(false);
|
||||||
const eventSourceRef = useRef(null);
|
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) => {
|
const handleSearch = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
@@ -26,6 +86,11 @@ export default function MultiSearch() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedSiteIds.length === 0) {
|
||||||
|
toast.error('Veuillez sélectionner au moins un site');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fermer l'EventSource précédent si existant
|
// Fermer l'EventSource précédent si existant
|
||||||
if (eventSourceRef.current) {
|
if (eventSourceRef.current) {
|
||||||
eventSourceRef.current.close();
|
eventSourceRef.current.close();
|
||||||
@@ -35,10 +100,11 @@ export default function MultiSearch() {
|
|||||||
setResults([]);
|
setResults([]);
|
||||||
setProgress({ status: 'searching', total: 0, completed: 0 });
|
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({
|
const params = new URLSearchParams({
|
||||||
q: query.trim(),
|
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
|
// Créer l'EventSource pour SSE
|
||||||
@@ -147,6 +213,65 @@ export default function MultiSearch() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{/* Site Selector */}
|
||||||
|
<div className="mt-4 border-t pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowSiteSelector(!showSiteSelector)}
|
||||||
|
className="flex items-center justify-between w-full text-sm font-medium mb-2"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Sites à rechercher ({selectedSiteIds.length} sur {availableSites.length} sélectionné{selectedSiteIds.length > 1 ? 's' : ''})
|
||||||
|
</span>
|
||||||
|
{showSiteSelector ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showSiteSelector && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={selectAll}
|
||||||
|
>
|
||||||
|
Tout sélectionner
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={deselectAll}
|
||||||
|
>
|
||||||
|
Tout désélectionner
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sites grouped by category */}
|
||||||
|
{Object.entries(sitesByCategory).map(([category, sites]) => (
|
||||||
|
<div key={category} className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">{category}</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||||
|
{sites.map(site => (
|
||||||
|
<label
|
||||||
|
key={site.id}
|
||||||
|
className="flex items-center space-x-2 cursor-pointer hover:bg-muted/50 p-2 rounded transition-colors"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedSiteIds.includes(site.id)}
|
||||||
|
onCheckedChange={() => toggleSite(site.id)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{site.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in new issue
Block a user