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 })}
/>
-
-
- {t('itemModal.targetPrice')} (€)
- setFormData({ ...formData, target_price: e.target.value })}
- />
-
-
- {t('itemModal.selector')}
- setFormData({ ...formData, selector: e.target.value })}
- />
-
+
+ {t('itemModal.targetPrice')} (€)
+ setFormData({ ...formData, target_price: e.target.value })}
+ />
{t('itemModal.notificationProfile')}
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 })} />
+
+
Sélecteur CSS (prix)
+
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'),