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:
Claude committed 2025-11-22 13:24:28 +00:00
1 parent e8ccd69bff
commit 933cb2e66f
7 files changed
+59 -31

No files matched your search

+10 -24
View File
@@ -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 })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="target_price">{t('itemModal.targetPrice')} (€)</Label>
<Input
id="target_price"
type="number"
step="0.01"
placeholder="0.00"
value={formData.target_price}
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 className="space-y-2">
<Label htmlFor="target_price">{t('itemModal.targetPrice')} (€)</Label>
<Input
id="target_price"
type="number"
step="0.01"
placeholder="0.00"
value={formData.target_price}
onChange={e => setFormData({ ...formData, target_price: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="profile">{t('itemModal.notificationProfile')}</Label>
+2 -2
View File
@@ -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",
+15 -4
View File
@@ -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() {
<Input type="number" min="0" value={newSite.priority} onChange={(e) => setNewSite({ ...newSite, priority: parseInt(e.target.value) || 0 })} />
</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 items-center space-x-2">
<Switch checked={newSite.is_active} onCheckedChange={(checked) => setNewSite({ ...newSite, is_active: checked })} />
@@ -695,6 +701,11 @@ export default function Settings() {
JavaScript
</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 className="pt-2">
<Switch checked={site.is_active} onCheckedChange={() => toggleSiteActive(site)} />