mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-22 14:48:13 +02:00
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
This commit is contained in:
7 files changed
+59
-31
No files matched your search
@@ -91,5 +91,6 @@ class SearchSite(Base):
|
|||||||
is_active: bool = Column(Boolean, default=True) # type: ignore
|
is_active: bool = Column(Boolean, default=True) # type: ignore
|
||||||
priority: int = Column(Integer, default=0) # type: ignore # Ordre d'affichage
|
priority: int = Column(Integer, default=0) # type: ignore # Ordre d'affichage
|
||||||
requires_js: bool = Column(Boolean, default=False) # type: ignore # Force Browserless si True
|
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
|
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
|
updated_at: datetime = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)) # type: ignore
|
||||||
@@ -62,6 +62,7 @@ class SearchSiteCreate(BaseModel):
|
|||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
priority: int = 0
|
priority: int = 0
|
||||||
requires_js: bool = False
|
requires_js: bool = False
|
||||||
|
price_selector: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class SearchSiteUpdate(BaseModel):
|
class SearchSiteUpdate(BaseModel):
|
||||||
@@ -72,6 +73,7 @@ class SearchSiteUpdate(BaseModel):
|
|||||||
is_active: bool | None = None
|
is_active: bool | None = None
|
||||||
priority: int | None = None
|
priority: int | None = None
|
||||||
requires_js: bool | None = None
|
requires_js: bool | None = None
|
||||||
|
price_selector: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class SearchSiteResponse(BaseModel):
|
class SearchSiteResponse(BaseModel):
|
||||||
@@ -83,6 +85,7 @@ class SearchSiteResponse(BaseModel):
|
|||||||
is_active: bool
|
is_active: bool
|
||||||
priority: int
|
priority: int
|
||||||
requires_js: bool
|
requires_js: bool
|
||||||
|
price_selector: str | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|||||||
+1
-1
@@ -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"]
|
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 = []
|
block_ip = []
|
||||||
EOF
|
EOF
|
||||||
exec /sbin/tini -- /usr/local/searxng/dockerfiles/docker-entrypoint.sh
|
exec /usr/local/searxng/dockerfiles/docker-entrypoint.sh
|
||||||
networks:
|
networks:
|
||||||
- nginx_default
|
- nginx_default
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export function ItemModal({ item, onClose, onSaved, open }) {
|
|||||||
url: '',
|
url: '',
|
||||||
name: '',
|
name: '',
|
||||||
target_price: '',
|
target_price: '',
|
||||||
selector: '',
|
|
||||||
tags: '',
|
tags: '',
|
||||||
description: '',
|
description: '',
|
||||||
notification_profile_id: ''
|
notification_profile_id: ''
|
||||||
@@ -29,7 +28,6 @@ export function ItemModal({ item, onClose, onSaved, open }) {
|
|||||||
url: item.url || '',
|
url: item.url || '',
|
||||||
name: item.name || '',
|
name: item.name || '',
|
||||||
target_price: item.target_price || '',
|
target_price: item.target_price || '',
|
||||||
selector: item.selector || '',
|
|
||||||
tags: item.tags || '',
|
tags: item.tags || '',
|
||||||
description: item.description || '',
|
description: item.description || '',
|
||||||
notification_profile_id: item.notification_profile_id ? item.notification_profile_id.toString() : ''
|
notification_profile_id: item.notification_profile_id ? item.notification_profile_id.toString() : ''
|
||||||
@@ -39,7 +37,6 @@ export function ItemModal({ item, onClose, onSaved, open }) {
|
|||||||
url: '',
|
url: '',
|
||||||
name: '',
|
name: '',
|
||||||
target_price: '',
|
target_price: '',
|
||||||
selector: '',
|
|
||||||
tags: '',
|
tags: '',
|
||||||
description: '',
|
description: '',
|
||||||
notification_profile_id: ''
|
notification_profile_id: ''
|
||||||
@@ -105,27 +102,16 @@ export function ItemModal({ item, onClose, onSaved, open }) {
|
|||||||
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="target_price">{t('itemModal.targetPrice')} (€)</Label>
|
||||||
<Label htmlFor="target_price">{t('itemModal.targetPrice')} (€)</Label>
|
<Input
|
||||||
<Input
|
id="target_price"
|
||||||
id="target_price"
|
type="number"
|
||||||
type="number"
|
step="0.01"
|
||||||
step="0.01"
|
placeholder="0.00"
|
||||||
placeholder="0.00"
|
value={formData.target_price}
|
||||||
value={formData.target_price}
|
onChange={e => setFormData({ ...formData, target_price: e.target.value })}
|
||||||
onChange={e => setFormData({ ...formData, target_price: e.target.value })}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="selector">{t('itemModal.selector')}</Label>
|
|
||||||
<Input
|
|
||||||
id="selector"
|
|
||||||
placeholder={t('itemModal.selectorPlaceholder')}
|
|
||||||
value={formData.selector}
|
|
||||||
onChange={e => setFormData({ ...formData, selector: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="profile">{t('itemModal.notificationProfile')}</Label>
|
<Label htmlFor="profile">{t('itemModal.notificationProfile')}</Label>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
"tagline": "Suivi de prix intelligent"
|
"tagline": "Suivi de prix intelligent"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Suivis produits",
|
||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"lightMode": "Mode clair",
|
"lightMode": "Mode clair",
|
||||||
"darkMode": "Mode sombre"
|
"darkMode": "Mode sombre"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Tableau de bord",
|
"title": "Suivis produits",
|
||||||
"description": "Gérez vos articles suivis et surveillez les changements de prix.",
|
"description": "Gérez vos articles suivis et surveillez les changements de prix.",
|
||||||
"addItem": "Ajouter un article",
|
"addItem": "Ajouter un article",
|
||||||
"refreshAll": "Actualiser tout",
|
"refreshAll": "Actualiser tout",
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
// Search Sites state
|
// Search Sites state
|
||||||
const [searchSites, setSearchSites] = useState([]);
|
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);
|
const [editingSiteId, setEditingSiteId] = useState(null);
|
||||||
|
|
||||||
// OpenRouter state
|
// OpenRouter state
|
||||||
@@ -251,7 +251,7 @@ export default function Settings() {
|
|||||||
await axios.post(`${API_URL}/search-sites`, newSite);
|
await axios.post(`${API_URL}/search-sites`, newSite);
|
||||||
toast.success('Site ajouté');
|
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);
|
setEditingSiteId(null);
|
||||||
fetchAll();
|
fetchAll();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -266,13 +266,14 @@ export default function Settings() {
|
|||||||
category: site.category || '',
|
category: site.category || '',
|
||||||
is_active: site.is_active,
|
is_active: site.is_active,
|
||||||
priority: site.priority,
|
priority: site.priority,
|
||||||
requires_js: site.requires_js
|
requires_js: site.requires_js,
|
||||||
|
price_selector: site.price_selector || ''
|
||||||
});
|
});
|
||||||
setEditingSiteId(site.id);
|
setEditingSiteId(site.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelSiteEdit = () => {
|
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);
|
setEditingSiteId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -634,6 +635,11 @@ export default function Settings() {
|
|||||||
<Input type="number" min="0" value={newSite.priority} onChange={(e) => setNewSite({ ...newSite, priority: parseInt(e.target.value) || 0 })} />
|
<Input type="number" min="0" value={newSite.priority} onChange={(e) => setNewSite({ ...newSite, priority: parseInt(e.target.value) || 0 })} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Sélecteur CSS (prix)</Label>
|
||||||
|
<Input value={newSite.price_selector} onChange={(e) => setNewSite({ ...newSite, price_selector: e.target.value })} placeholder="ex: .price, #product-price, [data-price]" />
|
||||||
|
<p className="text-xs text-muted-foreground">Sélecteur CSS pour extraire le prix sur ce site (optionnel)</p>
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={newSite.is_active} onCheckedChange={(checked) => setNewSite({ ...newSite, is_active: checked })} />
|
<Switch checked={newSite.is_active} onCheckedChange={(checked) => setNewSite({ ...newSite, is_active: checked })} />
|
||||||
@@ -695,6 +701,11 @@ export default function Settings() {
|
|||||||
JavaScript
|
JavaScript
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{site.price_selector && (
|
||||||
|
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-purple-500/10 text-purple-700 dark:text-purple-400" title={site.price_selector}>
|
||||||
|
CSS: {site.price_selector.length > 15 ? site.price_selector.substring(0, 15) + '...' : site.price_selector}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<Switch checked={site.is_active} onCheckedChange={() => toggleSiteActive(site)} />
|
<Switch checked={site.is_active} onCheckedChange={() => toggleSiteActive(site)} />
|
||||||
|
|||||||
@@ -73,6 +73,33 @@ CREATE TABLE IF NOT EXISTS settings (
|
|||||||
-- Index sur les paramètres
|
-- Index sur les paramètres
|
||||||
CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key);
|
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
|
-- Paramètres par défaut pour l'application
|
||||||
INSERT INTO settings (key, value) VALUES
|
INSERT INTO settings (key, value) VALUES
|
||||||
('ai_provider', 'ollama'),
|
('ai_provider', 'ollama'),
|
||||||
|
|||||||
Reference in new issue
Block a user