From 933cb2e66f9c2d3ec6bd51cbfd92f11d2c4c3d1f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 13:24:28 +0000 Subject: [PATCH] feat: Improve search config + rename Dashboard + fix SearXNG SearXNG: - Fix tini not found error by calling entrypoint.sh directly - Keep limiter.toml creation at container startup UI Changes: - Rename "Tableau de bord" to "Suivis produits" in navigation - Move CSS selector configuration from item modal to Settings page - Add price_selector field to search sites configuration - Display CSS selector badge on site cards Backend: - Add price_selector column to SearchSite model and schemas - Update init.sql with search_sites table and migration for existing DBs --- app/models.py | 1 + app/schemas.py | 3 ++ docker-compose.yml | 2 +- .../src/components/dashboard/ItemModal.jsx | 34 ++++++------------- frontend/src/i18n/locales/fr.json | 4 +-- frontend/src/pages/Settings.jsx | 19 ++++++++--- init.sql | 27 +++++++++++++++ 7 files changed, 59 insertions(+), 31 deletions(-) diff --git a/app/models.py b/app/models.py index 3c68ada..67223d2 100644 --- a/app/models.py +++ b/app/models.py @@ -91,5 +91,6 @@ class SearchSite(Base): is_active: bool = Column(Boolean, default=True) # type: ignore priority: int = Column(Integer, default=0) # type: ignore # Ordre d'affichage requires_js: bool = Column(Boolean, default=False) # type: ignore # Force Browserless si True + price_selector: str | None = Column(String, nullable=True) # type: ignore # Sélecteur CSS pour le prix created_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC)) # type: ignore updated_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) # type: ignore diff --git a/app/schemas.py b/app/schemas.py index 9f262a6..5679971 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -62,6 +62,7 @@ class SearchSiteCreate(BaseModel): is_active: bool = True priority: int = 0 requires_js: bool = False + price_selector: str | None = None class SearchSiteUpdate(BaseModel): @@ -72,6 +73,7 @@ class SearchSiteUpdate(BaseModel): is_active: bool | None = None priority: int | None = None requires_js: bool | None = None + price_selector: str | None = None class SearchSiteResponse(BaseModel): @@ -83,6 +85,7 @@ class SearchSiteResponse(BaseModel): is_active: bool priority: int requires_js: bool + price_selector: str | None = None created_at: datetime | None = None updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) diff --git a/docker-compose.yml b/docker-compose.yml index 1687dda..b0cbd60 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,7 +74,7 @@ services: pass_ip = ["0.0.0.0/0", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "10.0.0.0/8", "::/0"] block_ip = [] EOF - exec /sbin/tini -- /usr/local/searxng/dockerfiles/docker-entrypoint.sh + exec /usr/local/searxng/dockerfiles/docker-entrypoint.sh networks: - nginx_default diff --git a/frontend/src/components/dashboard/ItemModal.jsx b/frontend/src/components/dashboard/ItemModal.jsx index 9e3a37e..02a7b90 100644 --- a/frontend/src/components/dashboard/ItemModal.jsx +++ b/frontend/src/components/dashboard/ItemModal.jsx @@ -16,7 +16,6 @@ export function ItemModal({ item, onClose, onSaved, open }) { url: '', name: '', target_price: '', - selector: '', tags: '', description: '', notification_profile_id: '' @@ -29,7 +28,6 @@ export function ItemModal({ item, onClose, onSaved, open }) { url: item.url || '', name: item.name || '', target_price: item.target_price || '', - selector: item.selector || '', tags: item.tags || '', description: item.description || '', notification_profile_id: item.notification_profile_id ? item.notification_profile_id.toString() : '' @@ -39,7 +37,6 @@ export function ItemModal({ item, onClose, onSaved, open }) { url: '', name: '', target_price: '', - selector: '', tags: '', description: '', notification_profile_id: '' @@ -105,27 +102,16 @@ export function ItemModal({ item, onClose, onSaved, open }) { onChange={e => setFormData({ ...formData, name: e.target.value })} /> -
-
- - setFormData({ ...formData, target_price: e.target.value })} - /> -
-
- - setFormData({ ...formData, selector: e.target.value })} - /> -
+
+ + setFormData({ ...formData, target_price: e.target.value })} + />
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 3e07a62..821a32b 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -4,14 +4,14 @@ "tagline": "Suivi de prix intelligent" }, "nav": { - "dashboard": "Tableau de bord", + "dashboard": "Suivis produits", "search": "Recherche", "settings": "Paramètres", "lightMode": "Mode clair", "darkMode": "Mode sombre" }, "dashboard": { - "title": "Tableau de bord", + "title": "Suivis produits", "description": "Gérez vos articles suivis et surveillez les changements de prix.", "addItem": "Ajouter un article", "refreshAll": "Actualiser tout", diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 34d36c3..d982d06 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -56,7 +56,7 @@ export default function Settings() { // Search Sites state const [searchSites, setSearchSites] = useState([]); - const [newSite, setNewSite] = useState({ name: '', domain: '', category: '', is_active: true, priority: 0, requires_js: false }); + const [newSite, setNewSite] = useState({ name: '', domain: '', category: '', is_active: true, priority: 0, requires_js: false, price_selector: '' }); const [editingSiteId, setEditingSiteId] = useState(null); // OpenRouter state @@ -251,7 +251,7 @@ export default function Settings() { await axios.post(`${API_URL}/search-sites`, newSite); toast.success('Site ajouté'); } - setNewSite({ name: '', domain: '', category: '', is_active: true, priority: 0, requires_js: false }); + setNewSite({ name: '', domain: '', category: '', is_active: true, priority: 0, requires_js: false, price_selector: '' }); setEditingSiteId(null); fetchAll(); } catch (error) { @@ -266,13 +266,14 @@ export default function Settings() { category: site.category || '', is_active: site.is_active, priority: site.priority, - requires_js: site.requires_js + requires_js: site.requires_js, + price_selector: site.price_selector || '' }); setEditingSiteId(site.id); }; const cancelSiteEdit = () => { - setNewSite({ name: '', domain: '', category: '', is_active: true, priority: 0, requires_js: false }); + setNewSite({ name: '', domain: '', category: '', is_active: true, priority: 0, requires_js: false, price_selector: '' }); setEditingSiteId(null); }; @@ -634,6 +635,11 @@ export default function Settings() { setNewSite({ ...newSite, priority: parseInt(e.target.value) || 0 })} />
+
+ + setNewSite({ ...newSite, price_selector: e.target.value })} placeholder="ex: .price, #product-price, [data-price]" /> +

Sélecteur CSS pour extraire le prix sur ce site (optionnel)

+
setNewSite({ ...newSite, is_active: checked })} /> @@ -695,6 +701,11 @@ export default function Settings() { JavaScript )} + {site.price_selector && ( + + CSS: {site.price_selector.length > 15 ? site.price_selector.substring(0, 15) + '...' : site.price_selector} + + )}
toggleSiteActive(site)} /> diff --git a/init.sql b/init.sql index b35dcad..e64ebbf 100644 --- a/init.sql +++ b/init.sql @@ -73,6 +73,33 @@ CREATE TABLE IF NOT EXISTS settings ( -- Index sur les paramètres CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key); +-- Table des sites de recherche +CREATE TABLE IF NOT EXISTS search_sites ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + domain VARCHAR(512) UNIQUE NOT NULL, + logo_url VARCHAR(1024), + category VARCHAR(255), + is_active BOOLEAN DEFAULT TRUE, + priority INTEGER DEFAULT 0, + requires_js BOOLEAN DEFAULT FALSE, + price_selector VARCHAR(512), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Index sur les sites de recherche +CREATE INDEX IF NOT EXISTS idx_search_sites_domain ON search_sites(domain); +CREATE INDEX IF NOT EXISTS idx_search_sites_is_active ON search_sites(is_active); + +-- Ajouter la colonne price_selector si elle n'existe pas (pour bases existantes) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'search_sites' AND column_name = 'price_selector') THEN + ALTER TABLE search_sites ADD COLUMN price_selector VARCHAR(512); + END IF; +END $$; + -- Paramètres par défaut pour l'application INSERT INTO settings (key, value) VALUES ('ai_provider', 'ollama'),