diff --git a/CHANGELOG.md b/CHANGELOG.md index 623e305..f6693cd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,11 +53,14 @@ Le format est basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) - Web UI: popup ajout produit central + favicon - API: logs Uvicorn exposes pour l UI - Parsing prix: gestion des separateurs de milliers (espace, NBSP, point) -- API/DB: description + msrp + images/specs exposes, reduction calculee +- API/DB: exposition des champs Amazon enrichis (note, badge, stock texte, modele) +- Web UI: carte produit analytique avec resume, historique plein format et actions compactes +- Web UI: slider colonnes responsive + modal ajout produit scrollable avec footer sticky ### Corrigé - Migration Alembic: down_revision aligne sur 20260114_02 - Amazon: extraction images via data-a-dynamic-image + filtrage logos +- API: suppression du calcul automatique des reductions (valeurs explicites uniquement) --- diff --git a/README.md b/README.md index c2c7800..e47d482 100755 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ Guide de migration JSON -> DB: `MIGRATION_GUIDE.md` L'API est protegee par un token simple. +Note: l endpoint `/products` expose des champs Amazon explicites (asin, note, badge Choix d Amazon, stock_text/in_stock, model_number/model_name, main_image/gallery_images). Les reductions ne sont plus calculees cote API. + ```bash export PW_API_TOKEN=change_me docker compose up -d api diff --git a/TODO.md b/TODO.md index e9770d6..971a785 100755 --- a/TODO.md +++ b/TODO.md @@ -170,6 +170,7 @@ Liste des tâches priorisées pour le développement de PriceWatch. - [x] Tests performance (100+ produits) - [x] CRUD produits - [x] Historique prix +- [ ] Ajouter migration DB pour les nouveaux champs Amazon (note, badge, stock texte, modele) ### Documentation - [x] Migration guide (JSON -> DB) diff --git a/analytics-ui/app.py b/analytics-ui/app.py index e3883ef..e54180b 100644 --- a/analytics-ui/app.py +++ b/analytics-ui/app.py @@ -76,6 +76,81 @@ def _serialize_decimal(value): return value +def fetch_product_history(product_id: int) -> Tuple[List[Dict[str, Any]], Optional[str]]: + """Récupère l'historique complet des scraps pour un produit.""" + rows: List[Dict[str, Any]] = [] + try: + with get_db_connection() as conn: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + """ + SELECT + ph.id, + ph.price, + ph.shipping_cost, + ph.stock_status, + ph.fetch_method, + ph.fetch_status, + ph.fetched_at + FROM price_history ph + WHERE ph.product_id = %s + ORDER BY ph.fetched_at DESC + """, + (product_id,), + ) + fetched = cur.fetchall() + for item in fetched: + serialized = {key: _serialize_decimal(value) for key, value in item.items()} + if serialized.get("fetched_at"): + serialized["fetched_at"] = serialized["fetched_at"].strftime( + "%Y-%m-%d %H:%M:%S" + ) + rows.append(serialized) + return rows, None + except Exception as exc: + return rows, str(exc) + + +def fetch_all_price_history(limit: int = 500) -> Tuple[List[Dict[str, Any]], Optional[str]]: + """Récupère toutes les entrées de price_history avec infos produit.""" + rows: List[Dict[str, Any]] = [] + try: + with get_db_connection() as conn: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + """ + SELECT + ph.id, + ph.product_id, + p.source, + p.reference, + p.title, + ph.price, + ph.shipping_cost, + ph.stock_status, + ph.fetch_method, + ph.fetch_status, + ph.fetched_at + FROM price_history ph + LEFT JOIN products p ON p.id = ph.product_id + ORDER BY ph.fetched_at DESC + LIMIT %s + """, + (limit,), + ) + fetched = cur.fetchall() + for item in fetched: + serialized = {key: _serialize_decimal(value) for key, value in item.items()} + if serialized.get("fetched_at"): + serialized["fetched_at"] = serialized["fetched_at"].strftime( + "%Y-%m-%d %H:%M:%S" + ) + rows.append(serialized) + return rows, None + except Exception as exc: + return rows, str(exc) + + def fetch_products_list(limit: int = 200) -> Tuple[List[Dict[str, Any]], Optional[str]]: rows: List[Dict[str, Any]] = [] try: @@ -260,6 +335,68 @@ TEMPLATE = """ +
+

Historique complet des scraps

+
+
+ + +
+
+ + + + + + + + + + + + + + +
DatePrixFrais portStockMéthodeStatut
Sélectionnez un produit puis cliquez sur "Charger l'historique"
+
+
+
+
+

Parcourir la table price_history

+
+
+ + + + 0 / 0 + +
+
+
ID
+
-
+
Product ID
+
-
+
Store
+
-
+
Référence
+
-
+
Titre produit
+
-
+
Prix
+
-
+
Frais de port
+
-
+
Stock
+
-
+
Méthode
+
-
+
Statut
+
-
+
Date scraping
+
-
+
+
+
@@ -377,5 +685,21 @@ def products_json(): return jsonify(products) +@app.route("/product//history.json") +def product_history_json(product_id: int): + history, error = fetch_product_history(product_id) + if error: + return jsonify({"error": error}), 500 + return jsonify(history) + + +@app.route("/price_history.json") +def all_price_history_json(): + history, error = fetch_all_price_history() + if error: + return jsonify({"error": error}), 500 + return jsonify(history) + + if __name__ == "__main__": app.run(host="0.0.0.0", port=80) diff --git a/docker-compose.yml b/docker-compose.yml index 23a7229..9860185 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,19 @@ services: depends_on: - postgres - redis + + worker: + build: . + command: python -m pricewatch.app.cli.main worker + env_file: + - .env + environment: + PW_DB_HOST: postgres + PW_REDIS_HOST: redis + TZ: Europe/Paris + depends_on: + - postgres + - redis frontend: build: ./webui @@ -75,6 +88,23 @@ services: depends_on: - postgres + pgadmin: + image: dpage/pgadmin4:latest + ports: + - "8072:80" + environment: + TZ: Europe/Paris + PGADMIN_DEFAULT_EMAIL: admin@pricewatch.dev + PGADMIN_DEFAULT_PASSWORD: pricewatch + PGADMIN_CONFIG_SERVER_MODE: "False" + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" + volumes: + - pricewatch_pgadmin:/var/lib/pgadmin + - ./pgadmin-servers.json:/pgadmin4/servers.json:ro + depends_on: + - postgres + volumes: pricewatch_pgdata: pricewatch_redisdata: + pricewatch_pgadmin: diff --git a/pgadmin-servers.json b/pgadmin-servers.json new file mode 100644 index 0000000..72931a0 --- /dev/null +++ b/pgadmin-servers.json @@ -0,0 +1,14 @@ +{ + "Servers": { + "1": { + "Name": "PriceWatch PostgreSQL", + "Group": "Servers", + "Host": "postgres", + "Port": 5432, + "MaintenanceDB": "pricewatch", + "Username": "pricewatch", + "PassFile": "/pgadmin4/pgpass", + "SSLMode": "prefer" + } + } +} diff --git a/pricewatch.egg-info/PKG-INFO b/pricewatch.egg-info/PKG-INFO index ac434d9..8affc0d 100644 --- a/pricewatch.egg-info/PKG-INFO +++ b/pricewatch.egg-info/PKG-INFO @@ -196,6 +196,8 @@ Guide de migration JSON -> DB: `MIGRATION_GUIDE.md` L'API est protegee par un token simple. +Note: l endpoint `/products` expose des champs Amazon explicites (asin, note, badge Choix d Amazon, stock_text/in_stock, model_number/model_name, main_image/gallery_images). Les reductions ne sont plus calculees cote API. + ```bash export PW_API_TOKEN=change_me docker compose up -d api @@ -204,8 +206,54 @@ docker compose up -d api Exemples: ```bash -curl -H "Authorization: Bearer $PW_API_TOKEN" http://localhost:8000/products -curl http://localhost:8000/health +curl -H "Authorization: Bearer $PW_API_TOKEN" http://localhost:8001/products +curl http://localhost:8001/health +``` + +Filtres (exemples rapides): + +```bash +curl -H "Authorization: Bearer $PW_API_TOKEN" \\ + "http://localhost:8001/products?price_min=100&stock_status=in_stock" +curl -H "Authorization: Bearer $PW_API_TOKEN" \\ + "http://localhost:8001/products/1/prices?fetch_status=success&fetched_after=2026-01-14T00:00:00" +curl -H "Authorization: Bearer $PW_API_TOKEN" \\ + "http://localhost:8001/logs?fetch_status=failed&fetched_before=2026-01-15T00:00:00" +``` + +Exports (CSV/JSON): + +```bash +curl -H "Authorization: Bearer $PW_API_TOKEN" \\ + "http://localhost:8001/products/export?format=csv" +curl -H "Authorization: Bearer $PW_API_TOKEN" \\ + "http://localhost:8001/logs/export?format=json" +``` + +CRUD (examples rapides): + +```bash +curl -H "Authorization: Bearer $PW_API_TOKEN" -X POST http://localhost:8001/products \\ + -H "Content-Type: application/json" \\ + -d '{"source":"amazon","reference":"REF1","url":"https://example.com"}' +``` + +Webhooks (exemples rapides): + +```bash +curl -H "Authorization: Bearer $PW_API_TOKEN" -X POST http://localhost:8001/webhooks \\ + -H "Content-Type: application/json" \\ + -d '{"event":"price_changed","url":"https://example.com/webhook","enabled":true}' +curl -H "Authorization: Bearer $PW_API_TOKEN" -X POST http://localhost:8001/webhooks/1/test +``` + +## Web UI (Phase 4) + +Interface Vue 3 dense avec themes Gruvbox/Monokai, header fixe, sidebar filtres, et split compare. + +```bash +docker compose up -d frontend +# Acces: http://localhost:3000 ``` ## Configuration (scrap_url.yaml) diff --git a/pricewatch.egg-info/SOURCES.txt b/pricewatch.egg-info/SOURCES.txt index 48f824a..fab1309 100755 --- a/pricewatch.egg-info/SOURCES.txt +++ b/pricewatch.egg-info/SOURCES.txt @@ -22,6 +22,7 @@ pricewatch/app/scraping/pipeline.py pricewatch/app/scraping/pw_fetch.py pricewatch/app/stores/__init__.py pricewatch/app/stores/base.py +pricewatch/app/stores/price_parser.py pricewatch/app/stores/amazon/__init__.py pricewatch/app/stores/amazon/store.py pricewatch/app/stores/cdiscount/__init__.py diff --git a/pricewatch/app/api/main.py b/pricewatch/app/api/main.py index e3ec42e..06d2b19 100644 --- a/pricewatch/app/api/main.py +++ b/pricewatch/app/api/main.py @@ -22,6 +22,10 @@ from sqlalchemy.orm import Session from pricewatch.app.api.schemas import ( BackendLogEntry, + ClassificationOptionsOut, + ClassificationRuleCreate, + ClassificationRuleOut, + ClassificationRuleUpdate, EnqueueRequest, EnqueueResponse, HealthStatus, @@ -52,7 +56,8 @@ from pricewatch.app.core.config import get_config from pricewatch.app.core.logging import get_logger from pricewatch.app.core.schema import ProductSnapshot from pricewatch.app.db.connection import check_db_connection, get_session -from pricewatch.app.db.models import PriceHistory, Product, ScrapingLog, Webhook +from pricewatch.app.db.models import ClassificationRule, PriceHistory, Product, ScrapingLog, Webhook +from pricewatch.app.db.repository import ProductRepository from pricewatch.app.scraping.pipeline import ScrapingPipeline from pricewatch.app.tasks.scrape import scrape_product from pricewatch.app.tasks.scheduler import RedisUnavailableError, check_redis_connection, ScrapingScheduler @@ -188,6 +193,7 @@ def create_product( url=payload.url, title=payload.title, category=payload.category, + type=payload.type, description=payload.description, currency=payload.currency, msrp=payload.msrp, @@ -241,6 +247,129 @@ def update_product( return _product_to_out(session, product) +@app.get( + "/classification/rules", + response_model=list[ClassificationRuleOut], + dependencies=[Depends(require_token)], +) +def list_classification_rules( + session: Session = Depends(get_db_session), +) -> list[ClassificationRuleOut]: + """Liste les regles de classification.""" + rules = ( + session.query(ClassificationRule) + .order_by(ClassificationRule.sort_order, ClassificationRule.id) + .all() + ) + return [ + ClassificationRuleOut( + id=rule.id, + category=rule.category, + type=rule.type, + keywords=rule.keywords or [], + sort_order=rule.sort_order, + is_active=rule.is_active, + ) + for rule in rules + ] + + +@app.post( + "/classification/rules", + response_model=ClassificationRuleOut, + dependencies=[Depends(require_token)], +) +def create_classification_rule( + payload: ClassificationRuleCreate, + session: Session = Depends(get_db_session), +) -> ClassificationRuleOut: + """Cree une regle de classification.""" + rule = ClassificationRule( + category=payload.category, + type=payload.type, + keywords=payload.keywords, + sort_order=payload.sort_order or 0, + is_active=True if payload.is_active is None else payload.is_active, + ) + session.add(rule) + session.commit() + session.refresh(rule) + return ClassificationRuleOut( + id=rule.id, + category=rule.category, + type=rule.type, + keywords=rule.keywords or [], + sort_order=rule.sort_order, + is_active=rule.is_active, + ) + + +@app.patch( + "/classification/rules/{rule_id}", + response_model=ClassificationRuleOut, + dependencies=[Depends(require_token)], +) +def update_classification_rule( + rule_id: int, + payload: ClassificationRuleUpdate, + session: Session = Depends(get_db_session), +) -> ClassificationRuleOut: + """Met a jour une regle de classification.""" + rule = session.query(ClassificationRule).filter(ClassificationRule.id == rule_id).one_or_none() + if not rule: + raise HTTPException(status_code=404, detail="Regle non trouvee") + updates = payload.model_dump(exclude_unset=True) + for key, value in updates.items(): + setattr(rule, key, value) + session.commit() + session.refresh(rule) + return ClassificationRuleOut( + id=rule.id, + category=rule.category, + type=rule.type, + keywords=rule.keywords or [], + sort_order=rule.sort_order, + is_active=rule.is_active, + ) + + +@app.delete( + "/classification/rules/{rule_id}", + dependencies=[Depends(require_token)], +) +def delete_classification_rule( + rule_id: int, + session: Session = Depends(get_db_session), +) -> dict[str, str]: + """Supprime une regle de classification.""" + rule = session.query(ClassificationRule).filter(ClassificationRule.id == rule_id).one_or_none() + if not rule: + raise HTTPException(status_code=404, detail="Regle non trouvee") + session.delete(rule) + session.commit() + return {"status": "deleted"} + + +@app.get( + "/classification/options", + response_model=ClassificationOptionsOut, + dependencies=[Depends(require_token)], +) +def get_classification_options( + session: Session = Depends(get_db_session), +) -> ClassificationOptionsOut: + """Expose la liste des categories et types issus des regles actives.""" + rules = ( + session.query(ClassificationRule) + .filter(ClassificationRule.is_active == True) + .order_by(ClassificationRule.sort_order, ClassificationRule.id) + .all() + ) + categories = sorted({rule.category for rule in rules if rule.category}) + types = sorted({rule.type for rule in rules if rule.type}) + return ClassificationOptionsOut(categories=categories, types=types) + + @app.delete("/products/{product_id}", dependencies=[Depends(require_token)]) def delete_product( product_id: int, @@ -703,6 +832,13 @@ def preview_scrape(payload: ScrapePreviewRequest) -> ScrapePreviewResponse: if snapshot is None: _add_backend_log("ERROR", f"Preview scraping KO: {payload.url}") return ScrapePreviewResponse(success=False, snapshot=None, error=result.get("error")) + config = get_config() + if config.enable_db: + try: + with get_session(config) as session: + ProductRepository(session).apply_classification(snapshot) + except Exception as exc: + snapshot.add_note(f"Classification ignoree: {exc}") return ScrapePreviewResponse( success=bool(result.get("success")), snapshot=snapshot.model_dump(mode="json"), @@ -719,7 +855,9 @@ def commit_scrape(payload: ScrapeCommitRequest) -> ScrapeCommitResponse: _add_backend_log("ERROR", "Commit scraping KO: snapshot invalide") raise HTTPException(status_code=400, detail="Snapshot invalide") from exc - product_id = ScrapingPipeline(config=get_config()).process_snapshot(snapshot, save_to_db=True) + product_id = ScrapingPipeline(config=get_config()).process_snapshot( + snapshot, save_to_db=True, apply_classification=False + ) _add_backend_log("INFO", f"Commit scraping OK: product_id={product_id}") return ScrapeCommitResponse(success=True, product_id=product_id) @@ -808,12 +946,9 @@ def _product_to_out(session: Session, product: Product) -> ProductOut: ) images = [image.image_url for image in product.images] specs = {spec.spec_key: spec.spec_value for spec in product.specs} - discount_amount = None - discount_percent = None - if latest and latest.price is not None and product.msrp: - discount_amount = float(product.msrp) - float(latest.price) - if product.msrp > 0: - discount_percent = (discount_amount / float(product.msrp)) * 100 + main_image = images[0] if images else None + gallery_images = images[1:] if len(images) > 1 else [] + asin = product.reference if product.source == "amazon" else None history_rows = ( session.query(PriceHistory) .filter(PriceHistory.product_id == product.id, PriceHistory.price != None) @@ -830,12 +965,23 @@ def _product_to_out(session: Session, product: Product) -> ProductOut: id=product.id, source=product.source, reference=product.reference, + asin=asin, url=product.url, title=product.title, category=product.category, + type=product.type, description=product.description, currency=product.currency, msrp=float(product.msrp) if product.msrp is not None else None, + rating_value=float(product.rating_value) if product.rating_value is not None else None, + rating_count=product.rating_count, + amazon_choice=product.amazon_choice, + amazon_choice_label=product.amazon_choice_label, + discount_text=product.discount_text, + stock_text=product.stock_text, + in_stock=product.in_stock, + model_number=product.model_number, + model_name=product.model_name, first_seen_at=product.first_seen_at, last_updated_at=product.last_updated_at, latest_price=float(latest.price) if latest and latest.price is not None else None, @@ -845,9 +991,11 @@ def _product_to_out(session: Session, product: Product) -> ProductOut: latest_stock_status=latest.stock_status if latest else None, latest_fetched_at=latest.fetched_at if latest else None, images=images, + main_image=main_image, + gallery_images=gallery_images, specs=specs, - discount_amount=discount_amount, - discount_percent=discount_percent, + discount_amount=None, + discount_percent=None, history=history_points, ) diff --git a/pricewatch/app/api/schemas.py b/pricewatch/app/api/schemas.py index dad001f..6ec6364 100644 --- a/pricewatch/app/api/schemas.py +++ b/pricewatch/app/api/schemas.py @@ -22,12 +22,23 @@ class ProductOut(BaseModel): id: int source: str reference: str + asin: Optional[str] = None url: str title: Optional[str] = None category: Optional[str] = None + type: Optional[str] = None description: Optional[str] = None currency: Optional[str] = None msrp: Optional[float] = None + rating_value: Optional[float] = None + rating_count: Optional[int] = None + amazon_choice: Optional[bool] = None + amazon_choice_label: Optional[str] = None + discount_text: Optional[str] = None + stock_text: Optional[str] = None + in_stock: Optional[bool] = None + model_number: Optional[str] = None + model_name: Optional[str] = None first_seen_at: datetime last_updated_at: datetime latest_price: Optional[float] = None @@ -35,6 +46,8 @@ class ProductOut(BaseModel): latest_stock_status: Optional[str] = None latest_fetched_at: Optional[datetime] = None images: list[str] = [] + main_image: Optional[str] = None + gallery_images: list[str] = [] specs: dict[str, str] = {} discount_amount: Optional[float] = None discount_percent: Optional[float] = None @@ -47,6 +60,7 @@ class ProductCreate(BaseModel): url: str title: Optional[str] = None category: Optional[str] = None + type: Optional[str] = None description: Optional[str] = None currency: Optional[str] = None msrp: Optional[float] = None @@ -56,6 +70,7 @@ class ProductUpdate(BaseModel): url: Optional[str] = None title: Optional[str] = None category: Optional[str] = None + type: Optional[str] = None description: Optional[str] = None currency: Optional[str] = None msrp: Optional[float] = None @@ -208,6 +223,36 @@ class VersionResponse(BaseModel): api_version: str +class ClassificationRuleOut(BaseModel): + id: int + category: Optional[str] = None + type: Optional[str] = None + keywords: list[str] = Field(default_factory=list) + sort_order: int = 0 + is_active: bool = True + + +class ClassificationRuleCreate(BaseModel): + category: Optional[str] = None + type: Optional[str] = None + keywords: list[str] = Field(default_factory=list) + sort_order: Optional[int] = 0 + is_active: Optional[bool] = True + + +class ClassificationRuleUpdate(BaseModel): + category: Optional[str] = None + type: Optional[str] = None + keywords: Optional[list[str]] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + + +class ClassificationOptionsOut(BaseModel): + categories: list[str] = Field(default_factory=list) + types: list[str] = Field(default_factory=list) + + class BackendLogEntry(BaseModel): time: datetime level: str diff --git a/pricewatch/app/core/__pycache__/schema.cpython-313.pyc b/pricewatch/app/core/__pycache__/schema.cpython-313.pyc index 201237b..844bc6f 100644 Binary files a/pricewatch/app/core/__pycache__/schema.cpython-313.pyc and b/pricewatch/app/core/__pycache__/schema.cpython-313.pyc differ diff --git a/pricewatch/app/core/schema.py b/pricewatch/app/core/schema.py index 9c45228..26f1962 100755 --- a/pricewatch/app/core/schema.py +++ b/pricewatch/app/core/schema.py @@ -93,13 +93,52 @@ class ProductSnapshot(BaseModel): reference: Optional[str] = Field( default=None, description="Référence produit (ASIN, SKU, etc.)" ) + asin: Optional[str] = Field( + default=None, description="ASIN Amazon si disponible" + ) category: Optional[str] = Field(default=None, description="Catégorie du produit") + type: Optional[str] = Field(default=None, description="Type du produit") description: Optional[str] = Field(default=None, description="Description produit") + # Données Amazon explicites (si disponibles) + rating_value: Optional[float] = Field( + default=None, description="Note moyenne affichée" + ) + rating_count: Optional[int] = Field( + default=None, description="Nombre d'évaluations" + ) + amazon_choice: Optional[bool] = Field( + default=None, description="Badge Choix d'Amazon présent" + ) + amazon_choice_label: Optional[str] = Field( + default=None, description="Libellé du badge Choix d'Amazon" + ) + discount_text: Optional[str] = Field( + default=None, description="Texte de réduction affiché" + ) + stock_text: Optional[str] = Field( + default=None, description="Texte brut de stock" + ) + in_stock: Optional[bool] = Field( + default=None, description="Disponibilité dérivée" + ) + model_number: Optional[str] = Field( + default=None, description="Numéro du modèle de l'article" + ) + model_name: Optional[str] = Field( + default=None, description="Nom du modèle explicite" + ) + # Médias images: list[str] = Field( default_factory=list, description="Liste des URLs d'images du produit" ) + main_image: Optional[str] = Field( + default=None, description="Image principale du produit" + ) + gallery_images: list[str] = Field( + default_factory=list, description="Images de galerie dédoublonnées" + ) # Caractéristiques techniques specs: dict[str, str] = Field( @@ -134,6 +173,12 @@ class ProductSnapshot(BaseModel): """Filtre les URLs d'images vides.""" return [url.strip() for url in v if url and url.strip()] + @field_validator("gallery_images") + @classmethod + def validate_gallery_images(cls, v: list[str]) -> list[str]: + """Filtre les URLs de galerie vides.""" + return [url.strip() for url in v if url and url.strip()] + model_config = ConfigDict( use_enum_values=True, json_schema_extra={ diff --git a/pricewatch/app/db/__pycache__/models.cpython-313.pyc b/pricewatch/app/db/__pycache__/models.cpython-313.pyc index eb11a16..96c47fa 100644 Binary files a/pricewatch/app/db/__pycache__/models.cpython-313.pyc and b/pricewatch/app/db/__pycache__/models.cpython-313.pyc differ diff --git a/pricewatch/app/db/__pycache__/repository.cpython-313.pyc b/pricewatch/app/db/__pycache__/repository.cpython-313.pyc index 7b45fa3..bab80d0 100644 Binary files a/pricewatch/app/db/__pycache__/repository.cpython-313.pyc and b/pricewatch/app/db/__pycache__/repository.cpython-313.pyc differ diff --git a/pricewatch/app/db/migrations/versions/0014e51c4927_ajout_champs_amazon_produit.py b/pricewatch/app/db/migrations/versions/0014e51c4927_ajout_champs_amazon_produit.py new file mode 100644 index 0000000..6d2d9bf --- /dev/null +++ b/pricewatch/app/db/migrations/versions/0014e51c4927_ajout_champs_amazon_produit.py @@ -0,0 +1,350 @@ +"""Ajout champs Amazon produit + +Revision ID: 0014e51c4927 +Revises: 20260115_02_product_details +Create Date: 2026-01-17 19:23:01.866891 +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# Revision identifiers, used by Alembic. +revision = '0014e51c4927' +down_revision = '20260115_02_product_details' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('price_history', 'price', + existing_type=sa.NUMERIC(precision=10, scale=2), + comment='Product price', + existing_nullable=True) + op.alter_column('price_history', 'shipping_cost', + existing_type=sa.NUMERIC(precision=10, scale=2), + comment='Shipping cost', + existing_nullable=True) + op.alter_column('price_history', 'stock_status', + existing_type=sa.VARCHAR(length=20), + comment='Stock status (in_stock, out_of_stock, unknown)', + existing_nullable=True) + op.alter_column('price_history', 'fetch_method', + existing_type=sa.VARCHAR(length=20), + comment='Fetch method (http, playwright)', + existing_nullable=False) + op.alter_column('price_history', 'fetch_status', + existing_type=sa.VARCHAR(length=20), + comment='Fetch status (success, partial, failed)', + existing_nullable=False) + op.alter_column('price_history', 'fetched_at', + existing_type=postgresql.TIMESTAMP(), + comment='Scraping timestamp', + existing_nullable=False) + op.alter_column('product_images', 'image_url', + existing_type=sa.TEXT(), + comment='Image URL', + existing_nullable=False) + op.alter_column('product_images', 'position', + existing_type=sa.INTEGER(), + comment='Image position (0=main)', + existing_nullable=False) + op.alter_column('product_specs', 'spec_key', + existing_type=sa.VARCHAR(length=200), + comment="Specification key (e.g., 'Brand', 'Color')", + existing_nullable=False) + op.alter_column('product_specs', 'spec_value', + existing_type=sa.TEXT(), + comment='Specification value', + existing_nullable=False) + op.add_column('products', sa.Column('rating_value', sa.Numeric(precision=3, scale=2), nullable=True, comment='Note moyenne')) + op.add_column('products', sa.Column('rating_count', sa.Integer(), nullable=True, comment="Nombre d'evaluations")) + op.add_column('products', sa.Column('amazon_choice', sa.Boolean(), nullable=True, comment="Badge Choix d'Amazon")) + op.add_column('products', sa.Column('amazon_choice_label', sa.Text(), nullable=True, comment="Libelle Choix d'Amazon")) + op.add_column('products', sa.Column('discount_text', sa.Text(), nullable=True, comment='Texte de reduction affiche')) + op.add_column('products', sa.Column('stock_text', sa.Text(), nullable=True, comment='Texte brut du stock')) + op.add_column('products', sa.Column('in_stock', sa.Boolean(), nullable=True, comment='Disponibilite derivee')) + op.add_column('products', sa.Column('model_number', sa.Text(), nullable=True, comment='Numero du modele')) + op.add_column('products', sa.Column('model_name', sa.Text(), nullable=True, comment='Nom du modele')) + op.alter_column('products', 'source', + existing_type=sa.VARCHAR(length=50), + comment='Store ID (amazon, cdiscount, etc.)', + existing_nullable=False) + op.alter_column('products', 'reference', + existing_type=sa.VARCHAR(length=100), + comment='Product reference (ASIN, SKU, etc.)', + existing_nullable=False) + op.alter_column('products', 'url', + existing_type=sa.TEXT(), + comment='Canonical product URL', + existing_nullable=False) + op.alter_column('products', 'title', + existing_type=sa.TEXT(), + comment='Product title', + existing_nullable=True) + op.alter_column('products', 'category', + existing_type=sa.TEXT(), + comment='Product category (breadcrumb)', + existing_nullable=True) + op.alter_column('products', 'description', + existing_type=sa.TEXT(), + comment='Product description', + existing_nullable=True) + op.alter_column('products', 'currency', + existing_type=sa.VARCHAR(length=3), + comment='Currency code (EUR, USD, GBP)', + existing_nullable=True) + op.alter_column('products', 'msrp', + existing_type=sa.NUMERIC(precision=10, scale=2), + comment='Recommended price', + existing_nullable=True) + op.alter_column('products', 'first_seen_at', + existing_type=postgresql.TIMESTAMP(), + comment='First scraping timestamp', + existing_nullable=False) + op.alter_column('products', 'last_updated_at', + existing_type=postgresql.TIMESTAMP(), + comment='Last metadata update', + existing_nullable=False) + op.alter_column('scraping_logs', 'url', + existing_type=sa.TEXT(), + comment='Scraped URL', + existing_nullable=False) + op.alter_column('scraping_logs', 'source', + existing_type=sa.VARCHAR(length=50), + comment='Store ID (amazon, cdiscount, etc.)', + existing_nullable=False) + op.alter_column('scraping_logs', 'reference', + existing_type=sa.VARCHAR(length=100), + comment='Product reference (if extracted)', + existing_nullable=True) + op.alter_column('scraping_logs', 'fetch_method', + existing_type=sa.VARCHAR(length=20), + comment='Fetch method (http, playwright)', + existing_nullable=False) + op.alter_column('scraping_logs', 'fetch_status', + existing_type=sa.VARCHAR(length=20), + comment='Fetch status (success, partial, failed)', + existing_nullable=False) + op.alter_column('scraping_logs', 'fetched_at', + existing_type=postgresql.TIMESTAMP(), + comment='Scraping timestamp', + existing_nullable=False) + op.alter_column('scraping_logs', 'duration_ms', + existing_type=sa.INTEGER(), + comment='Fetch duration in milliseconds', + existing_nullable=True) + op.alter_column('scraping_logs', 'html_size_bytes', + existing_type=sa.INTEGER(), + comment='HTML response size in bytes', + existing_nullable=True) + op.alter_column('scraping_logs', 'errors', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + comment='Error messages (list of strings)', + existing_nullable=True) + op.alter_column('scraping_logs', 'notes', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + comment='Debug notes (list of strings)', + existing_nullable=True) + op.alter_column('webhooks', 'event', + existing_type=sa.VARCHAR(length=50), + comment='Event name', + existing_nullable=False) + op.alter_column('webhooks', 'url', + existing_type=sa.TEXT(), + comment='Webhook URL', + existing_nullable=False) + op.alter_column('webhooks', 'secret', + existing_type=sa.VARCHAR(length=200), + comment='Secret optionnel', + existing_nullable=True) + op.alter_column('webhooks', 'created_at', + existing_type=postgresql.TIMESTAMP(), + comment='Creation timestamp', + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('webhooks', 'created_at', + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment='Creation timestamp', + existing_nullable=False) + op.alter_column('webhooks', 'secret', + existing_type=sa.VARCHAR(length=200), + comment=None, + existing_comment='Secret optionnel', + existing_nullable=True) + op.alter_column('webhooks', 'url', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Webhook URL', + existing_nullable=False) + op.alter_column('webhooks', 'event', + existing_type=sa.VARCHAR(length=50), + comment=None, + existing_comment='Event name', + existing_nullable=False) + op.alter_column('scraping_logs', 'notes', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + comment=None, + existing_comment='Debug notes (list of strings)', + existing_nullable=True) + op.alter_column('scraping_logs', 'errors', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + comment=None, + existing_comment='Error messages (list of strings)', + existing_nullable=True) + op.alter_column('scraping_logs', 'html_size_bytes', + existing_type=sa.INTEGER(), + comment=None, + existing_comment='HTML response size in bytes', + existing_nullable=True) + op.alter_column('scraping_logs', 'duration_ms', + existing_type=sa.INTEGER(), + comment=None, + existing_comment='Fetch duration in milliseconds', + existing_nullable=True) + op.alter_column('scraping_logs', 'fetched_at', + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment='Scraping timestamp', + existing_nullable=False) + op.alter_column('scraping_logs', 'fetch_status', + existing_type=sa.VARCHAR(length=20), + comment=None, + existing_comment='Fetch status (success, partial, failed)', + existing_nullable=False) + op.alter_column('scraping_logs', 'fetch_method', + existing_type=sa.VARCHAR(length=20), + comment=None, + existing_comment='Fetch method (http, playwright)', + existing_nullable=False) + op.alter_column('scraping_logs', 'reference', + existing_type=sa.VARCHAR(length=100), + comment=None, + existing_comment='Product reference (if extracted)', + existing_nullable=True) + op.alter_column('scraping_logs', 'source', + existing_type=sa.VARCHAR(length=50), + comment=None, + existing_comment='Store ID (amazon, cdiscount, etc.)', + existing_nullable=False) + op.alter_column('scraping_logs', 'url', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Scraped URL', + existing_nullable=False) + op.alter_column('products', 'last_updated_at', + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment='Last metadata update', + existing_nullable=False) + op.alter_column('products', 'first_seen_at', + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment='First scraping timestamp', + existing_nullable=False) + op.alter_column('products', 'msrp', + existing_type=sa.NUMERIC(precision=10, scale=2), + comment=None, + existing_comment='Recommended price', + existing_nullable=True) + op.alter_column('products', 'currency', + existing_type=sa.VARCHAR(length=3), + comment=None, + existing_comment='Currency code (EUR, USD, GBP)', + existing_nullable=True) + op.alter_column('products', 'description', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Product description', + existing_nullable=True) + op.alter_column('products', 'category', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Product category (breadcrumb)', + existing_nullable=True) + op.alter_column('products', 'title', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Product title', + existing_nullable=True) + op.alter_column('products', 'url', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Canonical product URL', + existing_nullable=False) + op.alter_column('products', 'reference', + existing_type=sa.VARCHAR(length=100), + comment=None, + existing_comment='Product reference (ASIN, SKU, etc.)', + existing_nullable=False) + op.alter_column('products', 'source', + existing_type=sa.VARCHAR(length=50), + comment=None, + existing_comment='Store ID (amazon, cdiscount, etc.)', + existing_nullable=False) + op.drop_column('products', 'model_name') + op.drop_column('products', 'model_number') + op.drop_column('products', 'in_stock') + op.drop_column('products', 'stock_text') + op.drop_column('products', 'discount_text') + op.drop_column('products', 'amazon_choice_label') + op.drop_column('products', 'amazon_choice') + op.drop_column('products', 'rating_count') + op.drop_column('products', 'rating_value') + op.alter_column('product_specs', 'spec_value', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Specification value', + existing_nullable=False) + op.alter_column('product_specs', 'spec_key', + existing_type=sa.VARCHAR(length=200), + comment=None, + existing_comment="Specification key (e.g., 'Brand', 'Color')", + existing_nullable=False) + op.alter_column('product_images', 'position', + existing_type=sa.INTEGER(), + comment=None, + existing_comment='Image position (0=main)', + existing_nullable=False) + op.alter_column('product_images', 'image_url', + existing_type=sa.TEXT(), + comment=None, + existing_comment='Image URL', + existing_nullable=False) + op.alter_column('price_history', 'fetched_at', + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment='Scraping timestamp', + existing_nullable=False) + op.alter_column('price_history', 'fetch_status', + existing_type=sa.VARCHAR(length=20), + comment=None, + existing_comment='Fetch status (success, partial, failed)', + existing_nullable=False) + op.alter_column('price_history', 'fetch_method', + existing_type=sa.VARCHAR(length=20), + comment=None, + existing_comment='Fetch method (http, playwright)', + existing_nullable=False) + op.alter_column('price_history', 'stock_status', + existing_type=sa.VARCHAR(length=20), + comment=None, + existing_comment='Stock status (in_stock, out_of_stock, unknown)', + existing_nullable=True) + op.alter_column('price_history', 'shipping_cost', + existing_type=sa.NUMERIC(precision=10, scale=2), + comment=None, + existing_comment='Shipping cost', + existing_nullable=True) + op.alter_column('price_history', 'price', + existing_type=sa.NUMERIC(precision=10, scale=2), + comment=None, + existing_comment='Product price', + existing_nullable=True) + # ### end Alembic commands ### diff --git a/pricewatch/app/db/migrations/versions/1467e98fcbea_ajout_champs_amazon_produit.py b/pricewatch/app/db/migrations/versions/1467e98fcbea_ajout_champs_amazon_produit.py new file mode 100644 index 0000000..e8e67f4 --- /dev/null +++ b/pricewatch/app/db/migrations/versions/1467e98fcbea_ajout_champs_amazon_produit.py @@ -0,0 +1,28 @@ +"""Ajout champs Amazon produit + +Revision ID: 1467e98fcbea +Revises: 3e68b0f0c9e4 +Create Date: 2026-01-17 20:08:32.991650 +""" + +from alembic import op +import sqlalchemy as sa + + +# Revision identifiers, used by Alembic. +revision = '1467e98fcbea' +down_revision = '3e68b0f0c9e4' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/pricewatch/app/db/migrations/versions/20260117_03_classification_rules.py b/pricewatch/app/db/migrations/versions/20260117_03_classification_rules.py new file mode 100644 index 0000000..ab79c09 --- /dev/null +++ b/pricewatch/app/db/migrations/versions/20260117_03_classification_rules.py @@ -0,0 +1,114 @@ +"""Ajout classification rules et type produit + +Revision ID: 20260117_03_classification_rules +Revises: 3e68b0f0c9e4 +Create Date: 2026-01-17 20:05:00.000000 +""" + +from datetime import datetime, timezone + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# Revision identifiers, used by Alembic. +revision = "20260117_03_classification_rules" +down_revision = "3e68b0f0c9e4" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "products", + sa.Column("type", sa.Text(), nullable=True, comment="Product type"), + ) + + op.create_table( + "classification_rules", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column("category", sa.String(length=80), nullable=True, comment="Categorie cible"), + sa.Column("type", sa.String(length=80), nullable=True, comment="Type cible"), + sa.Column( + "keywords", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + comment="Mots-cles de matching", + ), + sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")), + sa.Column( + "created_at", + sa.TIMESTAMP(), + nullable=False, + server_default=sa.text("CURRENT_TIMESTAMP"), + comment="Creation timestamp", + ), + ) + op.create_index("ix_classification_rule_order", "classification_rules", ["sort_order"]) + op.create_index("ix_classification_rule_active", "classification_rules", ["is_active"]) + + rules_table = sa.table( + "classification_rules", + sa.column("category", sa.String), + sa.column("type", sa.String), + sa.column("keywords", postgresql.JSONB), + sa.column("sort_order", sa.Integer), + sa.column("is_active", sa.Boolean), + sa.column("created_at", sa.TIMESTAMP), + ) + + now = datetime.now(timezone.utc) + op.bulk_insert( + rules_table, + [ + { + "category": "Informatique", + "type": "Ecran", + "keywords": ["ecran", "moniteur", "display"], + "sort_order": 0, + "is_active": True, + "created_at": now, + }, + { + "category": "Informatique", + "type": "PC portable", + "keywords": ["pc portable", "ordinateur portable", "laptop", "notebook"], + "sort_order": 1, + "is_active": True, + "created_at": now, + }, + { + "category": "Informatique", + "type": "Unite centrale", + "keywords": ["unite centrale", "tour", "desktop", "pc fixe"], + "sort_order": 2, + "is_active": True, + "created_at": now, + }, + { + "category": "Informatique", + "type": "Clavier", + "keywords": ["clavier", "keyboard"], + "sort_order": 3, + "is_active": True, + "created_at": now, + }, + { + "category": "Informatique", + "type": "Souris", + "keywords": ["souris", "mouse"], + "sort_order": 4, + "is_active": True, + "created_at": now, + }, + ], + ) + + +def downgrade() -> None: + op.drop_index("ix_classification_rule_active", table_name="classification_rules") + op.drop_index("ix_classification_rule_order", table_name="classification_rules") + op.drop_table("classification_rules") + op.drop_column("products", "type") diff --git a/pricewatch/app/db/migrations/versions/3e68b0f0c9e4_ajout_champs_amazon_produit.py b/pricewatch/app/db/migrations/versions/3e68b0f0c9e4_ajout_champs_amazon_produit.py new file mode 100644 index 0000000..1d9c955 --- /dev/null +++ b/pricewatch/app/db/migrations/versions/3e68b0f0c9e4_ajout_champs_amazon_produit.py @@ -0,0 +1,28 @@ +"""Ajout champs Amazon produit + +Revision ID: 3e68b0f0c9e4 +Revises: 0014e51c4927 +Create Date: 2026-01-17 19:45:03.730218 +""" + +from alembic import op +import sqlalchemy as sa + + +# Revision identifiers, used by Alembic. +revision = '3e68b0f0c9e4' +down_revision = '0014e51c4927' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/pricewatch/app/db/migrations/versions/__pycache__/0014e51c4927_ajout_champs_amazon_produit.cpython-313.pyc b/pricewatch/app/db/migrations/versions/__pycache__/0014e51c4927_ajout_champs_amazon_produit.cpython-313.pyc new file mode 100644 index 0000000..1d3f310 Binary files /dev/null and b/pricewatch/app/db/migrations/versions/__pycache__/0014e51c4927_ajout_champs_amazon_produit.cpython-313.pyc differ diff --git a/pricewatch/app/db/migrations/versions/__pycache__/1467e98fcbea_ajout_champs_amazon_produit.cpython-313.pyc b/pricewatch/app/db/migrations/versions/__pycache__/1467e98fcbea_ajout_champs_amazon_produit.cpython-313.pyc new file mode 100644 index 0000000..f451a3b Binary files /dev/null and b/pricewatch/app/db/migrations/versions/__pycache__/1467e98fcbea_ajout_champs_amazon_produit.cpython-313.pyc differ diff --git a/pricewatch/app/db/migrations/versions/__pycache__/20260114_02_webhooks.cpython-313.pyc b/pricewatch/app/db/migrations/versions/__pycache__/20260114_02_webhooks.cpython-313.pyc new file mode 100644 index 0000000..848d12a Binary files /dev/null and b/pricewatch/app/db/migrations/versions/__pycache__/20260114_02_webhooks.cpython-313.pyc differ diff --git a/pricewatch/app/db/migrations/versions/__pycache__/20260115_02_product_details.cpython-313.pyc b/pricewatch/app/db/migrations/versions/__pycache__/20260115_02_product_details.cpython-313.pyc new file mode 100644 index 0000000..1f37a27 Binary files /dev/null and b/pricewatch/app/db/migrations/versions/__pycache__/20260115_02_product_details.cpython-313.pyc differ diff --git a/pricewatch/app/db/migrations/versions/__pycache__/20260117_03_classification_rules.cpython-313.pyc b/pricewatch/app/db/migrations/versions/__pycache__/20260117_03_classification_rules.cpython-313.pyc new file mode 100644 index 0000000..4264e6a Binary files /dev/null and b/pricewatch/app/db/migrations/versions/__pycache__/20260117_03_classification_rules.cpython-313.pyc differ diff --git a/pricewatch/app/db/migrations/versions/__pycache__/3e68b0f0c9e4_ajout_champs_amazon_produit.cpython-313.pyc b/pricewatch/app/db/migrations/versions/__pycache__/3e68b0f0c9e4_ajout_champs_amazon_produit.cpython-313.pyc new file mode 100644 index 0000000..e38926c Binary files /dev/null and b/pricewatch/app/db/migrations/versions/__pycache__/3e68b0f0c9e4_ajout_champs_amazon_produit.cpython-313.pyc differ diff --git a/pricewatch/app/db/models.py b/pricewatch/app/db/models.py index 20693b1..88ef2f2 100644 --- a/pricewatch/app/db/models.py +++ b/pricewatch/app/db/models.py @@ -84,6 +84,36 @@ class Product(Base): msrp: Mapped[Optional[Decimal]] = mapped_column( Numeric(10, 2), nullable=True, comment="Recommended price" ) + type: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, comment="Product type" + ) + rating_value: Mapped[Optional[Decimal]] = mapped_column( + Numeric(3, 2), nullable=True, comment="Note moyenne" + ) + rating_count: Mapped[Optional[int]] = mapped_column( + Integer, nullable=True, comment="Nombre d'evaluations" + ) + amazon_choice: Mapped[Optional[bool]] = mapped_column( + Boolean, nullable=True, comment="Badge Choix d'Amazon" + ) + amazon_choice_label: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, comment="Libelle Choix d'Amazon" + ) + discount_text: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, comment="Texte de reduction affiche" + ) + stock_text: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, comment="Texte brut du stock" + ) + in_stock: Mapped[Optional[bool]] = mapped_column( + Boolean, nullable=True, comment="Disponibilite derivee" + ) + model_number: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, comment="Numero du modele" + ) + model_name: Mapped[Optional[str]] = mapped_column( + Text, nullable=True, comment="Nom du modele" + ) # Timestamps first_seen_at: Mapped[datetime] = mapped_column( @@ -331,6 +361,45 @@ class ScrapingLog(Base): return f"" +class ClassificationRule(Base): + """ + Regles de classification categorie/type basees sur des mots-cles. + """ + + __tablename__ = "classification_rules" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + category: Mapped[Optional[str]] = mapped_column( + String(80), nullable=True, comment="Categorie cible" + ) + type: Mapped[Optional[str]] = mapped_column( + String(80), nullable=True, comment="Type cible" + ) + keywords: Mapped[list[str]] = mapped_column( + JSON().with_variant(JSONB, "postgresql"), + nullable=False, + default=list, + comment="Mots-cles de matching", + ) + sort_order: Mapped[int] = mapped_column( + Integer, nullable=False, default=0, comment="Ordre de priorite (0=haut)" + ) + is_active: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=True, comment="Regle active" + ) + created_at: Mapped[datetime] = mapped_column( + TIMESTAMP, nullable=False, default=utcnow, comment="Creation timestamp" + ) + + __table_args__ = ( + Index("ix_classification_rule_order", "sort_order"), + Index("ix_classification_rule_active", "is_active"), + ) + + def __repr__(self) -> str: + return f"" + + class Webhook(Base): """ Webhooks pour notifications externes. diff --git a/pricewatch/app/db/repository.py b/pricewatch/app/db/repository.py index d0b451c..daa915b 100644 --- a/pricewatch/app/db/repository.py +++ b/pricewatch/app/db/repository.py @@ -13,7 +13,14 @@ from sqlalchemy.orm import Session from pricewatch.app.core.logging import get_logger from pricewatch.app.core.schema import ProductSnapshot -from pricewatch.app.db.models import PriceHistory, Product, ProductImage, ProductSpec, ScrapingLog +from pricewatch.app.db.models import ( + ClassificationRule, + PriceHistory, + Product, + ProductImage, + ProductSpec, + ScrapingLog, +) logger = get_logger("db.repository") @@ -49,12 +56,58 @@ class ProductRepository: product.title = snapshot.title if snapshot.category: product.category = snapshot.category + if snapshot.type: + product.type = snapshot.type if snapshot.description: product.description = snapshot.description if snapshot.currency: product.currency = snapshot.currency if snapshot.msrp is not None: product.msrp = snapshot.msrp + if snapshot.rating_value is not None: + product.rating_value = snapshot.rating_value + if snapshot.rating_count is not None: + product.rating_count = snapshot.rating_count + if snapshot.amazon_choice is not None: + product.amazon_choice = snapshot.amazon_choice + if snapshot.amazon_choice_label: + product.amazon_choice_label = snapshot.amazon_choice_label + if snapshot.discount_text: + product.discount_text = snapshot.discount_text + if snapshot.stock_text: + product.stock_text = snapshot.stock_text + if snapshot.in_stock is not None: + product.in_stock = snapshot.in_stock + if snapshot.model_number: + product.model_number = snapshot.model_number + if snapshot.model_name: + product.model_name = snapshot.model_name + + def apply_classification(self, snapshot: ProductSnapshot) -> None: + """Applique les regles de classification au snapshot.""" + if not snapshot.title: + return + + rules = ( + self.session.query(ClassificationRule) + .filter(ClassificationRule.is_active == True) + .order_by(ClassificationRule.sort_order, ClassificationRule.id) + .all() + ) + if not rules: + return + + title = snapshot.title.lower() + for rule in rules: + keywords = rule.keywords or [] + if isinstance(keywords, str): + keywords = [keywords] + if any(keyword and keyword.lower() in title for keyword in keywords): + if rule.category: + snapshot.category = rule.category + if rule.type: + snapshot.type = rule.type + return def add_price_history(self, product: Product, snapshot: ProductSnapshot) -> Optional[PriceHistory]: """Ajoute une entree d'historique de prix si inexistante.""" diff --git a/pricewatch/app/scraping/__pycache__/pipeline.cpython-313.pyc b/pricewatch/app/scraping/__pycache__/pipeline.cpython-313.pyc index da613bb..c26e458 100644 Binary files a/pricewatch/app/scraping/__pycache__/pipeline.cpython-313.pyc and b/pricewatch/app/scraping/__pycache__/pipeline.cpython-313.pyc differ diff --git a/pricewatch/app/scraping/pipeline.py b/pricewatch/app/scraping/pipeline.py index cbf7865..8874a32 100644 --- a/pricewatch/app/scraping/pipeline.py +++ b/pricewatch/app/scraping/pipeline.py @@ -25,7 +25,12 @@ class ScrapingPipeline: def __init__(self, config: Optional[AppConfig] = None) -> None: self.config = config - def process_snapshot(self, snapshot: ProductSnapshot, save_to_db: bool = True) -> Optional[int]: + def process_snapshot( + self, + snapshot: ProductSnapshot, + save_to_db: bool = True, + apply_classification: bool = True, + ) -> Optional[int]: """ Persiste un snapshot en base si active. @@ -39,6 +44,8 @@ class ScrapingPipeline: try: with get_session(app_config) as session: repo = ProductRepository(session) + if apply_classification: + repo.apply_classification(snapshot) product_id = repo.safe_save_snapshot(snapshot) session.commit() return product_id diff --git a/pricewatch/app/stores/amazon/__pycache__/store.cpython-313.pyc b/pricewatch/app/stores/amazon/__pycache__/store.cpython-313.pyc index 7d485cd..a15294b 100644 Binary files a/pricewatch/app/stores/amazon/__pycache__/store.cpython-313.pyc and b/pricewatch/app/stores/amazon/__pycache__/store.cpython-313.pyc differ diff --git a/pricewatch/app/stores/amazon/selectors.yml b/pricewatch/app/stores/amazon/selectors.yml index 652ab5e..e41d3d4 100755 --- a/pricewatch/app/stores/amazon/selectors.yml +++ b/pricewatch/app/stores/amazon/selectors.yml @@ -15,6 +15,13 @@ price: - "#priceblock_dealprice" - ".a-price-range .a-price .a-offscreen" +# Texte de réduction explicite +discount_text: + - "#regularprice_savings" + - "#dealprice_savings" + - "#savingsPercentage" + - "span.savingsPercentage" + # Devise (généralement dans le symbole) currency: - "span.a-price-symbol" @@ -32,6 +39,24 @@ stock_status: - "#availability" - ".a-declarative .a-size-medium" +# Note moyenne +rating_value: + - "#acrPopover" + - "#averageCustomerReviews .a-icon-alt" + - "#averageCustomerReviews span.a-icon-alt" + +# Nombre d'évaluations +rating_count: + - "#acrCustomerReviewText" + - "#acrCustomerReviewLink" + +# Badge Choix d'Amazon +amazon_choice: + - "#acBadge_feature_div" + - "#acBadge_feature_div .ac-badge" + - "#acBadge_feature_div .ac-badge-rectangle" + - "#acBadge_feature_div .ac-badge-rectangle-icon" + # Images produit images: - "#landingImage" @@ -44,6 +69,13 @@ category: - "#wayfinding-breadcrumbs_feature_div" - ".a-breadcrumb" +# Description (détails de l'article) +description: + - "#detailBullets_feature_div" + - "#detailBulletsWrapper_feature_div" + - "#productDetails_detailBullets_sections1" + - "#feature-bullets" + # Caractéristiques techniques (table specs) specs_table: - "#productDetails_techSpec_section_1" diff --git a/pricewatch/app/stores/amazon/store.py b/pricewatch/app/stores/amazon/store.py index 26f98e2..0b0886f 100755 --- a/pricewatch/app/stores/amazon/store.py +++ b/pricewatch/app/stores/amazon/store.py @@ -130,13 +130,19 @@ class AmazonStore(BaseStore): title = self._extract_title(soup, debug_info) price = self._extract_price(soup, debug_info) currency = self._extract_currency(soup, debug_info) - stock_status = self._extract_stock(soup, debug_info) - images = self._extract_images(soup, debug_info) + stock_status, stock_text, in_stock = self._extract_stock_details(soup, debug_info) + main_image, gallery_images, images = self._extract_images(soup, debug_info) category = self._extract_category(soup, debug_info) specs = self._extract_specs(soup, debug_info) description = self._extract_description(soup, debug_info) msrp = self._extract_msrp(soup, debug_info) reference = self.extract_reference(url) or self._extract_asin_from_html(soup) + rating_value = self._extract_rating_value(soup, debug_info) + rating_count = self._extract_rating_count(soup, debug_info) + amazon_choice, amazon_choice_label = self._extract_amazon_choice(soup, debug_info) + discount_text = self._extract_discount_text(soup, debug_info) + model_number, model_name = self._extract_model_details(specs) + asin = reference # Déterminer le statut final (ne pas écraser FAILED) if debug_info.status != DebugStatus.FAILED: @@ -153,12 +159,24 @@ class AmazonStore(BaseStore): currency=currency or "EUR", shipping_cost=None, # Difficile à extraire stock_status=stock_status, + stock_text=stock_text, + in_stock=in_stock, reference=reference, + asin=asin, category=category, description=description, images=images, + main_image=main_image, + gallery_images=gallery_images, specs=specs, msrp=msrp, + rating_value=rating_value, + rating_count=rating_count, + amazon_choice=amazon_choice, + amazon_choice_label=amazon_choice_label, + discount_text=discount_text, + model_number=model_number, + model_name=model_name, debug=debug_info, ) @@ -203,14 +221,26 @@ class AmazonStore(BaseStore): return None def _extract_description(self, soup: BeautifulSoup, debug: DebugInfo) -> Optional[str]: - """Extrait la description (meta tags).""" - meta = soup.find("meta", property="og:description") or soup.find( - "meta", attrs={"name": "description"} - ) - if meta: - description = meta.get("content", "").strip() - if description: - return description + """Extrait la description depuis les détails de l'article.""" + selectors = self.get_selector("description", []) + if isinstance(selectors, str): + selectors = [selectors] + + for selector in selectors: + element = soup.select_one(selector) + if not element: + continue + items = [ + item.get_text(" ", strip=True) + for item in element.select("li") + if item.get_text(strip=True) + ] + if items: + return "\n".join(items) + text = " ".join(element.stripped_strings) + if text: + return text + return None def _extract_price(self, soup: BeautifulSoup, debug: DebugInfo) -> Optional[float]: @@ -271,8 +301,10 @@ class AmazonStore(BaseStore): # Défaut basé sur le domaine return "EUR" - def _extract_stock(self, soup: BeautifulSoup, debug: DebugInfo) -> StockStatus: - """Extrait le statut de stock.""" + def _extract_stock_details( + self, soup: BeautifulSoup, debug: DebugInfo + ) -> tuple[StockStatus, Optional[str], Optional[bool]]: + """Extrait le statut de stock avec texte brut.""" selectors = self.get_selector("stock_status", []) if isinstance(selectors, str): selectors = [selectors] @@ -280,22 +312,27 @@ class AmazonStore(BaseStore): for selector in selectors: element = soup.select_one(selector) if element: - text = element.get_text(strip=True).lower() - if "en stock" in text or "available" in text or "in stock" in text: - return StockStatus.IN_STOCK + text = element.get_text(strip=True) + normalized = text.lower() + if "en stock" in normalized or "available" in normalized or "in stock" in normalized: + return StockStatus.IN_STOCK, text, True elif ( - "rupture" in text - or "indisponible" in text - or "out of stock" in text + "rupture" in normalized + or "indisponible" in normalized + or "out of stock" in normalized ): - return StockStatus.OUT_OF_STOCK + return StockStatus.OUT_OF_STOCK, text, False - return StockStatus.UNKNOWN + return StockStatus.UNKNOWN, None, None - def _extract_images(self, soup: BeautifulSoup, debug: DebugInfo) -> list[str]: - """Extrait les URLs d'images.""" - images = [] - seen = set() + def _extract_images( + self, soup: BeautifulSoup, debug: DebugInfo + ) -> tuple[Optional[str], list[str], list[str]]: + """Extrait l'image principale et la galerie.""" + images: list[str] = [] + seen: set[str] = set() + main_image: Optional[str] = None + max_gallery = 15 selectors = self.get_selector("images", []) if isinstance(selectors, str): selectors = [selectors] @@ -309,6 +346,8 @@ class AmazonStore(BaseStore): if self._is_product_image(url) and url not in seen: images.append(url) seen.add(url) + if main_image is None: + main_image = url dynamic = element.get("data-a-dynamic-image") if dynamic: urls = self._extract_dynamic_images(dynamic) @@ -316,6 +355,8 @@ class AmazonStore(BaseStore): if self._is_product_image(dyn_url) and dyn_url not in seen: images.append(dyn_url) seen.add(dyn_url) + if main_image is None: + main_image = dyn_url # Fallback: chercher tous les img tags si aucune image trouvée if not images: @@ -326,8 +367,15 @@ class AmazonStore(BaseStore): if url not in seen: images.append(url) seen.add(url) + if main_image is None: + main_image = url - return images + if main_image is None and images: + main_image = images[0] + gallery_images = [url for url in images if url != main_image] + gallery_images = gallery_images[:max_gallery] + final_images = [main_image] + gallery_images if main_image else gallery_images + return main_image, gallery_images, final_images def _extract_dynamic_images(self, raw: str) -> list[str]: """Extrait les URLs du JSON data-a-dynamic-image.""" @@ -393,8 +441,111 @@ class AmazonStore(BaseStore): if key and value: specs[key] = value + # Détails de l'article sous forme de liste + detail_list = soup.select("#detailBullets_feature_div li") + for item in detail_list: + text = item.get_text(" ", strip=True) + if ":" not in text: + continue + key, value = text.split(":", 1) + key = key.strip() + value = value.strip() + if key and value and key not in specs: + specs[key] = value + return specs + def _extract_rating_value(self, soup: BeautifulSoup, debug: DebugInfo) -> Optional[float]: + """Extrait la note moyenne.""" + selectors = self.get_selector("rating_value", []) + if isinstance(selectors, str): + selectors = [selectors] + + for selector in selectors: + element = soup.select_one(selector) + if not element: + continue + text = element.get_text(" ", strip=True) or element.get("title", "").strip() + match = re.search(r"([\d.,]+)", text) + if match: + value = match.group(1).replace(",", ".") + try: + return float(value) + except ValueError: + continue + return None + + def _extract_rating_count(self, soup: BeautifulSoup, debug: DebugInfo) -> Optional[int]: + """Extrait le nombre d'évaluations.""" + selectors = self.get_selector("rating_count", []) + if isinstance(selectors, str): + selectors = [selectors] + + for selector in selectors: + element = soup.select_one(selector) + if not element: + continue + text = element.get_text(" ", strip=True) + match = re.search(r"([\d\s\u202f\u00a0]+)", text) + if match: + numeric = re.sub(r"[^\d]", "", match.group(1)) + if numeric: + return int(numeric) + return None + + def _extract_amazon_choice( + self, soup: BeautifulSoup, debug: DebugInfo + ) -> tuple[Optional[bool], Optional[str]]: + """Extrait le badge Choix d'Amazon.""" + selectors = self.get_selector("amazon_choice", []) + if isinstance(selectors, str): + selectors = [selectors] + + for selector in selectors: + element = soup.select_one(selector) + if element: + label_candidates = [ + element.get_text(" ", strip=True), + element.get("aria-label", "").strip(), + element.get("title", "").strip(), + element.get("data-a-badge-label", "").strip(), + ] + label = next((item for item in label_candidates if item), "") + normalized = label.lower() + if "choix d'amazon" in normalized or "amazon's choice" in normalized: + return True, label + if label: + return True, label + return True, None + return None, None + + def _extract_discount_text(self, soup: BeautifulSoup, debug: DebugInfo) -> Optional[str]: + """Extrait le texte de réduction explicite.""" + selectors = self.get_selector("discount_text", []) + if isinstance(selectors, str): + selectors = [selectors] + + for selector in selectors: + element = soup.select_one(selector) + if not element: + continue + text = element.get_text(" ", strip=True) + if text: + return text + return None + + def _extract_model_details(self, specs: dict[str, str]) -> tuple[Optional[str], Optional[str]]: + """Extrait le numero et le nom du modele depuis les specs.""" + model_number = None + model_name = None + for key, value in specs.items(): + normalized = key.lower() + if "numéro du modèle de l'article" in normalized or "numero du modele de l'article" in normalized: + model_number = value + if "nom du modèle" in normalized or "nom du modele" in normalized: + model_name = value + return model_number, model_name + def _extract_asin_from_html(self, soup: BeautifulSoup) -> Optional[str]: """Extrait l'ASIN depuis le HTML (fallback).""" selectors = self.get_selector("asin", []) diff --git a/pricewatch/app/tasks/scheduler.py b/pricewatch/app/tasks/scheduler.py index cb11883..30c1ce5 100644 --- a/pricewatch/app/tasks/scheduler.py +++ b/pricewatch/app/tasks/scheduler.py @@ -6,6 +6,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta, timezone +import hashlib from typing import Optional import redis @@ -127,11 +128,13 @@ class ScrapingScheduler: interval_hours: int = 24, use_playwright: Optional[bool] = None, save_db: bool = True, + job_id: Optional[str] = None, ) -> ScheduledJobInfo: """Planifie un scraping recurrent (intervalle en heures).""" interval_seconds = int(timedelta(hours=interval_hours).total_seconds()) next_run = datetime.now(timezone.utc) + timedelta(seconds=interval_seconds) + resolved_job_id = job_id or self._job_id_for_url(url) job = self.scheduler.schedule( scheduled_time=next_run, func=scrape_product, @@ -139,6 +142,13 @@ class ScrapingScheduler: kwargs={"use_playwright": use_playwright, "save_db": save_db}, interval=interval_seconds, repeat=None, + id=resolved_job_id, ) logger.info(f"Job planifie: {job.id}, prochaine execution: {next_run.isoformat()}") return ScheduledJobInfo(job_id=job.id, next_run=next_run) + + @staticmethod + def _job_id_for_url(url: str) -> str: + """Genere un job_id stable pour eviter les doublons.""" + fingerprint = hashlib.sha1(url.strip().lower().encode("utf-8")).hexdigest() + return f"scrape_{fingerprint}" diff --git a/pricewatch/app/tasks/scrape.py b/pricewatch/app/tasks/scrape.py index 44486d6..7b4ff21 100644 --- a/pricewatch/app/tasks/scrape.py +++ b/pricewatch/app/tasks/scrape.py @@ -157,6 +157,36 @@ def scrape_product( ) success = False fetch_error = str(exc) + # Si captcha detecte via HTTP, forcer une tentative Playwright. + if ( + fetch_method == FetchMethod.HTTP + and use_playwright + and snapshot.debug.errors + and any("captcha" in error.lower() for error in snapshot.debug.errors) + ): + logger.info("[FETCH] Captcha detecte, tentative Playwright") + pw_result = fetch_playwright( + canonical_url, + headless=not headful, + timeout_ms=timeout_ms, + save_screenshot=save_screenshot, + ) + if pw_result.success and pw_result.html: + try: + snapshot = store.parse(pw_result.html, canonical_url) + snapshot.debug.method = FetchMethod.PLAYWRIGHT + snapshot.debug.duration_ms = pw_result.duration_ms + snapshot.debug.html_size_bytes = len(pw_result.html.encode("utf-8")) + snapshot.add_note("Captcha detecte via HTTP, fallback Playwright") + success = snapshot.debug.status != DebugStatus.FAILED + except Exception as exc: + snapshot.add_note(f"Fallback Playwright echoue: {exc}") + logger.error(f"[PARSE] Exception fallback Playwright: {exc}") + fetch_error = str(exc) + else: + error = pw_result.error or "Erreur Playwright" + snapshot.add_note(f"Fallback Playwright echoue: {error}") + fetch_error = error else: snapshot = ProductSnapshot( source=store.store_id, diff --git a/webui/dist/assets/fa-brands-400-D1LuMI3I.ttf b/webui/dist/assets/fa-brands-400-D1LuMI3I.ttf new file mode 100644 index 0000000..0f82a83 Binary files /dev/null and b/webui/dist/assets/fa-brands-400-D1LuMI3I.ttf differ diff --git a/webui/dist/assets/fa-brands-400-D_cYUPeE.woff2 b/webui/dist/assets/fa-brands-400-D_cYUPeE.woff2 new file mode 100644 index 0000000..3c5cf97 Binary files /dev/null and b/webui/dist/assets/fa-brands-400-D_cYUPeE.woff2 differ diff --git a/webui/dist/assets/fa-regular-400-BjRzuEpd.woff2 b/webui/dist/assets/fa-regular-400-BjRzuEpd.woff2 new file mode 100644 index 0000000..57d9179 Binary files /dev/null and b/webui/dist/assets/fa-regular-400-BjRzuEpd.woff2 differ diff --git a/webui/dist/assets/fa-regular-400-DZaxPHgR.ttf b/webui/dist/assets/fa-regular-400-DZaxPHgR.ttf new file mode 100644 index 0000000..9ee1919 Binary files /dev/null and b/webui/dist/assets/fa-regular-400-DZaxPHgR.ttf differ diff --git a/webui/dist/assets/fa-solid-900-CTAAxXor.woff2 b/webui/dist/assets/fa-solid-900-CTAAxXor.woff2 new file mode 100644 index 0000000..1672102 Binary files /dev/null and b/webui/dist/assets/fa-solid-900-CTAAxXor.woff2 differ diff --git a/webui/dist/assets/fa-solid-900-D0aA9rwL.ttf b/webui/dist/assets/fa-solid-900-D0aA9rwL.ttf new file mode 100644 index 0000000..1c10972 Binary files /dev/null and b/webui/dist/assets/fa-solid-900-D0aA9rwL.ttf differ diff --git a/webui/dist/assets/fa-v4compatibility-C9RhG_FT.woff2 b/webui/dist/assets/fa-v4compatibility-C9RhG_FT.woff2 new file mode 100644 index 0000000..fbafb22 Binary files /dev/null and b/webui/dist/assets/fa-v4compatibility-C9RhG_FT.woff2 differ diff --git a/webui/dist/assets/fa-v4compatibility-CCth-dXg.ttf b/webui/dist/assets/fa-v4compatibility-CCth-dXg.ttf new file mode 100644 index 0000000..3bcb67f Binary files /dev/null and b/webui/dist/assets/fa-v4compatibility-CCth-dXg.ttf differ diff --git a/webui/dist/assets/index-BURbFjJa.css b/webui/dist/assets/index-BURbFjJa.css new file mode 100644 index 0000000..f64c2b1 --- /dev/null +++ b/webui/dist/assets/index-BURbFjJa.css @@ -0,0 +1,5 @@ +.price-block[data-v-f8f63757]{display:flex;flex-direction:column;gap:4px;min-width:100px}.price-block--compact[data-v-f8f63757]{gap:2px}.price-block__current[data-v-f8f63757]{font-size:1.25rem;font-weight:700;line-height:1.2;color:var(--text)}.price-block--compact .price-block__current[data-v-f8f63757]{font-size:1.1rem}.price-block__msrp[data-v-f8f63757]{font-size:.8rem;color:var(--muted);text-decoration:line-through}.price-block__discount[data-v-f8f63757]{font-size:.75rem;color:var(--success);font-weight:500}.price-block__stock[data-v-f8f63757]{font-size:.75rem;font-weight:500;margin-top:4px}.price-block__ref[data-v-f8f63757]{font-size:.65rem;color:var(--muted);font-family:var(--font-mono);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-actions[data-v-d4ee6168]{display:flex;align-items:center;gap:6px;padding:8px 0;border-top:1px solid rgba(255,255,255,.06);margin-top:auto}.card-actions__btn[data-v-d4ee6168]{width:32px;height:32px;border-radius:8px;background:var(--surface-2);border:1px solid rgba(255,255,255,.08);color:var(--muted);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s ease;font-size:.8rem}.card-actions__btn[data-v-d4ee6168]:hover{background:var(--accent);color:#1b1b1b;border-color:var(--accent);transform:translateY(-1px)}.card-actions__btn--primary[data-v-d4ee6168]{background:var(--accent);color:#1b1b1b;border-color:var(--accent)}.card-actions__btn--primary[data-v-d4ee6168]:hover{transform:translateY(-1px);box-shadow:0 4px 12px #fe80194d}.card-actions__btn--active[data-v-d4ee6168]{background:var(--success);color:#1b1b1b;border-color:var(--success)}.card-actions__btn--active[data-v-d4ee6168]:hover{background:var(--success)}.product-card[data-v-3a31b9af]{background:var(--surface);border-radius:var(--radius);border:1px solid rgba(255,255,255,.08);box-shadow:0 12px 24px var(--shadow);display:flex;flex-direction:column;padding:16px;position:relative;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease}.product-card[data-v-3a31b9af]:hover,.product-card[data-v-3a31b9af]:focus-within{transform:translateY(-2px);box-shadow:0 16px 32px var(--shadow)}.product-card--accent[data-v-3a31b9af]{border-color:#fe801980;box-shadow:0 10px 30px #fe801926}.product-card__header[data-v-3a31b9af]{margin-bottom:12px}.product-card__identity[data-v-3a31b9af]{display:flex;align-items:flex-start;gap:10px}.product-card__store-icon[data-v-3a31b9af]{width:36px;height:36px;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:var(--surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden}.product-card__store-icon img[data-v-3a31b9af]{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;padding:2px}.product-card__store-initials[data-v-3a31b9af]{font-size:.65rem;font-weight:600;color:var(--muted)}.product-card__identity-text[data-v-3a31b9af]{flex:1;min-width:0}.product-card__title[data-v-3a31b9af]{font-size:.9rem;font-weight:600;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.product-card__store-name[data-v-3a31b9af]{font-size:.7rem;color:var(--muted);margin-top:2px}.product-card__thumbnail[data-v-3a31b9af]{width:100%;height:var(--pw-card-media-height, 140px);border-radius:10px;background:var(--surface-2);display:flex;align-items:center;justify-content:center;overflow:hidden;border:1px solid rgba(255,255,255,.06);margin-bottom:12px}.product-card__image[data-v-3a31b9af]{max-width:100%;max-height:100%;width:auto;height:auto}.product-card__image--contain[data-v-3a31b9af]{-o-object-fit:contain;object-fit:contain}.product-card__image--cover[data-v-3a31b9af]{-o-object-fit:cover;object-fit:cover;width:100%;height:100%}.product-card__price-zone[data-v-3a31b9af]{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid rgba(255,255,255,.06)}.product-card__history-zone[data-v-3a31b9af]{flex:1;display:flex;flex-direction:column;gap:8px}.product-card__chart-container[data-v-3a31b9af]{height:120px;border-radius:8px;padding:8px;background:var(--surface-2);border:1px solid rgba(255,255,255,.04);overflow:hidden}.product-card__no-history[data-v-3a31b9af]{height:100%;display:flex;align-items:center;justify-content:center;font-size:.75rem;color:var(--muted);opacity:.6}.product-card__history-stats[data-v-3a31b9af]{display:flex;justify-content:space-between;gap:8px;font-size:.7rem}.product-card__stat[data-v-3a31b9af]{display:flex;flex-direction:column;gap:2px}.product-card__stat-label[data-v-3a31b9af]{color:var(--muted);font-size:.65rem;text-transform:uppercase;letter-spacing:.3px}.product-card__stat-value[data-v-3a31b9af]{font-weight:600;font-family:var(--font-mono)}.product-card__trend[data-v-3a31b9af]{display:flex;align-items:center;gap:4px}.product-card__trend-delta[data-v-3a31b9af]{font-size:.65rem;opacity:.8}.product-card__update[data-v-3a31b9af]{font-size:.65rem;color:var(--muted);opacity:.8}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.top-3{top:.75rem}.z-40{z-index:40}.z-50{z-index:50}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-20{height:5rem}.h-full{height:100%}.max-h-\[20rem\]{max-height:20rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-full{width:100%}.min-w-\[44px\]{min-width:44px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-\[320px\]{max-width:320px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-white\/10{border-color:#ffffff1a}.border-white\/5{border-color:#ffffff0d}.bg-\[var\(--accent\)\]{background-color:var(--accent)}.bg-\[var\(--surface-2\)\]{background-color:var(--surface-2)}.bg-black\/40{background-color:#0006}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-2{padding-bottom:.5rem}.pr-10{padding-right:2.5rem}.text-left{text-align:left}.text-right{text-align:right}.text-\[0\.75rem\]{font-size:.75rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.text-\[var\(--danger\)\]{color:var(--danger)}.text-\[var\(--muted\)\]{color:var(--muted)}.text-\[var\(--success\)\]{color:var(--success)}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.opacity-50{opacity:.5}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{color-scheme:light dark;--pw-store-icon: 40px;--pw-card-height-factor: 1;--pw-card-mobile-height-factor: 1;--pw-card-media-height: 140px;--pw-card-columns: 3}.app-root{--bg: #282828;--surface: #3c3836;--surface-2: #504945;--text: #ebdbb2;--muted: #a89984;--accent: #fe8019;--danger: #fb4934;--success: #b8bb26;--warning: #fabd2f;--shadow: rgba(0, 0, 0, .45);--radius: 14px;--font-title: "Space Mono", "JetBrains Mono", "Fira Code", monospace;--font-body: "JetBrains Mono", "Fira Code", "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;--font-mono: "JetBrains Mono", "Fira Code", "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;--font-size: 16px;background:var(--bg);color:var(--text);min-height:100vh;font-family:var(--font-body);font-size:var(--font-size)}.app-root.theme-gruvbox-dark{--bg: #282828;--surface: #3c3836;--surface-2: #504945;--text: #ebdbb2;--muted: #a89984;--accent: #fe8019;--danger: #fb4934;--success: #b8bb26;--warning: #fabd2f;--shadow: rgba(0, 0, 0, .45)}.app-root.theme-gruvbox-light{--bg: #fbf1c7;--surface: #f2e5bc;--surface-2: #ebdbb2;--text: #3c3836;--muted: #7c6f64;--accent: #d65d0e;--danger: #cc241d;--success: #98971a;--warning: #d79921;--shadow: rgba(60, 56, 54, .25)}.app-root.theme-monokai-dark{--bg: #1f1f1b;--surface: #272822;--surface-2: #3b3c35;--text: #f8f8f2;--muted: #9b9a84;--accent: #f92672;--danger: #fd5ff1;--success: #a6e22e;--warning: #fd971f;--shadow: rgba(0, 0, 0, .55)}.app-root.theme-monokai-light{--bg: #f8f8f2;--surface: #e8e8e3;--surface-2: #dcdcd2;--text: #272822;--muted: #75715e;--accent: #f92672;--danger: #c0005f;--success: #2d8f2d;--warning: #fd971f;--shadow: rgba(39, 40, 34, .2)}.app-header{position:sticky;top:0;z-index:40;background:linear-gradient(90deg,var(--surface),var(--surface-2));border-bottom:1px solid rgba(255,255,255,.06);box-shadow:0 10px 24px var(--shadow)}.vintage-shadow{box-shadow:0 14px 28px var(--shadow)}.icon-btn{width:42px;height:42px;border-radius:50%;background:var(--surface-2);color:var(--text);display:inline-flex;align-items:center;justify-content:center;transition:transform .15s ease,background .15s ease}.icon-btn:hover{background:var(--accent);color:#1b1b1b;transform:translateY(-1px)}.icon-btn:active{transform:translateY(1px)}.pill{border-radius:999px;padding:4px 10px;font-size:.75rem;background:var(--surface-2);color:var(--muted)}.panel{background:var(--surface);border-radius:var(--radius);border:1px solid rgba(255,255,255,.05)}.card{background:var(--surface);border-radius:var(--radius);border:1px solid rgba(255,255,255,.08);box-shadow:0 12px 24px var(--shadow);display:flex;flex-direction:column;gap:12px;padding:16px;position:relative}.card-thumbnail{width:100%;height:var(--pw-card-media-height, 160px);display:flex;align-items:center;justify-content:center;border-radius:16px;background:var(--surface-2);overflow:hidden;border:1px solid rgba(255,255,255,.08)}.card-media-image{max-width:100%;max-height:100%;width:auto;height:auto}.card-media-contain{-o-object-fit:contain;object-fit:contain}.card-media-cover{-o-object-fit:cover;object-fit:cover}.card-price-history{display:flex;flex-direction:column;gap:12px}.history-price-grid{display:grid;grid-template-columns:1fr auto;gap:16px}.history-panel{position:relative;border-radius:16px;border:1px solid rgba(255,255,255,.08);padding:12px;background:var(--surface);box-shadow:inset 0 0 0 1px #ffffff0d}.chart-period-label{margin-top:6px;font-size:.75rem;color:var(--muted);text-align:right}.price-panel{display:flex;flex-direction:column;gap:4px;justify-content:center;min-width:120px}.price-main{font-size:clamp(24px,2.2vw,32px);font-weight:700;text-align:right}.price-msrp{font-size:.85rem;color:var(--muted);text-align:right;text-decoration:line-through}.price-discount{font-size:.85rem;text-align:right}.stock-line{margin-top:6px;text-transform:none;font-weight:500;transition:color .2s ease}.history-summary{display:flex;justify-content:space-between;font-size:.75rem;color:var(--muted)}@media (max-width: 900px){.history-price-grid{grid-template-columns:1fr}}.history-trend{display:flex;gap:6px;align-items:center}.trend-pill{font-size:.85rem;font-weight:600}.trend-delta{font-size:.75rem;font-family:var(--font-mono)}.card-update{font-size:.72rem;color:#ebdbb2b3}.card-media{width:100%;height:var(--pw-card-media-height, 160px);border-radius:16px;border:1px solid rgba(255,255,255,.08);background:var(--surface-2);overflow:hidden;display:flex;align-items:center;justify-content:center}.card-media-image{width:100%;height:100%;-o-object-fit:contain;object-fit:contain}.card-identity{display:flex;align-items:flex-start;gap:12px}.store-icon{width:var(--pw-store-icon);height:var(--pw-store-icon);border-radius:14px;border:1px solid rgba(255,255,255,.08);background:var(--surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:2px}.store-icon img{width:100%;height:100%;-o-object-fit:contain;object-fit:contain}.card-identity-text{flex:1;display:flex;flex-direction:column;gap:4px}.filter-chip{border-radius:999px;padding:4px 10px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);font-size:.75rem;display:inline-flex;align-items:center;gap:4px;cursor:pointer}.filter-chip:hover{border-color:var(--accent)}.card-hover{overflow:hidden;transition:transform .2s ease,box-shadow .2s ease}.card-title{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.card-toolbar{position:absolute;bottom:12px;right:12px;display:flex;align-items:center;gap:6px}.card-toolbar .primary-action{width:36px;height:36px;pointer-events:auto}.secondary-actions{display:flex;gap:4px;opacity:0;transform:translateY(4px);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.group:hover .secondary-actions,.group:focus-within .secondary-actions{opacity:1;transform:translateY(0);pointer-events:auto}.card-hover:hover,.card-hover:focus-within{transform:translateY(-2px);box-shadow:0 18px 32px var(--shadow)}.card-delta{font-size:.75rem;letter-spacing:.5px}.status-pill[data-placeholder],.status-pill.pill{text-transform:none}.card-accent{border:1px solid rgba(254,128,25,.5);box-shadow:0 10px 30px #fe801933}.status-in_stock,.status-in-stock{color:var(--success)}.status-out_of_stock,.status-out-of-stock{color:var(--danger)}.status-unknown{color:var(--muted)}.status-error{color:var(--danger)}.density-dense .card{padding:12px}.density-comfort .card{padding:20px}.section-title{font-family:var(--font-title);letter-spacing:.5px}.label{font-size:.8rem;color:var(--muted)}.detail-dialog{max-height:90vh;border-radius:24px}.detail-modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.08)}.detail-title{font-weight:600;display:inline-block;max-width:22ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.detail-content-area{padding:24px;overflow-y:auto}.detail-columns{min-height:0}.detail-card{background:var(--surface-1);border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:16px;box-shadow:0 12px 32px #00000040;min-height:0}.detail-summary-image{width:100%;height:220px}.detail-summary-image img{width:100%;height:100%}.detail-tabs{display:flex;gap:8px}.detail-tab-button{border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:6px 14px;background:transparent;color:inherit;font-size:.75rem;line-height:1;cursor:pointer;transition:transform .2s ease,border-color .2s ease}.detail-tab-button:hover{border-color:#ffffff29;transform:translateY(-1px)}.detail-tab-button.active{border-color:#ffffff59;box-shadow:0 8px 18px #00000040}.detail-tab-panel{margin-top:4px}.detail-text{white-space:pre-wrap;line-height:1.4}.detail-empty{font-style:italic;opacity:.75}.detail-history-periods{margin-bottom:4px}.detail-period-button{border:1px solid rgba(255,255,255,.08);border-radius:999px;padding:4px 12px;background:transparent;font-size:.7rem;cursor:pointer;transition:box-shadow .2s ease}.detail-period-button.selected{box-shadow:0 10px 20px #00000040;border-color:#ffffff73}.detail-history-summary .section-title{font-size:.85rem}.detail-price-row{display:flex;flex-direction:column;gap:4px}.detail-price-value{font-size:clamp(28px,2.8vw,34px);font-weight:700}.detail-price-updated{font-size:.7rem;color:var(--muted);text-transform:uppercase}.detail-specs span{font-weight:600}.detail-card.edit-card .actions-section{justify-content:flex-start}.header-meta{display:flex;align-items:center;gap:8px}.header-actions{display:flex;align-items:center;gap:6px}.header-actions .icon-btn{width:32px;height:32px}.add-product-btn{width:50px;height:50px;box-shadow:0 16px 32px var(--shadow);transition:transform .2s ease,box-shadow .2s ease}.add-product-btn:hover{transform:translateY(-3px) scale(1.02);box-shadow:0 20px 36px var(--shadow)}.header-actions .icon-btn .fa-solid{font-size:.95rem}.input{width:100%;background:var(--surface-2);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:8px 10px;color:var(--text)}.input:focus{outline:2px solid rgba(254,128,25,.4)}.sidebar{width:280px;min-width:240px}.detail-panel{width:320px;min-width:280px}.image-toggle{border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:2px;background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border .15s ease}.image-toggle:hover{border-color:#fe8019cc;transform:translateY(-1px)}.image-toggle.selected{background:#fe801926;border-color:#fe8019e6;box-shadow:inset 0 0 6px #0000004d}.log-status-panel{border-color:#ffffff1a}.log-entry{transition:background .2s ease}.log-entry-error{border-color:#fb4934b3;background:#fb493412;color:var(--danger)}.detail-popup{border-radius:calc(var(--radius) * 1.2);border-width:1px;max-height:calc(100vh - 60px);box-shadow:0 20px 40px #0009}.view-mode-btn.active-view{background:var(--accent);color:#1b1b1b}.logs-panel{display:flex;flex-direction:column;max-height:min(80vh,560px);gap:.75rem}.logs-content{flex:1;display:flex;flex-direction:column;gap:.5rem;overflow-y:auto;max-height:calc(80vh - 200px)}.logs-toolbar{margin-top:auto;justify-content:flex-end}.scrape-log-bar{position:fixed;bottom:1.5rem;left:50%;transform:translate(-50%);width:min(80%,900px);max-height:140px;background:var(--surface);border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:16px;box-shadow:0 24px 40px #00000073;overflow-y:auto;display:flex;flex-direction:column;gap:6px;z-index:60;font-family:var(--font-mono);font-size:.85rem}.scrape-log-line{display:flex;align-items:center;gap:.8rem;color:var(--text);white-space:nowrap}.scrape-log-time{color:var(--muted);font-size:.75rem}.scrape-log-icon{font-size:.85rem}.scrape-log-text{flex:1;overflow:hidden;text-overflow:ellipsis}.scrape-log-enter-active,.scrape-log-leave-active{transition:opacity .25s ease,transform .25s ease}.scrape-log-enter-from,.scrape-log-leave-to{opacity:0;transform:translateY(12px)}.price-section .price-value{font-size:2.1rem;font-weight:700}.source-section .link{color:var(--accent);text-decoration:underline}.actions-section .icon-btn{background:var(--surface-2)}.app-root.layout-compact .sidebar,.app-root.layout-compact .detail-panel{display:none}.app-root.layout-compact .product-grid{grid-template-columns:1fr}.app-root.layout-wide .sidebar{width:320px}.app-root.layout-wide .detail-panel{width:360px}.compare-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px}.product-grid{display:grid;grid-template-columns:repeat(var(--pw-card-columns, 3),minmax(0,1fr));gap:24px}@media (max-width: 1024px){.sidebar,.detail-panel{display:none}}@media (max-width: 1200px){.product-grid{--pw-card-columns: min(var(--pw-card-columns, 3), 3)}}@media (max-width: 900px){.product-grid{--pw-card-columns: min(var(--pw-card-columns, 3), 2)}}@media (max-width: 640px){.app-header .toolbar-text{display:none}.icon-btn{width:36px;height:36px}.product-grid{grid-template-columns:1fr;--pw-card-columns: 1}.card{min-height:calc(470px * var(--pw-card-mobile-height-factor, 1))}}.view-toggle-group{display:flex;gap:4px}.price-dominant{margin-top:-4px}.price-dominant .price-value{font-size:clamp(24px,3vw,32px);line-height:1.1;font-size:clamp(20px,2.4vw,26px);font-weight:700}.mini-sparkline{margin-top:8px;border-radius:12px;padding:6px;background:var(--surface-2);overflow:hidden}.sparkline-polyline{stroke:var(--text);stroke-width:1.3;stroke-linejoin:round;stroke-linecap:round}.mini-line-chart__point--last{r:3}.history-placeholder{font-size:.75rem;opacity:.6;text-align:center;padding-top:10px}.mini-line-chart-panel{height:160px;overflow:hidden;position:relative;margin-top:12px;width:100%}.mini-line-chart-wrapper{height:100%}.mini-line-chart-wrapper svg{display:block}.price-history-popup{width:280px;border-radius:14px;box-shadow:0 14px 28px #00000059;background:var(--surface);color:var(--text);min-height:120px;max-width:320px}.price-history-popup .sparkline{stroke:currentColor;stroke-width:1.5}.price-history-popup strong{font-weight:700}.price-history-chart{border-radius:14px;box-shadow:0 12px 24px #00000059;background:var(--surface)}.price-history-chart .sparkline{stroke:var(--accent);stroke-width:1.6}@media (min-width: 1024px){.lg\:grid-cols-\[1\.1fr\,0\.9fr\]{grid-template-columns:1.1fr .9fr}}/*! + * Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2024 Fonticons, Inc. + */.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-regular,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-brands:before,.fa-regular:before,.fa-solid:before,.fa:before,.fab:before,.far:before,.fas:before{content:var(--fa)}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{animation-name:fa-beat;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{animation-name:fa-bounce;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{animation-name:fa-fade;animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{animation-name:fa-beat-fade;animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{animation-name:fa-flip;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{animation-name:fa-shake;animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{animation-name:fa-spin;animation-duration:var(--fa-animation-duration,2s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{animation-name:fa-spin;animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{animation-delay:-1ms;animation-duration:1ms;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@keyframes fa-beat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-bounce{0%{transform:scale(1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{transform:scale(1) translateY(0)}to{transform:scale(1) translateY(0)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-flip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-shake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,to{transform:rotate(0)}}@keyframes fa-spin{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.fa-rotate-90{transform:rotate(90deg)}.fa-rotate-180{transform:rotate(180deg)}.fa-rotate-270{transform:rotate(270deg)}.fa-flip-horizontal{transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}.fa-rotate-by{transform:rotate(var(--fa-rotate-angle,0))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-0{--fa:"0"}.fa-1{--fa:"1"}.fa-2{--fa:"2"}.fa-3{--fa:"3"}.fa-4{--fa:"4"}.fa-5{--fa:"5"}.fa-6{--fa:"6"}.fa-7{--fa:"7"}.fa-8{--fa:"8"}.fa-9{--fa:"9"}.fa-fill-drip{--fa:""}.fa-arrows-to-circle{--fa:""}.fa-chevron-circle-right,.fa-circle-chevron-right{--fa:""}.fa-at{--fa:"@"}.fa-trash-alt,.fa-trash-can{--fa:""}.fa-text-height{--fa:""}.fa-user-times,.fa-user-xmark{--fa:""}.fa-stethoscope{--fa:""}.fa-comment-alt,.fa-message{--fa:""}.fa-info{--fa:""}.fa-compress-alt,.fa-down-left-and-up-right-to-center{--fa:""}.fa-explosion{--fa:""}.fa-file-alt,.fa-file-lines,.fa-file-text{--fa:""}.fa-wave-square{--fa:""}.fa-ring{--fa:""}.fa-building-un{--fa:""}.fa-dice-three{--fa:""}.fa-calendar-alt,.fa-calendar-days{--fa:""}.fa-anchor-circle-check{--fa:""}.fa-building-circle-arrow-right{--fa:""}.fa-volleyball,.fa-volleyball-ball{--fa:""}.fa-arrows-up-to-line{--fa:""}.fa-sort-desc,.fa-sort-down{--fa:""}.fa-circle-minus,.fa-minus-circle{--fa:""}.fa-door-open{--fa:""}.fa-right-from-bracket,.fa-sign-out-alt{--fa:""}.fa-atom{--fa:""}.fa-soap{--fa:""}.fa-heart-music-camera-bolt,.fa-icons{--fa:""}.fa-microphone-alt-slash,.fa-microphone-lines-slash{--fa:""}.fa-bridge-circle-check{--fa:""}.fa-pump-medical{--fa:""}.fa-fingerprint{--fa:""}.fa-hand-point-right{--fa:""}.fa-magnifying-glass-location,.fa-search-location{--fa:""}.fa-forward-step,.fa-step-forward{--fa:""}.fa-face-smile-beam,.fa-smile-beam{--fa:""}.fa-flag-checkered{--fa:""}.fa-football,.fa-football-ball{--fa:""}.fa-school-circle-exclamation{--fa:""}.fa-crop{--fa:""}.fa-angle-double-down,.fa-angles-down{--fa:""}.fa-users-rectangle{--fa:""}.fa-people-roof{--fa:""}.fa-people-line{--fa:""}.fa-beer,.fa-beer-mug-empty{--fa:""}.fa-diagram-predecessor{--fa:""}.fa-arrow-up-long,.fa-long-arrow-up{--fa:""}.fa-burn,.fa-fire-flame-simple{--fa:""}.fa-male,.fa-person{--fa:""}.fa-laptop{--fa:""}.fa-file-csv{--fa:""}.fa-menorah{--fa:""}.fa-truck-plane{--fa:""}.fa-record-vinyl{--fa:""}.fa-face-grin-stars,.fa-grin-stars{--fa:""}.fa-bong{--fa:""}.fa-pastafarianism,.fa-spaghetti-monster-flying{--fa:""}.fa-arrow-down-up-across-line{--fa:""}.fa-spoon,.fa-utensil-spoon{--fa:""}.fa-jar-wheat{--fa:""}.fa-envelopes-bulk,.fa-mail-bulk{--fa:""}.fa-file-circle-exclamation{--fa:""}.fa-circle-h,.fa-hospital-symbol{--fa:""}.fa-pager{--fa:""}.fa-address-book,.fa-contact-book{--fa:""}.fa-strikethrough{--fa:""}.fa-k{--fa:"K"}.fa-landmark-flag{--fa:""}.fa-pencil,.fa-pencil-alt{--fa:""}.fa-backward{--fa:""}.fa-caret-right{--fa:""}.fa-comments{--fa:""}.fa-file-clipboard,.fa-paste{--fa:""}.fa-code-pull-request{--fa:""}.fa-clipboard-list{--fa:""}.fa-truck-loading,.fa-truck-ramp-box{--fa:""}.fa-user-check{--fa:""}.fa-vial-virus{--fa:""}.fa-sheet-plastic{--fa:""}.fa-blog{--fa:""}.fa-user-ninja{--fa:""}.fa-person-arrow-up-from-line{--fa:""}.fa-scroll-torah,.fa-torah{--fa:""}.fa-broom-ball,.fa-quidditch,.fa-quidditch-broom-ball{--fa:""}.fa-toggle-off{--fa:""}.fa-archive,.fa-box-archive{--fa:""}.fa-person-drowning{--fa:""}.fa-arrow-down-9-1,.fa-sort-numeric-desc,.fa-sort-numeric-down-alt{--fa:""}.fa-face-grin-tongue-squint,.fa-grin-tongue-squint{--fa:""}.fa-spray-can{--fa:""}.fa-truck-monster{--fa:""}.fa-w{--fa:"W"}.fa-earth-africa,.fa-globe-africa{--fa:""}.fa-rainbow{--fa:""}.fa-circle-notch{--fa:""}.fa-tablet-alt,.fa-tablet-screen-button{--fa:""}.fa-paw{--fa:""}.fa-cloud{--fa:""}.fa-trowel-bricks{--fa:""}.fa-face-flushed,.fa-flushed{--fa:""}.fa-hospital-user{--fa:""}.fa-tent-arrow-left-right{--fa:""}.fa-gavel,.fa-legal{--fa:""}.fa-binoculars{--fa:""}.fa-microphone-slash{--fa:""}.fa-box-tissue{--fa:""}.fa-motorcycle{--fa:""}.fa-bell-concierge,.fa-concierge-bell{--fa:""}.fa-pen-ruler,.fa-pencil-ruler{--fa:""}.fa-people-arrows,.fa-people-arrows-left-right{--fa:""}.fa-mars-and-venus-burst{--fa:""}.fa-caret-square-right,.fa-square-caret-right{--fa:""}.fa-cut,.fa-scissors{--fa:""}.fa-sun-plant-wilt{--fa:""}.fa-toilets-portable{--fa:""}.fa-hockey-puck{--fa:""}.fa-table{--fa:""}.fa-magnifying-glass-arrow-right{--fa:""}.fa-digital-tachograph,.fa-tachograph-digital{--fa:""}.fa-users-slash{--fa:""}.fa-clover{--fa:""}.fa-mail-reply,.fa-reply{--fa:""}.fa-star-and-crescent{--fa:""}.fa-house-fire{--fa:""}.fa-minus-square,.fa-square-minus{--fa:""}.fa-helicopter{--fa:""}.fa-compass{--fa:""}.fa-caret-square-down,.fa-square-caret-down{--fa:""}.fa-file-circle-question{--fa:""}.fa-laptop-code{--fa:""}.fa-swatchbook{--fa:""}.fa-prescription-bottle{--fa:""}.fa-bars,.fa-navicon{--fa:""}.fa-people-group{--fa:""}.fa-hourglass-3,.fa-hourglass-end{--fa:""}.fa-heart-broken,.fa-heart-crack{--fa:""}.fa-external-link-square-alt,.fa-square-up-right{--fa:""}.fa-face-kiss-beam,.fa-kiss-beam{--fa:""}.fa-film{--fa:""}.fa-ruler-horizontal{--fa:""}.fa-people-robbery{--fa:""}.fa-lightbulb{--fa:""}.fa-caret-left{--fa:""}.fa-circle-exclamation,.fa-exclamation-circle{--fa:""}.fa-school-circle-xmark{--fa:""}.fa-arrow-right-from-bracket,.fa-sign-out{--fa:""}.fa-chevron-circle-down,.fa-circle-chevron-down{--fa:""}.fa-unlock-alt,.fa-unlock-keyhole{--fa:""}.fa-cloud-showers-heavy{--fa:""}.fa-headphones-alt,.fa-headphones-simple{--fa:""}.fa-sitemap{--fa:""}.fa-circle-dollar-to-slot,.fa-donate{--fa:""}.fa-memory{--fa:""}.fa-road-spikes{--fa:""}.fa-fire-burner{--fa:""}.fa-flag{--fa:""}.fa-hanukiah{--fa:""}.fa-feather{--fa:""}.fa-volume-down,.fa-volume-low{--fa:""}.fa-comment-slash{--fa:""}.fa-cloud-sun-rain{--fa:""}.fa-compress{--fa:""}.fa-wheat-alt,.fa-wheat-awn{--fa:""}.fa-ankh{--fa:""}.fa-hands-holding-child{--fa:""}.fa-asterisk{--fa:"*"}.fa-check-square,.fa-square-check{--fa:""}.fa-peseta-sign{--fa:""}.fa-header,.fa-heading{--fa:""}.fa-ghost{--fa:""}.fa-list,.fa-list-squares{--fa:""}.fa-phone-square-alt,.fa-square-phone-flip{--fa:""}.fa-cart-plus{--fa:""}.fa-gamepad{--fa:""}.fa-circle-dot,.fa-dot-circle{--fa:""}.fa-dizzy,.fa-face-dizzy{--fa:""}.fa-egg{--fa:""}.fa-house-medical-circle-xmark{--fa:""}.fa-campground{--fa:""}.fa-folder-plus{--fa:""}.fa-futbol,.fa-futbol-ball,.fa-soccer-ball{--fa:""}.fa-paint-brush,.fa-paintbrush{--fa:""}.fa-lock{--fa:""}.fa-gas-pump{--fa:""}.fa-hot-tub,.fa-hot-tub-person{--fa:""}.fa-map-location,.fa-map-marked{--fa:""}.fa-house-flood-water{--fa:""}.fa-tree{--fa:""}.fa-bridge-lock{--fa:""}.fa-sack-dollar{--fa:""}.fa-edit,.fa-pen-to-square{--fa:""}.fa-car-side{--fa:""}.fa-share-alt,.fa-share-nodes{--fa:""}.fa-heart-circle-minus{--fa:""}.fa-hourglass-2,.fa-hourglass-half{--fa:""}.fa-microscope{--fa:""}.fa-sink{--fa:""}.fa-bag-shopping,.fa-shopping-bag{--fa:""}.fa-arrow-down-z-a,.fa-sort-alpha-desc,.fa-sort-alpha-down-alt{--fa:""}.fa-mitten{--fa:""}.fa-person-rays{--fa:""}.fa-users{--fa:""}.fa-eye-slash{--fa:""}.fa-flask-vial{--fa:""}.fa-hand,.fa-hand-paper{--fa:""}.fa-om{--fa:""}.fa-worm{--fa:""}.fa-house-circle-xmark{--fa:""}.fa-plug{--fa:""}.fa-chevron-up{--fa:""}.fa-hand-spock{--fa:""}.fa-stopwatch{--fa:""}.fa-face-kiss,.fa-kiss{--fa:""}.fa-bridge-circle-xmark{--fa:""}.fa-face-grin-tongue,.fa-grin-tongue{--fa:""}.fa-chess-bishop{--fa:""}.fa-face-grin-wink,.fa-grin-wink{--fa:""}.fa-deaf,.fa-deafness,.fa-ear-deaf,.fa-hard-of-hearing{--fa:""}.fa-road-circle-check{--fa:""}.fa-dice-five{--fa:""}.fa-rss-square,.fa-square-rss{--fa:""}.fa-land-mine-on{--fa:""}.fa-i-cursor{--fa:""}.fa-stamp{--fa:""}.fa-stairs{--fa:""}.fa-i{--fa:"I"}.fa-hryvnia,.fa-hryvnia-sign{--fa:""}.fa-pills{--fa:""}.fa-face-grin-wide,.fa-grin-alt{--fa:""}.fa-tooth{--fa:""}.fa-v{--fa:"V"}.fa-bangladeshi-taka-sign{--fa:""}.fa-bicycle{--fa:""}.fa-rod-asclepius,.fa-rod-snake,.fa-staff-aesculapius,.fa-staff-snake{--fa:""}.fa-head-side-cough-slash{--fa:""}.fa-ambulance,.fa-truck-medical{--fa:""}.fa-wheat-awn-circle-exclamation{--fa:""}.fa-snowman{--fa:""}.fa-mortar-pestle{--fa:""}.fa-road-barrier{--fa:""}.fa-school{--fa:""}.fa-igloo{--fa:""}.fa-joint{--fa:""}.fa-angle-right{--fa:""}.fa-horse{--fa:""}.fa-q{--fa:"Q"}.fa-g{--fa:"G"}.fa-notes-medical{--fa:""}.fa-temperature-2,.fa-temperature-half,.fa-thermometer-2,.fa-thermometer-half{--fa:""}.fa-dong-sign{--fa:""}.fa-capsules{--fa:""}.fa-poo-bolt,.fa-poo-storm{--fa:""}.fa-face-frown-open,.fa-frown-open{--fa:""}.fa-hand-point-up{--fa:""}.fa-money-bill{--fa:""}.fa-bookmark{--fa:""}.fa-align-justify{--fa:""}.fa-umbrella-beach{--fa:""}.fa-helmet-un{--fa:""}.fa-bullseye{--fa:""}.fa-bacon{--fa:""}.fa-hand-point-down{--fa:""}.fa-arrow-up-from-bracket{--fa:""}.fa-folder,.fa-folder-blank{--fa:""}.fa-file-medical-alt,.fa-file-waveform{--fa:""}.fa-radiation{--fa:""}.fa-chart-simple{--fa:""}.fa-mars-stroke{--fa:""}.fa-vial{--fa:""}.fa-dashboard,.fa-gauge,.fa-gauge-med,.fa-tachometer-alt-average{--fa:""}.fa-magic-wand-sparkles,.fa-wand-magic-sparkles{--fa:""}.fa-e{--fa:"E"}.fa-pen-alt,.fa-pen-clip{--fa:""}.fa-bridge-circle-exclamation{--fa:""}.fa-user{--fa:""}.fa-school-circle-check{--fa:""}.fa-dumpster{--fa:""}.fa-shuttle-van,.fa-van-shuttle{--fa:""}.fa-building-user{--fa:""}.fa-caret-square-left,.fa-square-caret-left{--fa:""}.fa-highlighter{--fa:""}.fa-key{--fa:""}.fa-bullhorn{--fa:""}.fa-globe{--fa:""}.fa-synagogue{--fa:""}.fa-person-half-dress{--fa:""}.fa-road-bridge{--fa:""}.fa-location-arrow{--fa:""}.fa-c{--fa:"C"}.fa-tablet-button{--fa:""}.fa-building-lock{--fa:""}.fa-pizza-slice{--fa:""}.fa-money-bill-wave{--fa:""}.fa-area-chart,.fa-chart-area{--fa:""}.fa-house-flag{--fa:""}.fa-person-circle-minus{--fa:""}.fa-ban,.fa-cancel{--fa:""}.fa-camera-rotate{--fa:""}.fa-air-freshener,.fa-spray-can-sparkles{--fa:""}.fa-star{--fa:""}.fa-repeat{--fa:""}.fa-cross{--fa:""}.fa-box{--fa:""}.fa-venus-mars{--fa:""}.fa-arrow-pointer,.fa-mouse-pointer{--fa:""}.fa-expand-arrows-alt,.fa-maximize{--fa:""}.fa-charging-station{--fa:""}.fa-shapes,.fa-triangle-circle-square{--fa:""}.fa-random,.fa-shuffle{--fa:""}.fa-person-running,.fa-running{--fa:""}.fa-mobile-retro{--fa:""}.fa-grip-lines-vertical{--fa:""}.fa-spider{--fa:""}.fa-hands-bound{--fa:""}.fa-file-invoice-dollar{--fa:""}.fa-plane-circle-exclamation{--fa:""}.fa-x-ray{--fa:""}.fa-spell-check{--fa:""}.fa-slash{--fa:""}.fa-computer-mouse,.fa-mouse{--fa:""}.fa-arrow-right-to-bracket,.fa-sign-in{--fa:""}.fa-shop-slash,.fa-store-alt-slash{--fa:""}.fa-server{--fa:""}.fa-virus-covid-slash{--fa:""}.fa-shop-lock{--fa:""}.fa-hourglass-1,.fa-hourglass-start{--fa:""}.fa-blender-phone{--fa:""}.fa-building-wheat{--fa:""}.fa-person-breastfeeding{--fa:""}.fa-right-to-bracket,.fa-sign-in-alt{--fa:""}.fa-venus{--fa:""}.fa-passport{--fa:""}.fa-thumb-tack-slash,.fa-thumbtack-slash{--fa:""}.fa-heart-pulse,.fa-heartbeat{--fa:""}.fa-people-carry,.fa-people-carry-box{--fa:""}.fa-temperature-high{--fa:""}.fa-microchip{--fa:""}.fa-crown{--fa:""}.fa-weight-hanging{--fa:""}.fa-xmarks-lines{--fa:""}.fa-file-prescription{--fa:""}.fa-weight,.fa-weight-scale{--fa:""}.fa-user-friends,.fa-user-group{--fa:""}.fa-arrow-up-a-z,.fa-sort-alpha-up{--fa:""}.fa-chess-knight{--fa:""}.fa-face-laugh-squint,.fa-laugh-squint{--fa:""}.fa-wheelchair{--fa:""}.fa-arrow-circle-up,.fa-circle-arrow-up{--fa:""}.fa-toggle-on{--fa:""}.fa-person-walking,.fa-walking{--fa:""}.fa-l{--fa:"L"}.fa-fire{--fa:""}.fa-bed-pulse,.fa-procedures{--fa:""}.fa-shuttle-space,.fa-space-shuttle{--fa:""}.fa-face-laugh,.fa-laugh{--fa:""}.fa-folder-open{--fa:""}.fa-heart-circle-plus{--fa:""}.fa-code-fork{--fa:""}.fa-city{--fa:""}.fa-microphone-alt,.fa-microphone-lines{--fa:""}.fa-pepper-hot{--fa:""}.fa-unlock{--fa:""}.fa-colon-sign{--fa:""}.fa-headset{--fa:""}.fa-store-slash{--fa:""}.fa-road-circle-xmark{--fa:""}.fa-user-minus{--fa:""}.fa-mars-stroke-up,.fa-mars-stroke-v{--fa:""}.fa-champagne-glasses,.fa-glass-cheers{--fa:""}.fa-clipboard{--fa:""}.fa-house-circle-exclamation{--fa:""}.fa-file-arrow-up,.fa-file-upload{--fa:""}.fa-wifi,.fa-wifi-3,.fa-wifi-strong{--fa:""}.fa-bath,.fa-bathtub{--fa:""}.fa-underline{--fa:""}.fa-user-edit,.fa-user-pen{--fa:""}.fa-signature{--fa:""}.fa-stroopwafel{--fa:""}.fa-bold{--fa:""}.fa-anchor-lock{--fa:""}.fa-building-ngo{--fa:""}.fa-manat-sign{--fa:""}.fa-not-equal{--fa:""}.fa-border-style,.fa-border-top-left{--fa:""}.fa-map-location-dot,.fa-map-marked-alt{--fa:""}.fa-jedi{--fa:""}.fa-poll,.fa-square-poll-vertical{--fa:""}.fa-mug-hot{--fa:""}.fa-battery-car,.fa-car-battery{--fa:""}.fa-gift{--fa:""}.fa-dice-two{--fa:""}.fa-chess-queen{--fa:""}.fa-glasses{--fa:""}.fa-chess-board{--fa:""}.fa-building-circle-check{--fa:""}.fa-person-chalkboard{--fa:""}.fa-mars-stroke-h,.fa-mars-stroke-right{--fa:""}.fa-hand-back-fist,.fa-hand-rock{--fa:""}.fa-caret-square-up,.fa-square-caret-up{--fa:""}.fa-cloud-showers-water{--fa:""}.fa-bar-chart,.fa-chart-bar{--fa:""}.fa-hands-bubbles,.fa-hands-wash{--fa:""}.fa-less-than-equal{--fa:""}.fa-train{--fa:""}.fa-eye-low-vision,.fa-low-vision{--fa:""}.fa-crow{--fa:""}.fa-sailboat{--fa:""}.fa-window-restore{--fa:""}.fa-plus-square,.fa-square-plus{--fa:""}.fa-torii-gate{--fa:""}.fa-frog{--fa:""}.fa-bucket{--fa:""}.fa-image{--fa:""}.fa-microphone{--fa:""}.fa-cow{--fa:""}.fa-caret-up{--fa:""}.fa-screwdriver{--fa:""}.fa-folder-closed{--fa:""}.fa-house-tsunami{--fa:""}.fa-square-nfi{--fa:""}.fa-arrow-up-from-ground-water{--fa:""}.fa-glass-martini-alt,.fa-martini-glass{--fa:""}.fa-square-binary{--fa:""}.fa-rotate-back,.fa-rotate-backward,.fa-rotate-left,.fa-undo-alt{--fa:""}.fa-columns,.fa-table-columns{--fa:""}.fa-lemon{--fa:""}.fa-head-side-mask{--fa:""}.fa-handshake{--fa:""}.fa-gem{--fa:""}.fa-dolly,.fa-dolly-box{--fa:""}.fa-smoking{--fa:""}.fa-compress-arrows-alt,.fa-minimize{--fa:""}.fa-monument{--fa:""}.fa-snowplow{--fa:""}.fa-angle-double-right,.fa-angles-right{--fa:""}.fa-cannabis{--fa:""}.fa-circle-play,.fa-play-circle{--fa:""}.fa-tablets{--fa:""}.fa-ethernet{--fa:""}.fa-eur,.fa-euro,.fa-euro-sign{--fa:""}.fa-chair{--fa:""}.fa-check-circle,.fa-circle-check{--fa:""}.fa-circle-stop,.fa-stop-circle{--fa:""}.fa-compass-drafting,.fa-drafting-compass{--fa:""}.fa-plate-wheat{--fa:""}.fa-icicles{--fa:""}.fa-person-shelter{--fa:""}.fa-neuter{--fa:""}.fa-id-badge{--fa:""}.fa-marker{--fa:""}.fa-face-laugh-beam,.fa-laugh-beam{--fa:""}.fa-helicopter-symbol{--fa:""}.fa-universal-access{--fa:""}.fa-chevron-circle-up,.fa-circle-chevron-up{--fa:""}.fa-lari-sign{--fa:""}.fa-volcano{--fa:""}.fa-person-walking-dashed-line-arrow-right{--fa:""}.fa-gbp,.fa-pound-sign,.fa-sterling-sign{--fa:""}.fa-viruses{--fa:""}.fa-square-person-confined{--fa:""}.fa-user-tie{--fa:""}.fa-arrow-down-long,.fa-long-arrow-down{--fa:""}.fa-tent-arrow-down-to-line{--fa:""}.fa-certificate{--fa:""}.fa-mail-reply-all,.fa-reply-all{--fa:""}.fa-suitcase{--fa:""}.fa-person-skating,.fa-skating{--fa:""}.fa-filter-circle-dollar,.fa-funnel-dollar{--fa:""}.fa-camera-retro{--fa:""}.fa-arrow-circle-down,.fa-circle-arrow-down{--fa:""}.fa-arrow-right-to-file,.fa-file-import{--fa:""}.fa-external-link-square,.fa-square-arrow-up-right{--fa:""}.fa-box-open{--fa:""}.fa-scroll{--fa:""}.fa-spa{--fa:""}.fa-location-pin-lock{--fa:""}.fa-pause{--fa:""}.fa-hill-avalanche{--fa:""}.fa-temperature-0,.fa-temperature-empty,.fa-thermometer-0,.fa-thermometer-empty{--fa:""}.fa-bomb{--fa:""}.fa-registered{--fa:""}.fa-address-card,.fa-contact-card,.fa-vcard{--fa:""}.fa-balance-scale-right,.fa-scale-unbalanced-flip{--fa:""}.fa-subscript{--fa:""}.fa-diamond-turn-right,.fa-directions{--fa:""}.fa-burst{--fa:""}.fa-house-laptop,.fa-laptop-house{--fa:""}.fa-face-tired,.fa-tired{--fa:""}.fa-money-bills{--fa:""}.fa-smog{--fa:""}.fa-crutch{--fa:""}.fa-cloud-arrow-up,.fa-cloud-upload,.fa-cloud-upload-alt{--fa:""}.fa-palette{--fa:""}.fa-arrows-turn-right{--fa:""}.fa-vest{--fa:""}.fa-ferry{--fa:""}.fa-arrows-down-to-people{--fa:""}.fa-seedling,.fa-sprout{--fa:""}.fa-arrows-alt-h,.fa-left-right{--fa:""}.fa-boxes-packing{--fa:""}.fa-arrow-circle-left,.fa-circle-arrow-left{--fa:""}.fa-group-arrows-rotate{--fa:""}.fa-bowl-food{--fa:""}.fa-candy-cane{--fa:""}.fa-arrow-down-wide-short,.fa-sort-amount-asc,.fa-sort-amount-down{--fa:""}.fa-cloud-bolt,.fa-thunderstorm{--fa:""}.fa-remove-format,.fa-text-slash{--fa:""}.fa-face-smile-wink,.fa-smile-wink{--fa:""}.fa-file-word{--fa:""}.fa-file-powerpoint{--fa:""}.fa-arrows-h,.fa-arrows-left-right{--fa:""}.fa-house-lock{--fa:""}.fa-cloud-arrow-down,.fa-cloud-download,.fa-cloud-download-alt{--fa:""}.fa-children{--fa:""}.fa-blackboard,.fa-chalkboard{--fa:""}.fa-user-alt-slash,.fa-user-large-slash{--fa:""}.fa-envelope-open{--fa:""}.fa-handshake-alt-slash,.fa-handshake-simple-slash{--fa:""}.fa-mattress-pillow{--fa:""}.fa-guarani-sign{--fa:""}.fa-arrows-rotate,.fa-refresh,.fa-sync{--fa:""}.fa-fire-extinguisher{--fa:""}.fa-cruzeiro-sign{--fa:""}.fa-greater-than-equal{--fa:""}.fa-shield-alt,.fa-shield-halved{--fa:""}.fa-atlas,.fa-book-atlas{--fa:""}.fa-virus{--fa:""}.fa-envelope-circle-check{--fa:""}.fa-layer-group{--fa:""}.fa-arrows-to-dot{--fa:""}.fa-archway{--fa:""}.fa-heart-circle-check{--fa:""}.fa-house-chimney-crack,.fa-house-damage{--fa:""}.fa-file-archive,.fa-file-zipper{--fa:""}.fa-square{--fa:""}.fa-glass-martini,.fa-martini-glass-empty{--fa:""}.fa-couch{--fa:""}.fa-cedi-sign{--fa:""}.fa-italic{--fa:""}.fa-table-cells-column-lock{--fa:""}.fa-church{--fa:""}.fa-comments-dollar{--fa:""}.fa-democrat{--fa:""}.fa-z{--fa:"Z"}.fa-person-skiing,.fa-skiing{--fa:""}.fa-road-lock{--fa:""}.fa-a{--fa:"A"}.fa-temperature-arrow-down,.fa-temperature-down{--fa:""}.fa-feather-alt,.fa-feather-pointed{--fa:""}.fa-p{--fa:"P"}.fa-snowflake{--fa:""}.fa-newspaper{--fa:""}.fa-ad,.fa-rectangle-ad{--fa:""}.fa-arrow-circle-right,.fa-circle-arrow-right{--fa:""}.fa-filter-circle-xmark{--fa:""}.fa-locust{--fa:""}.fa-sort,.fa-unsorted{--fa:""}.fa-list-1-2,.fa-list-numeric,.fa-list-ol{--fa:""}.fa-person-dress-burst{--fa:""}.fa-money-check-alt,.fa-money-check-dollar{--fa:""}.fa-vector-square{--fa:""}.fa-bread-slice{--fa:""}.fa-language{--fa:""}.fa-face-kiss-wink-heart,.fa-kiss-wink-heart{--fa:""}.fa-filter{--fa:""}.fa-question{--fa:"?"}.fa-file-signature{--fa:""}.fa-arrows-alt,.fa-up-down-left-right{--fa:""}.fa-house-chimney-user{--fa:""}.fa-hand-holding-heart{--fa:""}.fa-puzzle-piece{--fa:""}.fa-money-check{--fa:""}.fa-star-half-alt,.fa-star-half-stroke{--fa:""}.fa-code{--fa:""}.fa-glass-whiskey,.fa-whiskey-glass{--fa:""}.fa-building-circle-exclamation{--fa:""}.fa-magnifying-glass-chart{--fa:""}.fa-arrow-up-right-from-square,.fa-external-link{--fa:""}.fa-cubes-stacked{--fa:""}.fa-krw,.fa-won,.fa-won-sign{--fa:""}.fa-virus-covid{--fa:""}.fa-austral-sign{--fa:""}.fa-f{--fa:"F"}.fa-leaf{--fa:""}.fa-road{--fa:""}.fa-cab,.fa-taxi{--fa:""}.fa-person-circle-plus{--fa:""}.fa-chart-pie,.fa-pie-chart{--fa:""}.fa-bolt-lightning{--fa:""}.fa-sack-xmark{--fa:""}.fa-file-excel{--fa:""}.fa-file-contract{--fa:""}.fa-fish-fins{--fa:""}.fa-building-flag{--fa:""}.fa-face-grin-beam,.fa-grin-beam{--fa:""}.fa-object-ungroup{--fa:""}.fa-poop{--fa:""}.fa-location-pin,.fa-map-marker{--fa:""}.fa-kaaba{--fa:""}.fa-toilet-paper{--fa:""}.fa-hard-hat,.fa-hat-hard,.fa-helmet-safety{--fa:""}.fa-eject{--fa:""}.fa-arrow-alt-circle-right,.fa-circle-right{--fa:""}.fa-plane-circle-check{--fa:""}.fa-face-rolling-eyes,.fa-meh-rolling-eyes{--fa:""}.fa-object-group{--fa:""}.fa-chart-line,.fa-line-chart{--fa:""}.fa-mask-ventilator{--fa:""}.fa-arrow-right{--fa:""}.fa-map-signs,.fa-signs-post{--fa:""}.fa-cash-register{--fa:""}.fa-person-circle-question{--fa:""}.fa-h{--fa:"H"}.fa-tarp{--fa:""}.fa-screwdriver-wrench,.fa-tools{--fa:""}.fa-arrows-to-eye{--fa:""}.fa-plug-circle-bolt{--fa:""}.fa-heart{--fa:""}.fa-mars-and-venus{--fa:""}.fa-home-user,.fa-house-user{--fa:""}.fa-dumpster-fire{--fa:""}.fa-house-crack{--fa:""}.fa-cocktail,.fa-martini-glass-citrus{--fa:""}.fa-face-surprise,.fa-surprise{--fa:""}.fa-bottle-water{--fa:""}.fa-circle-pause,.fa-pause-circle{--fa:""}.fa-toilet-paper-slash{--fa:""}.fa-apple-alt,.fa-apple-whole{--fa:""}.fa-kitchen-set{--fa:""}.fa-r{--fa:"R"}.fa-temperature-1,.fa-temperature-quarter,.fa-thermometer-1,.fa-thermometer-quarter{--fa:""}.fa-cube{--fa:""}.fa-bitcoin-sign{--fa:""}.fa-shield-dog{--fa:""}.fa-solar-panel{--fa:""}.fa-lock-open{--fa:""}.fa-elevator{--fa:""}.fa-money-bill-transfer{--fa:""}.fa-money-bill-trend-up{--fa:""}.fa-house-flood-water-circle-arrow-right{--fa:""}.fa-poll-h,.fa-square-poll-horizontal{--fa:""}.fa-circle{--fa:""}.fa-backward-fast,.fa-fast-backward{--fa:""}.fa-recycle{--fa:""}.fa-user-astronaut{--fa:""}.fa-plane-slash{--fa:""}.fa-trademark{--fa:""}.fa-basketball,.fa-basketball-ball{--fa:""}.fa-satellite-dish{--fa:""}.fa-arrow-alt-circle-up,.fa-circle-up{--fa:""}.fa-mobile-alt,.fa-mobile-screen-button{--fa:""}.fa-volume-high,.fa-volume-up{--fa:""}.fa-users-rays{--fa:""}.fa-wallet{--fa:""}.fa-clipboard-check{--fa:""}.fa-file-audio{--fa:""}.fa-burger,.fa-hamburger{--fa:""}.fa-wrench{--fa:""}.fa-bugs{--fa:""}.fa-rupee,.fa-rupee-sign{--fa:""}.fa-file-image{--fa:""}.fa-circle-question,.fa-question-circle{--fa:""}.fa-plane-departure{--fa:""}.fa-handshake-slash{--fa:""}.fa-book-bookmark{--fa:""}.fa-code-branch{--fa:""}.fa-hat-cowboy{--fa:""}.fa-bridge{--fa:""}.fa-phone-alt,.fa-phone-flip{--fa:""}.fa-truck-front{--fa:""}.fa-cat{--fa:""}.fa-anchor-circle-exclamation{--fa:""}.fa-truck-field{--fa:""}.fa-route{--fa:""}.fa-clipboard-question{--fa:""}.fa-panorama{--fa:""}.fa-comment-medical{--fa:""}.fa-teeth-open{--fa:""}.fa-file-circle-minus{--fa:""}.fa-tags{--fa:""}.fa-wine-glass{--fa:""}.fa-fast-forward,.fa-forward-fast{--fa:""}.fa-face-meh-blank,.fa-meh-blank{--fa:""}.fa-parking,.fa-square-parking{--fa:""}.fa-house-signal{--fa:""}.fa-bars-progress,.fa-tasks-alt{--fa:""}.fa-faucet-drip{--fa:""}.fa-cart-flatbed,.fa-dolly-flatbed{--fa:""}.fa-ban-smoking,.fa-smoking-ban{--fa:""}.fa-terminal{--fa:""}.fa-mobile-button{--fa:""}.fa-house-medical-flag{--fa:""}.fa-basket-shopping,.fa-shopping-basket{--fa:""}.fa-tape{--fa:""}.fa-bus-alt,.fa-bus-simple{--fa:""}.fa-eye{--fa:""}.fa-face-sad-cry,.fa-sad-cry{--fa:""}.fa-audio-description{--fa:""}.fa-person-military-to-person{--fa:""}.fa-file-shield{--fa:""}.fa-user-slash{--fa:""}.fa-pen{--fa:""}.fa-tower-observation{--fa:""}.fa-file-code{--fa:""}.fa-signal,.fa-signal-5,.fa-signal-perfect{--fa:""}.fa-bus{--fa:""}.fa-heart-circle-xmark{--fa:""}.fa-home-lg,.fa-house-chimney{--fa:""}.fa-window-maximize{--fa:""}.fa-face-frown,.fa-frown{--fa:""}.fa-prescription{--fa:""}.fa-shop,.fa-store-alt{--fa:""}.fa-floppy-disk,.fa-save{--fa:""}.fa-vihara{--fa:""}.fa-balance-scale-left,.fa-scale-unbalanced{--fa:""}.fa-sort-asc,.fa-sort-up{--fa:""}.fa-comment-dots,.fa-commenting{--fa:""}.fa-plant-wilt{--fa:""}.fa-diamond{--fa:""}.fa-face-grin-squint,.fa-grin-squint{--fa:""}.fa-hand-holding-dollar,.fa-hand-holding-usd{--fa:""}.fa-chart-diagram{--fa:""}.fa-bacterium{--fa:""}.fa-hand-pointer{--fa:""}.fa-drum-steelpan{--fa:""}.fa-hand-scissors{--fa:""}.fa-hands-praying,.fa-praying-hands{--fa:""}.fa-arrow-right-rotate,.fa-arrow-rotate-forward,.fa-arrow-rotate-right,.fa-redo{--fa:""}.fa-biohazard{--fa:""}.fa-location,.fa-location-crosshairs{--fa:""}.fa-mars-double{--fa:""}.fa-child-dress{--fa:""}.fa-users-between-lines{--fa:""}.fa-lungs-virus{--fa:""}.fa-face-grin-tears,.fa-grin-tears{--fa:""}.fa-phone{--fa:""}.fa-calendar-times,.fa-calendar-xmark{--fa:""}.fa-child-reaching{--fa:""}.fa-head-side-virus{--fa:""}.fa-user-cog,.fa-user-gear{--fa:""}.fa-arrow-up-1-9,.fa-sort-numeric-up{--fa:""}.fa-door-closed{--fa:""}.fa-shield-virus{--fa:""}.fa-dice-six{--fa:""}.fa-mosquito-net{--fa:""}.fa-file-fragment{--fa:""}.fa-bridge-water{--fa:""}.fa-person-booth{--fa:""}.fa-text-width{--fa:""}.fa-hat-wizard{--fa:""}.fa-pen-fancy{--fa:""}.fa-digging,.fa-person-digging{--fa:""}.fa-trash{--fa:""}.fa-gauge-simple,.fa-gauge-simple-med,.fa-tachometer-average{--fa:""}.fa-book-medical{--fa:""}.fa-poo{--fa:""}.fa-quote-right,.fa-quote-right-alt{--fa:""}.fa-shirt,.fa-t-shirt,.fa-tshirt{--fa:""}.fa-cubes{--fa:""}.fa-divide{--fa:""}.fa-tenge,.fa-tenge-sign{--fa:""}.fa-headphones{--fa:""}.fa-hands-holding{--fa:""}.fa-hands-clapping{--fa:""}.fa-republican{--fa:""}.fa-arrow-left{--fa:""}.fa-person-circle-xmark{--fa:""}.fa-ruler{--fa:""}.fa-align-left{--fa:""}.fa-dice-d6{--fa:""}.fa-restroom{--fa:""}.fa-j{--fa:"J"}.fa-users-viewfinder{--fa:""}.fa-file-video{--fa:""}.fa-external-link-alt,.fa-up-right-from-square{--fa:""}.fa-table-cells,.fa-th{--fa:""}.fa-file-pdf{--fa:""}.fa-bible,.fa-book-bible{--fa:""}.fa-o{--fa:"O"}.fa-medkit,.fa-suitcase-medical{--fa:""}.fa-user-secret{--fa:""}.fa-otter{--fa:""}.fa-female,.fa-person-dress{--fa:""}.fa-comment-dollar{--fa:""}.fa-briefcase-clock,.fa-business-time{--fa:""}.fa-table-cells-large,.fa-th-large{--fa:""}.fa-book-tanakh,.fa-tanakh{--fa:""}.fa-phone-volume,.fa-volume-control-phone{--fa:""}.fa-hat-cowboy-side{--fa:""}.fa-clipboard-user{--fa:""}.fa-child{--fa:""}.fa-lira-sign{--fa:""}.fa-satellite{--fa:""}.fa-plane-lock{--fa:""}.fa-tag{--fa:""}.fa-comment{--fa:""}.fa-birthday-cake,.fa-cake,.fa-cake-candles{--fa:""}.fa-envelope{--fa:""}.fa-angle-double-up,.fa-angles-up{--fa:""}.fa-paperclip{--fa:""}.fa-arrow-right-to-city{--fa:""}.fa-ribbon{--fa:""}.fa-lungs{--fa:""}.fa-arrow-up-9-1,.fa-sort-numeric-up-alt{--fa:""}.fa-litecoin-sign{--fa:""}.fa-border-none{--fa:""}.fa-circle-nodes{--fa:""}.fa-parachute-box{--fa:""}.fa-indent{--fa:""}.fa-truck-field-un{--fa:""}.fa-hourglass,.fa-hourglass-empty{--fa:""}.fa-mountain{--fa:""}.fa-user-doctor,.fa-user-md{--fa:""}.fa-circle-info,.fa-info-circle{--fa:""}.fa-cloud-meatball{--fa:""}.fa-camera,.fa-camera-alt{--fa:""}.fa-square-virus{--fa:""}.fa-meteor{--fa:""}.fa-car-on{--fa:""}.fa-sleigh{--fa:""}.fa-arrow-down-1-9,.fa-sort-numeric-asc,.fa-sort-numeric-down{--fa:""}.fa-hand-holding-droplet,.fa-hand-holding-water{--fa:""}.fa-water{--fa:""}.fa-calendar-check{--fa:""}.fa-braille{--fa:""}.fa-prescription-bottle-alt,.fa-prescription-bottle-medical{--fa:""}.fa-landmark{--fa:""}.fa-truck{--fa:""}.fa-crosshairs{--fa:""}.fa-person-cane{--fa:""}.fa-tent{--fa:""}.fa-vest-patches{--fa:""}.fa-check-double{--fa:""}.fa-arrow-down-a-z,.fa-sort-alpha-asc,.fa-sort-alpha-down{--fa:""}.fa-money-bill-wheat{--fa:""}.fa-cookie{--fa:""}.fa-arrow-left-rotate,.fa-arrow-rotate-back,.fa-arrow-rotate-backward,.fa-arrow-rotate-left,.fa-undo{--fa:""}.fa-hard-drive,.fa-hdd{--fa:""}.fa-face-grin-squint-tears,.fa-grin-squint-tears{--fa:""}.fa-dumbbell{--fa:""}.fa-list-alt,.fa-rectangle-list{--fa:""}.fa-tarp-droplet{--fa:""}.fa-house-medical-circle-check{--fa:""}.fa-person-skiing-nordic,.fa-skiing-nordic{--fa:""}.fa-calendar-plus{--fa:""}.fa-plane-arrival{--fa:""}.fa-arrow-alt-circle-left,.fa-circle-left{--fa:""}.fa-subway,.fa-train-subway{--fa:""}.fa-chart-gantt{--fa:""}.fa-indian-rupee,.fa-indian-rupee-sign,.fa-inr{--fa:""}.fa-crop-alt,.fa-crop-simple{--fa:""}.fa-money-bill-1,.fa-money-bill-alt{--fa:""}.fa-left-long,.fa-long-arrow-alt-left{--fa:""}.fa-dna{--fa:""}.fa-virus-slash{--fa:""}.fa-minus,.fa-subtract{--fa:""}.fa-chess{--fa:""}.fa-arrow-left-long,.fa-long-arrow-left{--fa:""}.fa-plug-circle-check{--fa:""}.fa-street-view{--fa:""}.fa-franc-sign{--fa:""}.fa-volume-off{--fa:""}.fa-american-sign-language-interpreting,.fa-asl-interpreting,.fa-hands-american-sign-language-interpreting,.fa-hands-asl-interpreting{--fa:""}.fa-cog,.fa-gear{--fa:""}.fa-droplet-slash,.fa-tint-slash{--fa:""}.fa-mosque{--fa:""}.fa-mosquito{--fa:""}.fa-star-of-david{--fa:""}.fa-person-military-rifle{--fa:""}.fa-cart-shopping,.fa-shopping-cart{--fa:""}.fa-vials{--fa:""}.fa-plug-circle-plus{--fa:""}.fa-place-of-worship{--fa:""}.fa-grip-vertical{--fa:""}.fa-hexagon-nodes{--fa:""}.fa-arrow-turn-up,.fa-level-up{--fa:""}.fa-u{--fa:"U"}.fa-square-root-alt,.fa-square-root-variable{--fa:""}.fa-clock,.fa-clock-four{--fa:""}.fa-backward-step,.fa-step-backward{--fa:""}.fa-pallet{--fa:""}.fa-faucet{--fa:""}.fa-baseball-bat-ball{--fa:""}.fa-s{--fa:"S"}.fa-timeline{--fa:""}.fa-keyboard{--fa:""}.fa-caret-down{--fa:""}.fa-clinic-medical,.fa-house-chimney-medical{--fa:""}.fa-temperature-3,.fa-temperature-three-quarters,.fa-thermometer-3,.fa-thermometer-three-quarters{--fa:""}.fa-mobile-android-alt,.fa-mobile-screen{--fa:""}.fa-plane-up{--fa:""}.fa-piggy-bank{--fa:""}.fa-battery-3,.fa-battery-half{--fa:""}.fa-mountain-city{--fa:""}.fa-coins{--fa:""}.fa-khanda{--fa:""}.fa-sliders,.fa-sliders-h{--fa:""}.fa-folder-tree{--fa:""}.fa-network-wired{--fa:""}.fa-map-pin{--fa:""}.fa-hamsa{--fa:""}.fa-cent-sign{--fa:""}.fa-flask{--fa:""}.fa-person-pregnant{--fa:""}.fa-wand-sparkles{--fa:""}.fa-ellipsis-v,.fa-ellipsis-vertical{--fa:""}.fa-ticket{--fa:""}.fa-power-off{--fa:""}.fa-long-arrow-alt-right,.fa-right-long{--fa:""}.fa-flag-usa{--fa:""}.fa-laptop-file{--fa:""}.fa-teletype,.fa-tty{--fa:""}.fa-diagram-next{--fa:""}.fa-person-rifle{--fa:""}.fa-house-medical-circle-exclamation{--fa:""}.fa-closed-captioning{--fa:""}.fa-hiking,.fa-person-hiking{--fa:""}.fa-venus-double{--fa:""}.fa-images{--fa:""}.fa-calculator{--fa:""}.fa-people-pulling{--fa:""}.fa-n{--fa:"N"}.fa-cable-car,.fa-tram{--fa:""}.fa-cloud-rain{--fa:""}.fa-building-circle-xmark{--fa:""}.fa-ship{--fa:""}.fa-arrows-down-to-line{--fa:""}.fa-download{--fa:""}.fa-face-grin,.fa-grin{--fa:""}.fa-backspace,.fa-delete-left{--fa:""}.fa-eye-dropper,.fa-eye-dropper-empty,.fa-eyedropper{--fa:""}.fa-file-circle-check{--fa:""}.fa-forward{--fa:""}.fa-mobile,.fa-mobile-android,.fa-mobile-phone{--fa:""}.fa-face-meh,.fa-meh{--fa:""}.fa-align-center{--fa:""}.fa-book-dead,.fa-book-skull{--fa:""}.fa-drivers-license,.fa-id-card{--fa:""}.fa-dedent,.fa-outdent{--fa:""}.fa-heart-circle-exclamation{--fa:""}.fa-home,.fa-home-alt,.fa-home-lg-alt,.fa-house{--fa:""}.fa-calendar-week{--fa:""}.fa-laptop-medical{--fa:""}.fa-b{--fa:"B"}.fa-file-medical{--fa:""}.fa-dice-one{--fa:""}.fa-kiwi-bird{--fa:""}.fa-arrow-right-arrow-left,.fa-exchange{--fa:""}.fa-redo-alt,.fa-rotate-forward,.fa-rotate-right{--fa:""}.fa-cutlery,.fa-utensils{--fa:""}.fa-arrow-up-wide-short,.fa-sort-amount-up{--fa:""}.fa-mill-sign{--fa:""}.fa-bowl-rice{--fa:""}.fa-skull{--fa:""}.fa-broadcast-tower,.fa-tower-broadcast{--fa:""}.fa-truck-pickup{--fa:""}.fa-long-arrow-alt-up,.fa-up-long{--fa:""}.fa-stop{--fa:""}.fa-code-merge{--fa:""}.fa-upload{--fa:""}.fa-hurricane{--fa:""}.fa-mound{--fa:""}.fa-toilet-portable{--fa:""}.fa-compact-disc{--fa:""}.fa-file-arrow-down,.fa-file-download{--fa:""}.fa-caravan{--fa:""}.fa-shield-cat{--fa:""}.fa-bolt,.fa-zap{--fa:""}.fa-glass-water{--fa:""}.fa-oil-well{--fa:""}.fa-vault{--fa:""}.fa-mars{--fa:""}.fa-toilet{--fa:""}.fa-plane-circle-xmark{--fa:""}.fa-cny,.fa-jpy,.fa-rmb,.fa-yen,.fa-yen-sign{--fa:""}.fa-rouble,.fa-rub,.fa-ruble,.fa-ruble-sign{--fa:""}.fa-sun{--fa:""}.fa-guitar{--fa:""}.fa-face-laugh-wink,.fa-laugh-wink{--fa:""}.fa-horse-head{--fa:""}.fa-bore-hole{--fa:""}.fa-industry{--fa:""}.fa-arrow-alt-circle-down,.fa-circle-down{--fa:""}.fa-arrows-turn-to-dots{--fa:""}.fa-florin-sign{--fa:""}.fa-arrow-down-short-wide,.fa-sort-amount-desc,.fa-sort-amount-down-alt{--fa:""}.fa-less-than{--fa:"<"}.fa-angle-down{--fa:""}.fa-car-tunnel{--fa:""}.fa-head-side-cough{--fa:""}.fa-grip-lines{--fa:""}.fa-thumbs-down{--fa:""}.fa-user-lock{--fa:""}.fa-arrow-right-long,.fa-long-arrow-right{--fa:""}.fa-anchor-circle-xmark{--fa:""}.fa-ellipsis,.fa-ellipsis-h{--fa:""}.fa-chess-pawn{--fa:""}.fa-first-aid,.fa-kit-medical{--fa:""}.fa-person-through-window{--fa:""}.fa-toolbox{--fa:""}.fa-hands-holding-circle{--fa:""}.fa-bug{--fa:""}.fa-credit-card,.fa-credit-card-alt{--fa:""}.fa-automobile,.fa-car{--fa:""}.fa-hand-holding-hand{--fa:""}.fa-book-open-reader,.fa-book-reader{--fa:""}.fa-mountain-sun{--fa:""}.fa-arrows-left-right-to-line{--fa:""}.fa-dice-d20{--fa:""}.fa-truck-droplet{--fa:""}.fa-file-circle-xmark{--fa:""}.fa-temperature-arrow-up,.fa-temperature-up{--fa:""}.fa-medal{--fa:""}.fa-bed{--fa:""}.fa-h-square,.fa-square-h{--fa:""}.fa-podcast{--fa:""}.fa-temperature-4,.fa-temperature-full,.fa-thermometer-4,.fa-thermometer-full{--fa:""}.fa-bell{--fa:""}.fa-superscript{--fa:""}.fa-plug-circle-xmark{--fa:""}.fa-star-of-life{--fa:""}.fa-phone-slash{--fa:""}.fa-paint-roller{--fa:""}.fa-hands-helping,.fa-handshake-angle{--fa:""}.fa-location-dot,.fa-map-marker-alt{--fa:""}.fa-file{--fa:""}.fa-greater-than{--fa:">"}.fa-person-swimming,.fa-swimmer{--fa:""}.fa-arrow-down{--fa:""}.fa-droplet,.fa-tint{--fa:""}.fa-eraser{--fa:""}.fa-earth,.fa-earth-america,.fa-earth-americas,.fa-globe-americas{--fa:""}.fa-person-burst{--fa:""}.fa-dove{--fa:""}.fa-battery-0,.fa-battery-empty{--fa:""}.fa-socks{--fa:""}.fa-inbox{--fa:""}.fa-section{--fa:""}.fa-gauge-high,.fa-tachometer-alt,.fa-tachometer-alt-fast{--fa:""}.fa-envelope-open-text{--fa:""}.fa-hospital,.fa-hospital-alt,.fa-hospital-wide{--fa:""}.fa-wine-bottle{--fa:""}.fa-chess-rook{--fa:""}.fa-bars-staggered,.fa-reorder,.fa-stream{--fa:""}.fa-dharmachakra{--fa:""}.fa-hotdog{--fa:""}.fa-blind,.fa-person-walking-with-cane{--fa:""}.fa-drum{--fa:""}.fa-ice-cream{--fa:""}.fa-heart-circle-bolt{--fa:""}.fa-fax{--fa:""}.fa-paragraph{--fa:""}.fa-check-to-slot,.fa-vote-yea{--fa:""}.fa-star-half{--fa:""}.fa-boxes,.fa-boxes-alt,.fa-boxes-stacked{--fa:""}.fa-chain,.fa-link{--fa:""}.fa-assistive-listening-systems,.fa-ear-listen{--fa:""}.fa-tree-city{--fa:""}.fa-play{--fa:""}.fa-font{--fa:""}.fa-table-cells-row-lock{--fa:""}.fa-rupiah-sign{--fa:""}.fa-magnifying-glass,.fa-search{--fa:""}.fa-ping-pong-paddle-ball,.fa-table-tennis,.fa-table-tennis-paddle-ball{--fa:""}.fa-diagnoses,.fa-person-dots-from-line{--fa:""}.fa-trash-can-arrow-up,.fa-trash-restore-alt{--fa:""}.fa-naira-sign{--fa:""}.fa-cart-arrow-down{--fa:""}.fa-walkie-talkie{--fa:""}.fa-file-edit,.fa-file-pen{--fa:""}.fa-receipt{--fa:""}.fa-pen-square,.fa-pencil-square,.fa-square-pen{--fa:""}.fa-suitcase-rolling{--fa:""}.fa-person-circle-exclamation{--fa:""}.fa-chevron-down{--fa:""}.fa-battery,.fa-battery-5,.fa-battery-full{--fa:""}.fa-skull-crossbones{--fa:""}.fa-code-compare{--fa:""}.fa-list-dots,.fa-list-ul{--fa:""}.fa-school-lock{--fa:""}.fa-tower-cell{--fa:""}.fa-down-long,.fa-long-arrow-alt-down{--fa:""}.fa-ranking-star{--fa:""}.fa-chess-king{--fa:""}.fa-person-harassing{--fa:""}.fa-brazilian-real-sign{--fa:""}.fa-landmark-alt,.fa-landmark-dome{--fa:""}.fa-arrow-up{--fa:""}.fa-television,.fa-tv,.fa-tv-alt{--fa:""}.fa-shrimp{--fa:""}.fa-list-check,.fa-tasks{--fa:""}.fa-jug-detergent{--fa:""}.fa-circle-user,.fa-user-circle{--fa:""}.fa-user-shield{--fa:""}.fa-wind{--fa:""}.fa-car-burst,.fa-car-crash{--fa:""}.fa-y{--fa:"Y"}.fa-person-snowboarding,.fa-snowboarding{--fa:""}.fa-shipping-fast,.fa-truck-fast{--fa:""}.fa-fish{--fa:""}.fa-user-graduate{--fa:""}.fa-adjust,.fa-circle-half-stroke{--fa:""}.fa-clapperboard{--fa:""}.fa-circle-radiation,.fa-radiation-alt{--fa:""}.fa-baseball,.fa-baseball-ball{--fa:""}.fa-jet-fighter-up{--fa:""}.fa-diagram-project,.fa-project-diagram{--fa:""}.fa-copy{--fa:""}.fa-volume-mute,.fa-volume-times,.fa-volume-xmark{--fa:""}.fa-hand-sparkles{--fa:""}.fa-grip,.fa-grip-horizontal{--fa:""}.fa-share-from-square,.fa-share-square{--fa:""}.fa-child-combatant,.fa-child-rifle{--fa:""}.fa-gun{--fa:""}.fa-phone-square,.fa-square-phone{--fa:""}.fa-add,.fa-plus{--fa:"+"}.fa-expand{--fa:""}.fa-computer{--fa:""}.fa-close,.fa-multiply,.fa-remove,.fa-times,.fa-xmark{--fa:""}.fa-arrows,.fa-arrows-up-down-left-right{--fa:""}.fa-chalkboard-teacher,.fa-chalkboard-user{--fa:""}.fa-peso-sign{--fa:""}.fa-building-shield{--fa:""}.fa-baby{--fa:""}.fa-users-line{--fa:""}.fa-quote-left,.fa-quote-left-alt{--fa:""}.fa-tractor{--fa:""}.fa-trash-arrow-up,.fa-trash-restore{--fa:""}.fa-arrow-down-up-lock{--fa:""}.fa-lines-leaning{--fa:""}.fa-ruler-combined{--fa:""}.fa-copyright{--fa:""}.fa-equals{--fa:"="}.fa-blender{--fa:""}.fa-teeth{--fa:""}.fa-ils,.fa-shekel,.fa-shekel-sign,.fa-sheqel,.fa-sheqel-sign{--fa:""}.fa-map{--fa:""}.fa-rocket{--fa:""}.fa-photo-film,.fa-photo-video{--fa:""}.fa-folder-minus{--fa:""}.fa-hexagon-nodes-bolt{--fa:""}.fa-store{--fa:""}.fa-arrow-trend-up{--fa:""}.fa-plug-circle-minus{--fa:""}.fa-sign,.fa-sign-hanging{--fa:""}.fa-bezier-curve{--fa:""}.fa-bell-slash{--fa:""}.fa-tablet,.fa-tablet-android{--fa:""}.fa-school-flag{--fa:""}.fa-fill{--fa:""}.fa-angle-up{--fa:""}.fa-drumstick-bite{--fa:""}.fa-holly-berry{--fa:""}.fa-chevron-left{--fa:""}.fa-bacteria{--fa:""}.fa-hand-lizard{--fa:""}.fa-notdef{--fa:""}.fa-disease{--fa:""}.fa-briefcase-medical{--fa:""}.fa-genderless{--fa:""}.fa-chevron-right{--fa:""}.fa-retweet{--fa:""}.fa-car-alt,.fa-car-rear{--fa:""}.fa-pump-soap{--fa:""}.fa-video-slash{--fa:""}.fa-battery-2,.fa-battery-quarter{--fa:""}.fa-radio{--fa:""}.fa-baby-carriage,.fa-carriage-baby{--fa:""}.fa-traffic-light{--fa:""}.fa-thermometer{--fa:""}.fa-vr-cardboard{--fa:""}.fa-hand-middle-finger{--fa:""}.fa-percent,.fa-percentage{--fa:"%"}.fa-truck-moving{--fa:""}.fa-glass-water-droplet{--fa:""}.fa-display{--fa:""}.fa-face-smile,.fa-smile{--fa:""}.fa-thumb-tack,.fa-thumbtack{--fa:""}.fa-trophy{--fa:""}.fa-person-praying,.fa-pray{--fa:""}.fa-hammer{--fa:""}.fa-hand-peace{--fa:""}.fa-rotate,.fa-sync-alt{--fa:""}.fa-spinner{--fa:""}.fa-robot{--fa:""}.fa-peace{--fa:""}.fa-cogs,.fa-gears{--fa:""}.fa-warehouse{--fa:""}.fa-arrow-up-right-dots{--fa:""}.fa-splotch{--fa:""}.fa-face-grin-hearts,.fa-grin-hearts{--fa:""}.fa-dice-four{--fa:""}.fa-sim-card{--fa:""}.fa-transgender,.fa-transgender-alt{--fa:""}.fa-mercury{--fa:""}.fa-arrow-turn-down,.fa-level-down{--fa:""}.fa-person-falling-burst{--fa:""}.fa-award{--fa:""}.fa-ticket-alt,.fa-ticket-simple{--fa:""}.fa-building{--fa:""}.fa-angle-double-left,.fa-angles-left{--fa:""}.fa-qrcode{--fa:""}.fa-clock-rotate-left,.fa-history{--fa:""}.fa-face-grin-beam-sweat,.fa-grin-beam-sweat{--fa:""}.fa-arrow-right-from-file,.fa-file-export{--fa:""}.fa-shield,.fa-shield-blank{--fa:""}.fa-arrow-up-short-wide,.fa-sort-amount-up-alt{--fa:""}.fa-comment-nodes{--fa:""}.fa-house-medical{--fa:""}.fa-golf-ball,.fa-golf-ball-tee{--fa:""}.fa-chevron-circle-left,.fa-circle-chevron-left{--fa:""}.fa-house-chimney-window{--fa:""}.fa-pen-nib{--fa:""}.fa-tent-arrow-turn-left{--fa:""}.fa-tents{--fa:""}.fa-magic,.fa-wand-magic{--fa:""}.fa-dog{--fa:""}.fa-carrot{--fa:""}.fa-moon{--fa:""}.fa-wine-glass-alt,.fa-wine-glass-empty{--fa:""}.fa-cheese{--fa:""}.fa-yin-yang{--fa:""}.fa-music{--fa:""}.fa-code-commit{--fa:""}.fa-temperature-low{--fa:""}.fa-biking,.fa-person-biking{--fa:""}.fa-broom{--fa:""}.fa-shield-heart{--fa:""}.fa-gopuram{--fa:""}.fa-earth-oceania,.fa-globe-oceania{--fa:""}.fa-square-xmark,.fa-times-square,.fa-xmark-square{--fa:""}.fa-hashtag{--fa:"#"}.fa-expand-alt,.fa-up-right-and-down-left-from-center{--fa:""}.fa-oil-can{--fa:""}.fa-t{--fa:"T"}.fa-hippo{--fa:""}.fa-chart-column{--fa:""}.fa-infinity{--fa:""}.fa-vial-circle-check{--fa:""}.fa-person-arrow-down-to-line{--fa:""}.fa-voicemail{--fa:""}.fa-fan{--fa:""}.fa-person-walking-luggage{--fa:""}.fa-arrows-alt-v,.fa-up-down{--fa:""}.fa-cloud-moon-rain{--fa:""}.fa-calendar{--fa:""}.fa-trailer{--fa:""}.fa-bahai,.fa-haykal{--fa:""}.fa-sd-card{--fa:""}.fa-dragon{--fa:""}.fa-shoe-prints{--fa:""}.fa-circle-plus,.fa-plus-circle{--fa:""}.fa-face-grin-tongue-wink,.fa-grin-tongue-wink{--fa:""}.fa-hand-holding{--fa:""}.fa-plug-circle-exclamation{--fa:""}.fa-chain-broken,.fa-chain-slash,.fa-link-slash,.fa-unlink{--fa:""}.fa-clone{--fa:""}.fa-person-walking-arrow-loop-left{--fa:""}.fa-arrow-up-z-a,.fa-sort-alpha-up-alt{--fa:""}.fa-fire-alt,.fa-fire-flame-curved{--fa:""}.fa-tornado{--fa:""}.fa-file-circle-plus{--fa:""}.fa-book-quran,.fa-quran{--fa:""}.fa-anchor{--fa:""}.fa-border-all{--fa:""}.fa-angry,.fa-face-angry{--fa:""}.fa-cookie-bite{--fa:""}.fa-arrow-trend-down{--fa:""}.fa-feed,.fa-rss{--fa:""}.fa-draw-polygon{--fa:""}.fa-balance-scale,.fa-scale-balanced{--fa:""}.fa-gauge-simple-high,.fa-tachometer,.fa-tachometer-fast{--fa:""}.fa-shower{--fa:""}.fa-desktop,.fa-desktop-alt{--fa:""}.fa-m{--fa:"M"}.fa-table-list,.fa-th-list{--fa:""}.fa-comment-sms,.fa-sms{--fa:""}.fa-book{--fa:""}.fa-user-plus{--fa:""}.fa-check{--fa:""}.fa-battery-4,.fa-battery-three-quarters{--fa:""}.fa-house-circle-check{--fa:""}.fa-angle-left{--fa:""}.fa-diagram-successor{--fa:""}.fa-truck-arrow-right{--fa:""}.fa-arrows-split-up-and-left{--fa:""}.fa-fist-raised,.fa-hand-fist{--fa:""}.fa-cloud-moon{--fa:""}.fa-briefcase{--fa:""}.fa-person-falling{--fa:""}.fa-image-portrait,.fa-portrait{--fa:""}.fa-user-tag{--fa:""}.fa-rug{--fa:""}.fa-earth-europe,.fa-globe-europe{--fa:""}.fa-cart-flatbed-suitcase,.fa-luggage-cart{--fa:""}.fa-rectangle-times,.fa-rectangle-xmark,.fa-times-rectangle,.fa-window-close{--fa:""}.fa-baht-sign{--fa:""}.fa-book-open{--fa:""}.fa-book-journal-whills,.fa-journal-whills{--fa:""}.fa-handcuffs{--fa:""}.fa-exclamation-triangle,.fa-triangle-exclamation,.fa-warning{--fa:""}.fa-database{--fa:""}.fa-mail-forward,.fa-share{--fa:""}.fa-bottle-droplet{--fa:""}.fa-mask-face{--fa:""}.fa-hill-rockslide{--fa:""}.fa-exchange-alt,.fa-right-left{--fa:""}.fa-paper-plane{--fa:""}.fa-road-circle-exclamation{--fa:""}.fa-dungeon{--fa:""}.fa-align-right{--fa:""}.fa-money-bill-1-wave,.fa-money-bill-wave-alt{--fa:""}.fa-life-ring{--fa:""}.fa-hands,.fa-sign-language,.fa-signing{--fa:""}.fa-calendar-day{--fa:""}.fa-ladder-water,.fa-swimming-pool,.fa-water-ladder{--fa:""}.fa-arrows-up-down,.fa-arrows-v{--fa:""}.fa-face-grimace,.fa-grimace{--fa:""}.fa-wheelchair-alt,.fa-wheelchair-move{--fa:""}.fa-level-down-alt,.fa-turn-down{--fa:""}.fa-person-walking-arrow-right{--fa:""}.fa-envelope-square,.fa-square-envelope{--fa:""}.fa-dice{--fa:""}.fa-bowling-ball{--fa:""}.fa-brain{--fa:""}.fa-band-aid,.fa-bandage{--fa:""}.fa-calendar-minus{--fa:""}.fa-circle-xmark,.fa-times-circle,.fa-xmark-circle{--fa:""}.fa-gifts{--fa:""}.fa-hotel{--fa:""}.fa-earth-asia,.fa-globe-asia{--fa:""}.fa-id-card-alt,.fa-id-card-clip{--fa:""}.fa-magnifying-glass-plus,.fa-search-plus{--fa:""}.fa-thumbs-up{--fa:""}.fa-user-clock{--fa:""}.fa-allergies,.fa-hand-dots{--fa:""}.fa-file-invoice{--fa:""}.fa-window-minimize{--fa:""}.fa-coffee,.fa-mug-saucer{--fa:""}.fa-brush{--fa:""}.fa-file-half-dashed{--fa:""}.fa-mask{--fa:""}.fa-magnifying-glass-minus,.fa-search-minus{--fa:""}.fa-ruler-vertical{--fa:""}.fa-user-alt,.fa-user-large{--fa:""}.fa-train-tram{--fa:""}.fa-user-nurse{--fa:""}.fa-syringe{--fa:""}.fa-cloud-sun{--fa:""}.fa-stopwatch-20{--fa:""}.fa-square-full{--fa:""}.fa-magnet{--fa:""}.fa-jar{--fa:""}.fa-note-sticky,.fa-sticky-note{--fa:""}.fa-bug-slash{--fa:""}.fa-arrow-up-from-water-pump{--fa:""}.fa-bone{--fa:""}.fa-table-cells-row-unlock{--fa:""}.fa-user-injured{--fa:""}.fa-face-sad-tear,.fa-sad-tear{--fa:""}.fa-plane{--fa:""}.fa-tent-arrows-down{--fa:""}.fa-exclamation{--fa:"!"}.fa-arrows-spin{--fa:""}.fa-print{--fa:""}.fa-try,.fa-turkish-lira,.fa-turkish-lira-sign{--fa:""}.fa-dollar,.fa-dollar-sign,.fa-usd{--fa:"$"}.fa-x{--fa:"X"}.fa-magnifying-glass-dollar,.fa-search-dollar{--fa:""}.fa-users-cog,.fa-users-gear{--fa:""}.fa-person-military-pointing{--fa:""}.fa-bank,.fa-building-columns,.fa-institution,.fa-museum,.fa-university{--fa:""}.fa-umbrella{--fa:""}.fa-trowel{--fa:""}.fa-d{--fa:"D"}.fa-stapler{--fa:""}.fa-masks-theater,.fa-theater-masks{--fa:""}.fa-kip-sign{--fa:""}.fa-hand-point-left{--fa:""}.fa-handshake-alt,.fa-handshake-simple{--fa:""}.fa-fighter-jet,.fa-jet-fighter{--fa:""}.fa-share-alt-square,.fa-square-share-nodes{--fa:""}.fa-barcode{--fa:""}.fa-plus-minus{--fa:""}.fa-video,.fa-video-camera{--fa:""}.fa-graduation-cap,.fa-mortar-board{--fa:""}.fa-hand-holding-medical{--fa:""}.fa-person-circle-check{--fa:""}.fa-level-up-alt,.fa-turn-up{--fa:""}.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(/assets/fa-brands-400-D_cYUPeE.woff2) format("woff2"),url(/assets/fa-brands-400-D1LuMI3I.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero{--fa:""}.fa-hooli{--fa:""}.fa-yelp{--fa:""}.fa-cc-visa{--fa:""}.fa-lastfm{--fa:""}.fa-shopware{--fa:""}.fa-creative-commons-nc{--fa:""}.fa-aws{--fa:""}.fa-redhat{--fa:""}.fa-yoast{--fa:""}.fa-cloudflare{--fa:""}.fa-ups{--fa:""}.fa-pixiv{--fa:""}.fa-wpexplorer{--fa:""}.fa-dyalog{--fa:""}.fa-bity{--fa:""}.fa-stackpath{--fa:""}.fa-buysellads{--fa:""}.fa-first-order{--fa:""}.fa-modx{--fa:""}.fa-guilded{--fa:""}.fa-vnv{--fa:""}.fa-js-square,.fa-square-js{--fa:""}.fa-microsoft{--fa:""}.fa-qq{--fa:""}.fa-orcid{--fa:""}.fa-java{--fa:""}.fa-invision{--fa:""}.fa-creative-commons-pd-alt{--fa:""}.fa-centercode{--fa:""}.fa-glide-g{--fa:""}.fa-drupal{--fa:""}.fa-jxl{--fa:""}.fa-dart-lang{--fa:""}.fa-hire-a-helper{--fa:""}.fa-creative-commons-by{--fa:""}.fa-unity{--fa:""}.fa-whmcs{--fa:""}.fa-rocketchat{--fa:""}.fa-vk{--fa:""}.fa-untappd{--fa:""}.fa-mailchimp{--fa:""}.fa-css3-alt{--fa:""}.fa-reddit-square,.fa-square-reddit{--fa:""}.fa-vimeo-v{--fa:""}.fa-contao{--fa:""}.fa-square-font-awesome{--fa:""}.fa-deskpro{--fa:""}.fa-brave{--fa:""}.fa-sistrix{--fa:""}.fa-instagram-square,.fa-square-instagram{--fa:""}.fa-battle-net{--fa:""}.fa-the-red-yeti{--fa:""}.fa-hacker-news-square,.fa-square-hacker-news{--fa:""}.fa-edge{--fa:""}.fa-threads{--fa:""}.fa-napster{--fa:""}.fa-snapchat-square,.fa-square-snapchat{--fa:""}.fa-google-plus-g{--fa:""}.fa-artstation{--fa:""}.fa-markdown{--fa:""}.fa-sourcetree{--fa:""}.fa-google-plus{--fa:""}.fa-diaspora{--fa:""}.fa-foursquare{--fa:""}.fa-stack-overflow{--fa:""}.fa-github-alt{--fa:""}.fa-phoenix-squadron{--fa:""}.fa-pagelines{--fa:""}.fa-algolia{--fa:""}.fa-red-river{--fa:""}.fa-creative-commons-sa{--fa:""}.fa-safari{--fa:""}.fa-google{--fa:""}.fa-font-awesome-alt,.fa-square-font-awesome-stroke{--fa:""}.fa-atlassian{--fa:""}.fa-linkedin-in{--fa:""}.fa-digital-ocean{--fa:""}.fa-nimblr{--fa:""}.fa-chromecast{--fa:""}.fa-evernote{--fa:""}.fa-hacker-news{--fa:""}.fa-creative-commons-sampling{--fa:""}.fa-adversal{--fa:""}.fa-creative-commons{--fa:""}.fa-watchman-monitoring{--fa:""}.fa-fonticons{--fa:""}.fa-weixin{--fa:""}.fa-shirtsinbulk{--fa:""}.fa-codepen{--fa:""}.fa-git-alt{--fa:""}.fa-lyft{--fa:""}.fa-rev{--fa:""}.fa-windows{--fa:""}.fa-wizards-of-the-coast{--fa:""}.fa-square-viadeo,.fa-viadeo-square{--fa:""}.fa-meetup{--fa:""}.fa-centos{--fa:""}.fa-adn{--fa:""}.fa-cloudsmith{--fa:""}.fa-opensuse{--fa:""}.fa-pied-piper-alt{--fa:""}.fa-dribbble-square,.fa-square-dribbble{--fa:""}.fa-codiepie{--fa:""}.fa-node{--fa:""}.fa-mix{--fa:""}.fa-steam{--fa:""}.fa-cc-apple-pay{--fa:""}.fa-scribd{--fa:""}.fa-debian{--fa:""}.fa-openid{--fa:""}.fa-instalod{--fa:""}.fa-files-pinwheel{--fa:""}.fa-expeditedssl{--fa:""}.fa-sellcast{--fa:""}.fa-square-twitter,.fa-twitter-square{--fa:""}.fa-r-project{--fa:""}.fa-delicious{--fa:""}.fa-freebsd{--fa:""}.fa-vuejs{--fa:""}.fa-accusoft{--fa:""}.fa-ioxhost{--fa:""}.fa-fonticons-fi{--fa:""}.fa-app-store{--fa:""}.fa-cc-mastercard{--fa:""}.fa-itunes-note{--fa:""}.fa-golang{--fa:""}.fa-kickstarter,.fa-square-kickstarter{--fa:""}.fa-grav{--fa:""}.fa-weibo{--fa:""}.fa-uncharted{--fa:""}.fa-firstdraft{--fa:""}.fa-square-youtube,.fa-youtube-square{--fa:""}.fa-wikipedia-w{--fa:""}.fa-rendact,.fa-wpressr{--fa:""}.fa-angellist{--fa:""}.fa-galactic-republic{--fa:""}.fa-nfc-directional{--fa:""}.fa-skype{--fa:""}.fa-joget{--fa:""}.fa-fedora{--fa:""}.fa-stripe-s{--fa:""}.fa-meta{--fa:""}.fa-laravel{--fa:""}.fa-hotjar{--fa:""}.fa-bluetooth-b{--fa:""}.fa-square-letterboxd{--fa:""}.fa-sticker-mule{--fa:""}.fa-creative-commons-zero{--fa:""}.fa-hips{--fa:""}.fa-css{--fa:""}.fa-behance{--fa:""}.fa-reddit{--fa:""}.fa-discord{--fa:""}.fa-chrome{--fa:""}.fa-app-store-ios{--fa:""}.fa-cc-discover{--fa:""}.fa-wpbeginner{--fa:""}.fa-confluence{--fa:""}.fa-shoelace{--fa:""}.fa-mdb{--fa:""}.fa-dochub{--fa:""}.fa-accessible-icon{--fa:""}.fa-ebay{--fa:""}.fa-amazon{--fa:""}.fa-unsplash{--fa:""}.fa-yarn{--fa:""}.fa-square-steam,.fa-steam-square{--fa:""}.fa-500px{--fa:""}.fa-square-vimeo,.fa-vimeo-square{--fa:""}.fa-asymmetrik{--fa:""}.fa-font-awesome,.fa-font-awesome-flag,.fa-font-awesome-logo-full{--fa:""}.fa-gratipay{--fa:""}.fa-apple{--fa:""}.fa-hive{--fa:""}.fa-gitkraken{--fa:""}.fa-keybase{--fa:""}.fa-apple-pay{--fa:""}.fa-padlet{--fa:""}.fa-amazon-pay{--fa:""}.fa-github-square,.fa-square-github{--fa:""}.fa-stumbleupon{--fa:""}.fa-fedex{--fa:""}.fa-phoenix-framework{--fa:""}.fa-shopify{--fa:""}.fa-neos{--fa:""}.fa-square-threads{--fa:""}.fa-hackerrank{--fa:""}.fa-researchgate{--fa:""}.fa-swift{--fa:""}.fa-angular{--fa:""}.fa-speakap{--fa:""}.fa-angrycreative{--fa:""}.fa-y-combinator{--fa:""}.fa-empire{--fa:""}.fa-envira{--fa:""}.fa-google-scholar{--fa:""}.fa-gitlab-square,.fa-square-gitlab{--fa:""}.fa-studiovinari{--fa:""}.fa-pied-piper{--fa:""}.fa-wordpress{--fa:""}.fa-product-hunt{--fa:""}.fa-firefox{--fa:""}.fa-linode{--fa:""}.fa-goodreads{--fa:""}.fa-odnoklassniki-square,.fa-square-odnoklassniki{--fa:""}.fa-jsfiddle{--fa:""}.fa-sith{--fa:""}.fa-themeisle{--fa:""}.fa-page4{--fa:""}.fa-hashnode{--fa:""}.fa-react{--fa:""}.fa-cc-paypal{--fa:""}.fa-squarespace{--fa:""}.fa-cc-stripe{--fa:""}.fa-creative-commons-share{--fa:""}.fa-bitcoin{--fa:""}.fa-keycdn{--fa:""}.fa-opera{--fa:""}.fa-itch-io{--fa:""}.fa-umbraco{--fa:""}.fa-galactic-senate{--fa:""}.fa-ubuntu{--fa:""}.fa-draft2digital{--fa:""}.fa-stripe{--fa:""}.fa-houzz{--fa:""}.fa-gg{--fa:""}.fa-dhl{--fa:""}.fa-pinterest-square,.fa-square-pinterest{--fa:""}.fa-xing{--fa:""}.fa-blackberry{--fa:""}.fa-creative-commons-pd{--fa:""}.fa-playstation{--fa:""}.fa-quinscape{--fa:""}.fa-less{--fa:""}.fa-blogger-b{--fa:""}.fa-opencart{--fa:""}.fa-vine{--fa:""}.fa-signal-messenger{--fa:""}.fa-paypal{--fa:""}.fa-gitlab{--fa:""}.fa-typo3{--fa:""}.fa-reddit-alien{--fa:""}.fa-yahoo{--fa:""}.fa-dailymotion{--fa:""}.fa-affiliatetheme{--fa:""}.fa-pied-piper-pp{--fa:""}.fa-bootstrap{--fa:""}.fa-odnoklassniki{--fa:""}.fa-nfc-symbol{--fa:""}.fa-mintbit{--fa:""}.fa-ethereum{--fa:""}.fa-speaker-deck{--fa:""}.fa-creative-commons-nc-eu{--fa:""}.fa-patreon{--fa:""}.fa-avianex{--fa:""}.fa-ello{--fa:""}.fa-gofore{--fa:""}.fa-bimobject{--fa:""}.fa-brave-reverse{--fa:""}.fa-facebook-f{--fa:""}.fa-google-plus-square,.fa-square-google-plus{--fa:""}.fa-web-awesome{--fa:""}.fa-mandalorian{--fa:""}.fa-first-order-alt{--fa:""}.fa-osi{--fa:""}.fa-google-wallet{--fa:""}.fa-d-and-d-beyond{--fa:""}.fa-periscope{--fa:""}.fa-fulcrum{--fa:""}.fa-cloudscale{--fa:""}.fa-forumbee{--fa:""}.fa-mizuni{--fa:""}.fa-schlix{--fa:""}.fa-square-xing,.fa-xing-square{--fa:""}.fa-bandcamp{--fa:""}.fa-wpforms{--fa:""}.fa-cloudversify{--fa:""}.fa-usps{--fa:""}.fa-megaport{--fa:""}.fa-magento{--fa:""}.fa-spotify{--fa:""}.fa-optin-monster{--fa:""}.fa-fly{--fa:""}.fa-square-bluesky{--fa:""}.fa-aviato{--fa:""}.fa-itunes{--fa:""}.fa-cuttlefish{--fa:""}.fa-blogger{--fa:""}.fa-flickr{--fa:""}.fa-viber{--fa:""}.fa-soundcloud{--fa:""}.fa-digg{--fa:""}.fa-tencent-weibo{--fa:""}.fa-letterboxd{--fa:""}.fa-symfony{--fa:""}.fa-maxcdn{--fa:""}.fa-etsy{--fa:""}.fa-facebook-messenger{--fa:""}.fa-audible{--fa:""}.fa-think-peaks{--fa:""}.fa-bilibili{--fa:""}.fa-erlang{--fa:""}.fa-x-twitter{--fa:""}.fa-cotton-bureau{--fa:""}.fa-dashcube{--fa:""}.fa-42-group,.fa-innosoft{--fa:""}.fa-stack-exchange{--fa:""}.fa-elementor{--fa:""}.fa-pied-piper-square,.fa-square-pied-piper{--fa:""}.fa-creative-commons-nd{--fa:""}.fa-palfed{--fa:""}.fa-superpowers{--fa:""}.fa-resolving{--fa:""}.fa-xbox{--fa:""}.fa-square-web-awesome-stroke{--fa:""}.fa-searchengin{--fa:""}.fa-tiktok{--fa:""}.fa-facebook-square,.fa-square-facebook{--fa:""}.fa-renren{--fa:""}.fa-linux{--fa:""}.fa-glide{--fa:""}.fa-linkedin{--fa:""}.fa-hubspot{--fa:""}.fa-deploydog{--fa:""}.fa-twitch{--fa:""}.fa-flutter{--fa:""}.fa-ravelry{--fa:""}.fa-mixer{--fa:""}.fa-lastfm-square,.fa-square-lastfm{--fa:""}.fa-vimeo{--fa:""}.fa-mendeley{--fa:""}.fa-uniregistry{--fa:""}.fa-figma{--fa:""}.fa-creative-commons-remix{--fa:""}.fa-cc-amazon-pay{--fa:""}.fa-dropbox{--fa:""}.fa-instagram{--fa:""}.fa-cmplid{--fa:""}.fa-upwork{--fa:""}.fa-facebook{--fa:""}.fa-gripfire{--fa:""}.fa-jedi-order{--fa:""}.fa-uikit{--fa:""}.fa-fort-awesome-alt{--fa:""}.fa-phabricator{--fa:""}.fa-ussunnah{--fa:""}.fa-earlybirds{--fa:""}.fa-trade-federation{--fa:""}.fa-autoprefixer{--fa:""}.fa-whatsapp{--fa:""}.fa-square-upwork{--fa:""}.fa-slideshare{--fa:""}.fa-google-play{--fa:""}.fa-viadeo{--fa:""}.fa-line{--fa:""}.fa-google-drive{--fa:""}.fa-servicestack{--fa:""}.fa-simplybuilt{--fa:""}.fa-bitbucket{--fa:""}.fa-imdb{--fa:""}.fa-deezer{--fa:""}.fa-raspberry-pi{--fa:""}.fa-jira{--fa:""}.fa-docker{--fa:""}.fa-screenpal{--fa:""}.fa-bluetooth{--fa:""}.fa-gitter{--fa:""}.fa-d-and-d{--fa:""}.fa-microblog{--fa:""}.fa-cc-diners-club{--fa:""}.fa-gg-circle{--fa:""}.fa-pied-piper-hat{--fa:""}.fa-kickstarter-k{--fa:""}.fa-yandex{--fa:""}.fa-readme{--fa:""}.fa-html5{--fa:""}.fa-sellsy{--fa:""}.fa-square-web-awesome{--fa:""}.fa-sass{--fa:""}.fa-wirsindhandwerk,.fa-wsh{--fa:""}.fa-buromobelexperte{--fa:""}.fa-salesforce{--fa:""}.fa-octopus-deploy{--fa:""}.fa-medapps{--fa:""}.fa-ns8{--fa:""}.fa-pinterest-p{--fa:""}.fa-apper{--fa:""}.fa-fort-awesome{--fa:""}.fa-waze{--fa:""}.fa-bluesky{--fa:""}.fa-cc-jcb{--fa:""}.fa-snapchat,.fa-snapchat-ghost{--fa:""}.fa-fantasy-flight-games{--fa:""}.fa-rust{--fa:""}.fa-wix{--fa:""}.fa-behance-square,.fa-square-behance{--fa:""}.fa-supple{--fa:""}.fa-webflow{--fa:""}.fa-rebel{--fa:""}.fa-css3{--fa:""}.fa-staylinked{--fa:""}.fa-kaggle{--fa:""}.fa-space-awesome{--fa:""}.fa-deviantart{--fa:""}.fa-cpanel{--fa:""}.fa-goodreads-g{--fa:""}.fa-git-square,.fa-square-git{--fa:""}.fa-square-tumblr,.fa-tumblr-square{--fa:""}.fa-trello{--fa:""}.fa-creative-commons-nc-jp{--fa:""}.fa-get-pocket{--fa:""}.fa-perbyte{--fa:""}.fa-grunt{--fa:""}.fa-weebly{--fa:""}.fa-connectdevelop{--fa:""}.fa-leanpub{--fa:""}.fa-black-tie{--fa:""}.fa-themeco{--fa:""}.fa-python{--fa:""}.fa-android{--fa:""}.fa-bots{--fa:""}.fa-free-code-camp{--fa:""}.fa-hornbill{--fa:""}.fa-js{--fa:""}.fa-ideal{--fa:""}.fa-git{--fa:""}.fa-dev{--fa:""}.fa-sketch{--fa:""}.fa-yandex-international{--fa:""}.fa-cc-amex{--fa:""}.fa-uber{--fa:""}.fa-github{--fa:""}.fa-php{--fa:""}.fa-alipay{--fa:""}.fa-youtube{--fa:""}.fa-skyatlas{--fa:""}.fa-firefox-browser{--fa:""}.fa-replyd{--fa:""}.fa-suse{--fa:""}.fa-jenkins{--fa:""}.fa-twitter{--fa:""}.fa-rockrms{--fa:""}.fa-pinterest{--fa:""}.fa-buffer{--fa:""}.fa-npm{--fa:""}.fa-yammer{--fa:""}.fa-btc{--fa:""}.fa-dribbble{--fa:""}.fa-stumbleupon-circle{--fa:""}.fa-internet-explorer{--fa:""}.fa-stubber{--fa:""}.fa-telegram,.fa-telegram-plane{--fa:""}.fa-old-republic{--fa:""}.fa-odysee{--fa:""}.fa-square-whatsapp,.fa-whatsapp-square{--fa:""}.fa-node-js{--fa:""}.fa-edge-legacy{--fa:""}.fa-slack,.fa-slack-hash{--fa:""}.fa-medrt{--fa:""}.fa-usb{--fa:""}.fa-tumblr{--fa:""}.fa-vaadin{--fa:""}.fa-quora{--fa:""}.fa-square-x-twitter{--fa:""}.fa-reacteurope{--fa:""}.fa-medium,.fa-medium-m{--fa:""}.fa-amilia{--fa:""}.fa-mixcloud{--fa:""}.fa-flipboard{--fa:""}.fa-viacoin{--fa:""}.fa-critical-role{--fa:""}.fa-sitrox{--fa:""}.fa-discourse{--fa:""}.fa-joomla{--fa:""}.fa-mastodon{--fa:""}.fa-airbnb{--fa:""}.fa-wolf-pack-battalion{--fa:""}.fa-buy-n-large{--fa:""}.fa-gulp{--fa:""}.fa-creative-commons-sampling-plus{--fa:""}.fa-strava{--fa:""}.fa-ember{--fa:""}.fa-canadian-maple-leaf{--fa:""}.fa-teamspeak{--fa:""}.fa-pushed{--fa:""}.fa-wordpress-simple{--fa:""}.fa-nutritionix{--fa:""}.fa-wodu{--fa:""}.fa-google-pay{--fa:""}.fa-intercom{--fa:""}.fa-zhihu{--fa:""}.fa-korvue{--fa:""}.fa-pix{--fa:""}.fa-steam-symbol{--fa:""}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(/assets/fa-regular-400-BjRzuEpd.woff2) format("woff2"),url(/assets/fa-regular-400-DZaxPHgR.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(/assets/fa-solid-900-CTAAxXor.woff2) format("woff2"),url(/assets/fa-solid-900-D0aA9rwL.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(/assets/fa-brands-400-D_cYUPeE.woff2) format("woff2"),url(/assets/fa-brands-400-D1LuMI3I.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(/assets/fa-solid-900-CTAAxXor.woff2) format("woff2"),url(/assets/fa-solid-900-D0aA9rwL.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(/assets/fa-regular-400-BjRzuEpd.woff2) format("woff2"),url(/assets/fa-regular-400-DZaxPHgR.ttf) format("truetype")}@font-face{font-family:FontAwesome;font-display:block;src:url(/assets/fa-solid-900-CTAAxXor.woff2) format("woff2"),url(/assets/fa-solid-900-D0aA9rwL.ttf) format("truetype")}@font-face{font-family:FontAwesome;font-display:block;src:url(/assets/fa-brands-400-D_cYUPeE.woff2) format("woff2"),url(/assets/fa-brands-400-D1LuMI3I.ttf) format("truetype")}@font-face{font-family:FontAwesome;font-display:block;src:url(/assets/fa-regular-400-BjRzuEpd.woff2) format("woff2"),url(/assets/fa-regular-400-DZaxPHgR.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:FontAwesome;font-display:block;src:url(/assets/fa-v4compatibility-C9RhG_FT.woff2) format("woff2"),url(/assets/fa-v4compatibility-CCth-dXg.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} diff --git a/webui/dist/assets/index-ZvFbjZEA.js b/webui/dist/assets/index-ZvFbjZEA.js new file mode 100644 index 0000000..526abd0 --- /dev/null +++ b/webui/dist/assets/index-ZvFbjZEA.js @@ -0,0 +1,18 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const a of r.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&n(a)}).observe(document,{childList:!0,subtree:!0});function s(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(i){if(i.ep)return;i.ep=!0;const r=s(i);fetch(i.href,r)}})();/** +* @vue/shared v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function li(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const me={},os=[],vt=()=>{},Vl=()=>!1,un=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),oi=e=>e.startsWith("onUpdate:"),Pe=Object.assign,ri=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Xr=Object.prototype.hasOwnProperty,ae=(e,t)=>Xr.call(e,t),K=Array.isArray,rs=e=>Ns(e)==="[object Map]",fs=e=>Ns(e)==="[object Set]",zi=e=>Ns(e)==="[object Date]",Q=e=>typeof e=="function",_e=e=>typeof e=="string",ht=e=>typeof e=="symbol",pe=e=>e!==null&&typeof e=="object",Bl=e=>(pe(e)||Q(e))&&Q(e.then)&&Q(e.catch),Wl=Object.prototype.toString,Ns=e=>Wl.call(e),ea=e=>Ns(e).slice(8,-1),Gl=e=>Ns(e)==="[object Object]",ai=e=>_e(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,gs=li(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),cn=e=>{const t=Object.create(null);return s=>t[s]||(t[s]=e(s))},ta=/-\w/g,Lt=cn(e=>e.replace(ta,t=>t.slice(1).toUpperCase())),sa=/\B([A-Z])/g,qt=cn(e=>e.replace(sa,"-$1").toLowerCase()),Kl=cn(e=>e.charAt(0).toUpperCase()+e.slice(1)),In=cn(e=>e?`on${Kl(e)}`:""),Ft=(e,t)=>!Object.is(e,t),zs=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},dn=e=>{const t=parseFloat(e);return isNaN(t)?e:t},na=e=>{const t=_e(e)?Number(e):NaN;return isNaN(t)?e:t};let Zi;const fn=()=>Zi||(Zi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Jt(e){if(K(e)){const t={};for(let s=0;s{if(s){const n=s.split(la);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Se(e){let t="";if(_e(e))t=e;else if(K(e))for(let s=0;s$s(s,t))}const Yl=e=>!!(e&&e.__v_isRef===!0),I=e=>_e(e)?e:e==null?"":K(e)||pe(e)&&(e.toString===Wl||!Q(e.toString))?Yl(e)?I(e.value):JSON.stringify(e,Jl,2):String(e),Jl=(e,t)=>Yl(t)?Jl(e,t.value):rs(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,i],r)=>(s[Tn(n,r)+" =>"]=i,s),{})}:fs(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>Tn(s))}:ht(t)?Tn(t):pe(t)&&!K(t)&&!Gl(t)?String(t):t,Tn=(e,t="")=>{var s;return ht(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};/** +* @vue/reactivity v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ke;class da{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=Ke,!t&&Ke&&(this.index=(Ke.scopes||(Ke.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes)for(t=0,s=this.scopes.length;t0&&--this._on===0&&(Ke=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let s,n;for(s=0,n=this.effects.length;s0)return;if(ys){let t=ys;for(ys=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;bs;){let t=bs;for(bs=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function eo(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function to(e){let t,s=e.depsTail,n=s;for(;n;){const i=n.prevDep;n.version===-1?(n===s&&(s=i),fi(n),pa(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=i}e.deps=t,e.depsTail=s}function Kn(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(so(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function so(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===ks)||(e.globalVersion=ks,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Kn(e))))return;e.flags|=2;const t=e.dep,s=ge,n=ot;ge=e,ot=!0;try{eo(e);const i=e.fn(e._value);(t.version===0||Ft(i,e._value))&&(e.flags|=128,e._value=i,t.version++)}catch(i){throw t.version++,i}finally{ge=s,ot=n,to(e),e.flags&=-3}}function fi(e,t=!1){const{dep:s,prevSub:n,nextSub:i}=e;if(n&&(n.nextSub=i,e.prevSub=void 0),i&&(i.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let r=s.computed.deps;r;r=r.nextDep)fi(r,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function pa(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let ot=!0;const no=[];function kt(){no.push(ot),ot=!1}function At(){const e=no.pop();ot=e===void 0?!0:e}function Yi(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=ge;ge=void 0;try{t()}finally{ge=s}}}let ks=0;class ma{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class pi{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!ge||!ot||ge===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==ge)s=this.activeLink=new ma(ge,this),ge.deps?(s.prevDep=ge.depsTail,ge.depsTail.nextDep=s,ge.depsTail=s):ge.deps=ge.depsTail=s,io(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=ge.depsTail,s.nextDep=void 0,ge.depsTail.nextDep=s,ge.depsTail=s,ge.deps===s&&(ge.deps=n)}return s}trigger(t){this.version++,ks++,this.notify(t)}notify(t){ci();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{di()}}}function io(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)io(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const zn=new WeakMap,Zt=Symbol(""),Zn=Symbol(""),As=Symbol("");function $e(e,t,s){if(ot&&ge){let n=zn.get(e);n||zn.set(e,n=new Map);let i=n.get(s);i||(n.set(s,i=new pi),i.map=n,i.key=s),i.track()}}function wt(e,t,s,n,i,r){const a=zn.get(e);if(!a){ks++;return}const u=d=>{d&&d.trigger()};if(ci(),t==="clear")a.forEach(u);else{const d=K(e),v=d&&ai(s);if(d&&s==="length"){const m=Number(n);a.forEach((b,w)=>{(w==="length"||w===As||!ht(w)&&w>=m)&&u(b)})}else switch((s!==void 0||a.has(void 0))&&u(a.get(s)),v&&u(a.get(As)),t){case"add":d?v&&u(a.get("length")):(u(a.get(Zt)),rs(e)&&u(a.get(Zn)));break;case"delete":d||(u(a.get(Zt)),rs(e)&&u(a.get(Zn)));break;case"set":rs(e)&&u(a.get(Zt));break}}di()}function ns(e){const t=re(e);return t===e?t:($e(t,"iterate",As),st(e)?t:t.map(rt))}function pn(e){return $e(e=re(e),"iterate",As),e}function Rt(e,t){return Pt(e)?Yt(e)?cs(rt(t)):cs(t):rt(t)}const va={__proto__:null,[Symbol.iterator](){return Rn(this,Symbol.iterator,e=>Rt(this,e))},concat(...e){return ns(this).concat(...e.map(t=>K(t)?ns(t):t))},entries(){return Rn(this,"entries",e=>(e[1]=Rt(this,e[1]),e))},every(e,t){return bt(this,"every",e,t,void 0,arguments)},filter(e,t){return bt(this,"filter",e,t,s=>s.map(n=>Rt(this,n)),arguments)},find(e,t){return bt(this,"find",e,t,s=>Rt(this,s),arguments)},findIndex(e,t){return bt(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return bt(this,"findLast",e,t,s=>Rt(this,s),arguments)},findLastIndex(e,t){return bt(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return bt(this,"forEach",e,t,void 0,arguments)},includes(...e){return Nn(this,"includes",e)},indexOf(...e){return Nn(this,"indexOf",e)},join(e){return ns(this).join(e)},lastIndexOf(...e){return Nn(this,"lastIndexOf",e)},map(e,t){return bt(this,"map",e,t,void 0,arguments)},pop(){return ms(this,"pop")},push(...e){return ms(this,"push",e)},reduce(e,...t){return Ji(this,"reduce",e,t)},reduceRight(e,...t){return Ji(this,"reduceRight",e,t)},shift(){return ms(this,"shift")},some(e,t){return bt(this,"some",e,t,void 0,arguments)},splice(...e){return ms(this,"splice",e)},toReversed(){return ns(this).toReversed()},toSorted(e){return ns(this).toSorted(e)},toSpliced(...e){return ns(this).toSpliced(...e)},unshift(...e){return ms(this,"unshift",e)},values(){return Rn(this,"values",e=>Rt(this,e))}};function Rn(e,t,s){const n=pn(e),i=n[t]();return n!==e&&!st(e)&&(i._next=i.next,i.next=()=>{const r=i._next();return r.done||(r.value=s(r.value)),r}),i}const ha=Array.prototype;function bt(e,t,s,n,i,r){const a=pn(e),u=a!==e&&!st(e),d=a[t];if(d!==ha[t]){const b=d.apply(e,r);return u?rt(b):b}let v=s;a!==e&&(u?v=function(b,w){return s.call(this,Rt(e,b),w,e)}:s.length>2&&(v=function(b,w){return s.call(this,b,w,e)}));const m=d.call(a,v,n);return u&&i?i(m):m}function Ji(e,t,s,n){const i=pn(e);let r=s;return i!==e&&(st(e)?s.length>3&&(r=function(a,u,d){return s.call(this,a,u,d,e)}):r=function(a,u,d){return s.call(this,a,Rt(e,u),d,e)}),i[t](r,...n)}function Nn(e,t,s){const n=re(e);$e(n,"iterate",As);const i=n[t](...s);return(i===-1||i===!1)&&gi(s[0])?(s[0]=re(s[0]),n[t](...s)):i}function ms(e,t,s=[]){kt(),ci();const n=re(e)[t].apply(e,s);return di(),At(),n}const ga=li("__proto__,__v_isRef,__isVue"),lo=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ht));function ba(e){ht(e)||(e=String(e));const t=re(this);return $e(t,"has",e),t.hasOwnProperty(e)}class oo{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const i=this._isReadonly,r=this._isShallow;if(s==="__v_isReactive")return!i;if(s==="__v_isReadonly")return i;if(s==="__v_isShallow")return r;if(s==="__v_raw")return n===(i?r?Ma:co:r?uo:ao).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const a=K(t);if(!i){let d;if(a&&(d=va[s]))return d;if(s==="hasOwnProperty")return ba}const u=Reflect.get(t,s,De(t)?t:n);if((ht(s)?lo.has(s):ga(s))||(i||$e(t,"get",s),r))return u;if(De(u)){const d=a&&ai(s)?u:u.value;return i&&pe(d)?Jn(d):d}return pe(u)?i?Jn(u):vi(u):u}}class ro extends oo{constructor(t=!1){super(!1,t)}set(t,s,n,i){let r=t[s];const a=K(t)&&ai(s);if(!this._isShallow){const v=Pt(r);if(!st(n)&&!Pt(n)&&(r=re(r),n=re(n)),!a&&De(r)&&!De(n))return v||(r.value=n),!0}const u=a?Number(s)e,js=e=>Reflect.getPrototypeOf(e);function Ca(e,t,s){return function(...n){const i=this.__v_raw,r=re(i),a=rs(r),u=e==="entries"||e===Symbol.iterator&&a,d=e==="keys"&&a,v=i[e](...n),m=s?Yn:t?cs:rt;return!t&&$e(r,"iterate",d?Zn:Zt),{next(){const{value:b,done:w}=v.next();return w?{value:b,done:w}:{value:u?[m(b[0]),m(b[1])]:m(b),done:w}},[Symbol.iterator](){return this}}}}function Us(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Sa(e,t){const s={get(i){const r=this.__v_raw,a=re(r),u=re(i);e||(Ft(i,u)&&$e(a,"get",i),$e(a,"get",u));const{has:d}=js(a),v=t?Yn:e?cs:rt;if(d.call(a,i))return v(r.get(i));if(d.call(a,u))return v(r.get(u));r!==a&&r.get(i)},get size(){const i=this.__v_raw;return!e&&$e(re(i),"iterate",Zt),i.size},has(i){const r=this.__v_raw,a=re(r),u=re(i);return e||(Ft(i,u)&&$e(a,"has",i),$e(a,"has",u)),i===u?r.has(i):r.has(i)||r.has(u)},forEach(i,r){const a=this,u=a.__v_raw,d=re(u),v=t?Yn:e?cs:rt;return!e&&$e(d,"iterate",Zt),u.forEach((m,b)=>i.call(r,v(m),v(b),a))}};return Pe(s,e?{add:Us("add"),set:Us("set"),delete:Us("delete"),clear:Us("clear")}:{add(i){!t&&!st(i)&&!Pt(i)&&(i=re(i));const r=re(this);return js(r).has.call(r,i)||(r.add(i),wt(r,"add",i,i)),this},set(i,r){!t&&!st(r)&&!Pt(r)&&(r=re(r));const a=re(this),{has:u,get:d}=js(a);let v=u.call(a,i);v||(i=re(i),v=u.call(a,i));const m=d.call(a,i);return a.set(i,r),v?Ft(r,m)&&wt(a,"set",i,r):wt(a,"add",i,r),this},delete(i){const r=re(this),{has:a,get:u}=js(r);let d=a.call(r,i);d||(i=re(i),d=a.call(r,i)),u&&u.call(r,i);const v=r.delete(i);return d&&wt(r,"delete",i,void 0),v},clear(){const i=re(this),r=i.size!==0,a=i.clear();return r&&wt(i,"clear",void 0,void 0),a}}),["keys","values","entries",Symbol.iterator].forEach(i=>{s[i]=Ca(i,e,t)}),s}function mi(e,t){const s=Sa(e,t);return(n,i,r)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(ae(s,i)&&i in n?s:n,i,r)}const ka={get:mi(!1,!1)},Aa={get:mi(!1,!0)},Pa={get:mi(!0,!1)};const ao=new WeakMap,uo=new WeakMap,co=new WeakMap,Ma=new WeakMap;function Ia(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ta(e){return e.__v_skip||!Object.isExtensible(e)?0:Ia(ea(e))}function vi(e){return Pt(e)?e:hi(e,!1,xa,ka,ao)}function Ea(e){return hi(e,!1,wa,Aa,uo)}function Jn(e){return hi(e,!0,_a,Pa,co)}function hi(e,t,s,n,i){if(!pe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const r=Ta(e);if(r===0)return e;const a=i.get(e);if(a)return a;const u=new Proxy(e,r===2?n:s);return i.set(e,u),u}function Yt(e){return Pt(e)?Yt(e.__v_raw):!!(e&&e.__v_isReactive)}function Pt(e){return!!(e&&e.__v_isReadonly)}function st(e){return!!(e&&e.__v_isShallow)}function gi(e){return e?!!e.__v_raw:!1}function re(e){const t=e&&e.__v_raw;return t?re(t):e}function Ra(e){return!ae(e,"__v_skip")&&Object.isExtensible(e)&&zl(e,"__v_skip",!0),e}const rt=e=>pe(e)?vi(e):e,cs=e=>pe(e)?Jn(e):e;function De(e){return e?e.__v_isRef===!0:!1}function te(e){return Na(e,!1)}function Na(e,t){return De(e)?e:new $a(e,t)}class $a{constructor(t,s){this.dep=new pi,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:re(t),this._value=s?t:rt(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,n=this.__v_isShallow||st(t)||Pt(t);t=n?t:re(t),Ft(t,s)&&(this._rawValue=t,this._value=n?t:rt(t),this.dep.trigger())}}function fo(e){return De(e)?e.value:e}const Da={get:(e,t,s)=>t==="__v_raw"?e:fo(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const i=e[t];return De(i)&&!De(s)?(i.value=s,!0):Reflect.set(e,t,s,n)}};function po(e){return Yt(e)?e:new Proxy(e,Da)}class Fa{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new pi(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=ks-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&ge!==this)return Xl(this,!0),!0}get value(){const t=this.dep.track();return so(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function La(e,t,s=!1){let n,i;return Q(e)?n=e:(n=e.get,i=e.set),new Fa(n,i,s)}const Vs={},Xs=new WeakMap;let Kt;function Oa(e,t=!1,s=Kt){if(s){let n=Xs.get(s);n||Xs.set(s,n=[]),n.push(e)}}function Ha(e,t,s=me){const{immediate:n,deep:i,once:r,scheduler:a,augmentJob:u,call:d}=s,v=_=>i?_:st(_)||i===!1||i===0?Ct(_,1):Ct(_);let m,b,w,C,O=!1,g=!1;if(De(e)?(b=()=>e.value,O=st(e)):Yt(e)?(b=()=>v(e),O=!0):K(e)?(g=!0,O=e.some(_=>Yt(_)||st(_)),b=()=>e.map(_=>{if(De(_))return _.value;if(Yt(_))return v(_);if(Q(_))return d?d(_,2):_()})):Q(e)?t?b=d?()=>d(e,2):e:b=()=>{if(w){kt();try{w()}finally{At()}}const _=Kt;Kt=m;try{return d?d(e,3,[C]):e(C)}finally{Kt=_}}:b=vt,t&&i){const _=b,W=i===!0?1/0:i;b=()=>Ct(_(),W)}const N=fa(),M=()=>{m.stop(),N&&N.active&&ri(N.effects,m)};if(r&&t){const _=t;t=(...W)=>{_(...W),M()}}let D=g?new Array(e.length).fill(Vs):Vs;const L=_=>{if(!(!(m.flags&1)||!m.dirty&&!_))if(t){const W=m.run();if(i||O||(g?W.some((J,le)=>Ft(J,D[le])):Ft(W,D))){w&&w();const J=Kt;Kt=m;try{const le=[W,D===Vs?void 0:g&&D[0]===Vs?[]:D,C];D=W,d?d(t,3,le):t(...le)}finally{Kt=J}}}else m.run()};return u&&u(L),m=new ql(b),m.scheduler=a?()=>a(L,!1):L,C=_=>Oa(_,!1,m),w=m.onStop=()=>{const _=Xs.get(m);if(_){if(d)d(_,4);else for(const W of _)W();Xs.delete(m)}},t?n?L(!0):D=m.run():a?a(L.bind(null,!0),!0):m.run(),M.pause=m.pause.bind(m),M.resume=m.resume.bind(m),M.stop=M,M}function Ct(e,t=1/0,s){if(t<=0||!pe(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,De(e))Ct(e.value,t,s);else if(K(e))for(let n=0;n{Ct(n,t,s)});else if(Gl(e)){for(const n in e)Ct(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&Ct(e[n],t,s)}return e}/** +* @vue/runtime-core v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Ds(e,t,s,n){try{return n?e(...n):e()}catch(i){mn(i,t,s)}}function at(e,t,s,n){if(Q(e)){const i=Ds(e,t,s,n);return i&&Bl(i)&&i.catch(r=>{mn(r,t,s)}),i}if(K(e)){const i=[];for(let r=0;r>>1,i=Ue[n],r=Ps(i);r=Ps(s)?Ue.push(e):Ue.splice(Ua(t),0,e),e.flags|=1,vo()}}function vo(){en||(en=mo.then(go))}function Va(e){K(e)?as.push(...e):Nt&&e.id===-1?Nt.splice(is+1,0,e):e.flags&1||(as.push(e),e.flags|=1),vo()}function qi(e,t,s=pt+1){for(;sPs(s)-Ps(n));if(as.length=0,Nt){Nt.push(...t);return}for(Nt=t,is=0;ise.id==null?e.flags&2?-1:1/0:e.id;function go(e){try{for(pt=0;pt{n._d&&on(-1);const r=tn(t);let a;try{a=e(...i)}finally{tn(r),n._d&&on(1)}return a};return n._n=!0,n._c=!0,n._d=!0,n}function q(e,t){if(tt===null)return e;const s=xn(tt),n=e.dirs||(e.dirs=[]);for(let i=0;i1)return s&&Q(t)?t.call(n&&n.proxy):t}}const Wa=Symbol.for("v-scx"),Ga=()=>Zs(Wa);function Ge(e,t,s){return xo(e,t,s)}function xo(e,t,s=me){const{immediate:n,deep:i,flush:r,once:a}=s,u=Pe({},s),d=t&&n||!t&&r!=="post";let v;if(Ts){if(r==="sync"){const C=Ga();v=C.__watcherHandles||(C.__watcherHandles=[])}else if(!d){const C=()=>{};return C.stop=vt,C.resume=vt,C.pause=vt,C}}const m=Be;u.call=(C,O,g)=>at(C,m,O,g);let b=!1;r==="post"?u.scheduler=C=>{je(C,m&&m.suspense)}:r!=="sync"&&(b=!0,u.scheduler=(C,O)=>{O?C():bi(C)}),u.augmentJob=C=>{t&&(C.flags|=4),b&&(C.flags|=2,m&&(C.id=m.uid,C.i=m))};const w=Ha(e,t,u);return Ts&&(v?v.push(w):d&&w()),w}function Ka(e,t,s){const n=this.proxy,i=_e(e)?e.includes(".")?_o(n,e):()=>n[e]:e.bind(n,n);let r;Q(t)?r=t:(r=t.handler,s=t);const a=Fs(this),u=xo(i,r.bind(n),s);return a(),u}function _o(e,t){const s=t.split(".");return()=>{let n=e;for(let i=0;ie.__isTeleport,xs=e=>e&&(e.disabled||e.disabled===""),Qi=e=>e&&(e.defer||e.defer===""),Xi=e=>typeof SVGElement<"u"&&e instanceof SVGElement,el=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,qn=(e,t)=>{const s=e&&e.to;return _e(s)?t?t(s):null:s},So={name:"Teleport",__isTeleport:!0,process(e,t,s,n,i,r,a,u,d,v){const{mc:m,pc:b,pbc:w,o:{insert:C,querySelector:O,createText:g,createComment:N}}=v,M=xs(t.props);let{shapeFlag:D,children:L,dynamicChildren:_}=t;if(e==null){const W=t.el=g(""),J=t.anchor=g("");C(W,s,n),C(J,s,n);const le=(H,z)=>{D&16&&m(L,H,z,i,r,a,u,d)},ve=()=>{const H=t.target=qn(t.props,O),z=ko(H,t,g,C);H&&(a!=="svg"&&Xi(H)?a="svg":a!=="mathml"&&el(H)&&(a="mathml"),i&&i.isCE&&(i.ce._teleportTargets||(i.ce._teleportTargets=new Set)).add(H),M||(le(H,z),Ys(t,!1)))};M&&(le(s,J),Ys(t,!0)),Qi(t.props)?(t.el.__isMounted=!1,je(()=>{ve(),delete t.el.__isMounted},r)):ve()}else{if(Qi(t.props)&&e.el.__isMounted===!1){je(()=>{So.process(e,t,s,n,i,r,a,u,d,v)},r);return}t.el=e.el,t.targetStart=e.targetStart;const W=t.anchor=e.anchor,J=t.target=e.target,le=t.targetAnchor=e.targetAnchor,ve=xs(e.props),H=ve?s:J,z=ve?W:le;if(a==="svg"||Xi(J)?a="svg":(a==="mathml"||el(J))&&(a="mathml"),_?(w(e.dynamicChildren,_,H,i,r,a,u),wi(e,t,!0)):d||b(e,t,H,z,i,r,a,u,!1),M)ve?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Bs(t,s,W,v,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const ee=t.target=qn(t.props,O);ee&&Bs(t,ee,null,v,0)}else ve&&Bs(t,J,le,v,1);Ys(t,M)}},remove(e,t,s,{um:n,o:{remove:i}},r){const{shapeFlag:a,children:u,anchor:d,targetStart:v,targetAnchor:m,target:b,props:w}=e;if(b&&(i(v),i(m)),r&&i(d),a&16){const C=r||!xs(w);for(let O=0;O{e.isMounted=!0}),nn(()=>{e.isUnmounting=!0}),e}const et=[Function,Array],Ao={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:et,onEnter:et,onAfterEnter:et,onEnterCancelled:et,onBeforeLeave:et,onLeave:et,onAfterLeave:et,onLeaveCancelled:et,onBeforeAppear:et,onAppear:et,onAfterAppear:et,onAppearCancelled:et},Po=e=>{const t=e.subTree;return t.component?Po(t.component):t},Ja={name:"BaseTransition",props:Ao,setup(e,{slots:t}){const s=qo(),n=Ya();return()=>{const i=t.default&&To(t.default(),!0);if(!i||!i.length)return;const r=Mo(i),a=re(e),{mode:u}=a;if(n.isLeaving)return $n(r);const d=tl(r);if(!d)return $n(r);let v=Qn(d,a,n,s,b=>v=b);d.type!==Ve&&Ms(d,v);let m=s.subTree&&tl(s.subTree);if(m&&m.type!==Ve&&!zt(m,d)&&Po(s).type!==Ve){let b=Qn(m,a,n,s);if(Ms(m,b),u==="out-in"&&d.type!==Ve)return n.isLeaving=!0,b.afterLeave=()=>{n.isLeaving=!1,s.job.flags&8||s.update(),delete b.afterLeave,m=void 0},$n(r);u==="in-out"&&d.type!==Ve?b.delayLeave=(w,C,O)=>{const g=Io(n,m);g[String(m.key)]=m,w[_t]=()=>{C(),w[_t]=void 0,delete v.delayedLeave,m=void 0},v.delayedLeave=()=>{O(),delete v.delayedLeave,m=void 0}}:m=void 0}else m&&(m=void 0);return r}}};function Mo(e){let t=e[0];if(e.length>1){for(const s of e)if(s.type!==Ve){t=s;break}}return t}const qa=Ja;function Io(e,t){const{leavingVNodes:s}=e;let n=s.get(t.type);return n||(n=Object.create(null),s.set(t.type,n)),n}function Qn(e,t,s,n,i){const{appear:r,mode:a,persisted:u=!1,onBeforeEnter:d,onEnter:v,onAfterEnter:m,onEnterCancelled:b,onBeforeLeave:w,onLeave:C,onAfterLeave:O,onLeaveCancelled:g,onBeforeAppear:N,onAppear:M,onAfterAppear:D,onAppearCancelled:L}=t,_=String(e.key),W=Io(s,e),J=(H,z)=>{H&&at(H,n,9,z)},le=(H,z)=>{const ee=z[1];J(H,z),K(H)?H.every(U=>U.length<=1)&&ee():H.length<=1&&ee()},ve={mode:a,persisted:u,beforeEnter(H){let z=d;if(!s.isMounted)if(r)z=N||d;else return;H[_t]&&H[_t](!0);const ee=W[_];ee&&zt(e,ee)&&ee.el[_t]&&ee.el[_t](),J(z,[H])},enter(H){let z=v,ee=m,U=b;if(!s.isMounted)if(r)z=M||v,ee=D||m,U=L||b;else return;let ue=!1;const we=H[Ws]=be=>{ue||(ue=!0,be?J(U,[H]):J(ee,[H]),ve.delayedLeave&&ve.delayedLeave(),H[Ws]=void 0)};z?le(z,[H,we]):we()},leave(H,z){const ee=String(e.key);if(H[Ws]&&H[Ws](!0),s.isUnmounting)return z();J(w,[H]);let U=!1;const ue=H[_t]=we=>{U||(U=!0,z(),we?J(g,[H]):J(O,[H]),H[_t]=void 0,W[ee]===e&&delete W[ee])};W[ee]=e,C?le(C,[H,ue]):ue()},clone(H){const z=Qn(H,t,s,n,i);return i&&i(z),z}};return ve}function $n(e){if(vn(e))return e=Ot(e),e.children=null,e}function tl(e){if(!vn(e))return Co(e.type)&&e.children?Mo(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:s}=e;if(s){if(t&16)return s[0];if(t&32&&Q(s.default))return s.default()}}function Ms(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Ms(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function To(e,t=!1,s){let n=[],i=0;for(let r=0;r1)for(let r=0;r_s(O,t&&(K(t)?t[g]:t),s,n,i));return}if(ws(n)&&!i){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&_s(e,t,s,n.component.subTree);return}const r=n.shapeFlag&4?xn(n.component):n.el,a=i?null:r,{i:u,r:d}=e,v=t&&t.r,m=u.refs===me?u.refs={}:u.refs,b=u.setupState,w=re(b),C=b===me?Vl:O=>ae(w,O);if(v!=null&&v!==d){if(sl(t),_e(v))m[v]=null,C(v)&&(b[v]=null);else if(De(v)){v.value=null;const O=t;O.k&&(m[O.k]=null)}}if(Q(d))Ds(d,u,12,[a,m]);else{const O=_e(d),g=De(d);if(O||g){const N=()=>{if(e.f){const M=O?C(d)?b[d]:m[d]:d.value;if(i)K(M)&&ri(M,r);else if(K(M))M.includes(r)||M.push(r);else if(O)m[d]=[r],C(d)&&(b[d]=m[d]);else{const D=[r];d.value=D,e.k&&(m[e.k]=D)}}else O?(m[d]=a,C(d)&&(b[d]=a)):g&&(d.value=a,e.k&&(m[e.k]=a))};if(a){const M=()=>{N(),sn.delete(e)};M.id=-1,sn.set(e,M),je(M,s)}else sl(e),N()}}}function sl(e){const t=sn.get(e);t&&(t.flags|=8,sn.delete(e))}fn().requestIdleCallback;fn().cancelIdleCallback;const ws=e=>!!e.type.__asyncLoader,vn=e=>e.type.__isKeepAlive;function Qa(e,t){Ro(e,"a",t)}function Xa(e,t){Ro(e,"da",t)}function Ro(e,t,s=Be){const n=e.__wdc||(e.__wdc=()=>{let i=s;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(hn(t,n,s),s){let i=s.parent;for(;i&&i.parent;)vn(i.parent.vnode)&&eu(n,t,s,i),i=i.parent}}function eu(e,t,s,n){const i=hn(t,e,n,!0);No(()=>{ri(n[t],i)},s)}function hn(e,t,s=Be,n=!1){if(s){const i=s[e]||(s[e]=[]),r=t.__weh||(t.__weh=(...a)=>{kt();const u=Fs(s),d=at(t,s,e,a);return u(),At(),d});return n?i.unshift(r):i.push(r),r}}const Mt=e=>(t,s=Be)=>{(!Ts||e==="sp")&&hn(e,(...n)=>t(...n),s)},tu=Mt("bm"),yi=Mt("m"),su=Mt("bu"),nu=Mt("u"),nn=Mt("bum"),No=Mt("um"),iu=Mt("sp"),lu=Mt("rtg"),ou=Mt("rtc");function ru(e,t=Be){hn("ec",e,t)}const au=Symbol.for("v-ndc");function xe(e,t,s,n){let i;const r=s,a=K(e);if(a||_e(e)){const u=a&&Yt(e);let d=!1,v=!1;u&&(d=!st(e),v=Pt(e),e=pn(e)),i=new Array(e.length);for(let m=0,b=e.length;mt(u,d,void 0,r));else{const u=Object.keys(e);i=new Array(u.length);for(let d=0,v=u.length;de?Qo(e)?xn(e):Xn(e.parent):null,Cs=Pe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Xn(e.parent),$root:e=>Xn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Do(e),$forceUpdate:e=>e.f||(e.f=()=>{bi(e.update)}),$nextTick:e=>e.n||(e.n=ls.bind(e.proxy)),$watch:e=>Ka.bind(e)}),Dn=(e,t)=>e!==me&&!e.__isScriptSetup&&ae(e,t),uu={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:i,props:r,accessCache:a,type:u,appContext:d}=e;if(t[0]!=="$"){const w=a[t];if(w!==void 0)switch(w){case 1:return n[t];case 2:return i[t];case 4:return s[t];case 3:return r[t]}else{if(Dn(n,t))return a[t]=1,n[t];if(i!==me&&ae(i,t))return a[t]=2,i[t];if(ae(r,t))return a[t]=3,r[t];if(s!==me&&ae(s,t))return a[t]=4,s[t];ei&&(a[t]=0)}}const v=Cs[t];let m,b;if(v)return t==="$attrs"&&$e(e.attrs,"get",""),v(e);if((m=u.__cssModules)&&(m=m[t]))return m;if(s!==me&&ae(s,t))return a[t]=4,s[t];if(b=d.config.globalProperties,ae(b,t))return b[t]},set({_:e},t,s){const{data:n,setupState:i,ctx:r}=e;return Dn(i,t)?(i[t]=s,!0):n!==me&&ae(n,t)?(n[t]=s,!0):ae(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(r[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:i,props:r,type:a}},u){let d;return!!(s[u]||e!==me&&u[0]!=="$"&&ae(e,u)||Dn(t,u)||ae(r,u)||ae(n,u)||ae(Cs,u)||ae(i.config.globalProperties,u)||(d=a.__cssModules)&&d[u])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:ae(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function nl(e){return K(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let ei=!0;function cu(e){const t=Do(e),s=e.proxy,n=e.ctx;ei=!1,t.beforeCreate&&il(t.beforeCreate,e,"bc");const{data:i,computed:r,methods:a,watch:u,provide:d,inject:v,created:m,beforeMount:b,mounted:w,beforeUpdate:C,updated:O,activated:g,deactivated:N,beforeDestroy:M,beforeUnmount:D,destroyed:L,unmounted:_,render:W,renderTracked:J,renderTriggered:le,errorCaptured:ve,serverPrefetch:H,expose:z,inheritAttrs:ee,components:U,directives:ue,filters:we}=t;if(v&&du(v,n,null),a)for(const ne in a){const ce=a[ne];Q(ce)&&(n[ne]=ce.bind(s))}if(i){const ne=i.call(s,s);pe(ne)&&(e.data=vi(ne))}if(ei=!0,r)for(const ne in r){const ce=r[ne],Me=Q(ce)?ce.bind(s,s):Q(ce.get)?ce.get.bind(s,s):vt,Ie=!Q(ce)&&Q(ce.set)?ce.set.bind(s):vt,We=B({get:Me,set:Ie});Object.defineProperty(n,ne,{enumerable:!0,configurable:!0,get:()=>We.value,set:Fe=>We.value=Fe})}if(u)for(const ne in u)$o(u[ne],n,s,ne);if(d){const ne=Q(d)?d.call(s):d;Reflect.ownKeys(ne).forEach(ce=>{Ba(ce,ne[ce])})}m&&il(m,e,"c");function Ce(ne,ce){K(ce)?ce.forEach(Me=>ne(Me.bind(s))):ce&&ne(ce.bind(s))}if(Ce(tu,b),Ce(yi,w),Ce(su,C),Ce(nu,O),Ce(Qa,g),Ce(Xa,N),Ce(ru,ve),Ce(ou,J),Ce(lu,le),Ce(nn,D),Ce(No,_),Ce(iu,H),K(z))if(z.length){const ne=e.exposed||(e.exposed={});z.forEach(ce=>{Object.defineProperty(ne,ce,{get:()=>s[ce],set:Me=>s[ce]=Me,enumerable:!0})})}else e.exposed||(e.exposed={});W&&e.render===vt&&(e.render=W),ee!=null&&(e.inheritAttrs=ee),U&&(e.components=U),ue&&(e.directives=ue),H&&Eo(e)}function du(e,t,s=vt){K(e)&&(e=ti(e));for(const n in e){const i=e[n];let r;pe(i)?"default"in i?r=Zs(i.from||n,i.default,!0):r=Zs(i.from||n):r=Zs(i),De(r)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>r.value,set:a=>r.value=a}):t[n]=r}}function il(e,t,s){at(K(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function $o(e,t,s,n){let i=n.includes(".")?_o(s,n):()=>s[n];if(_e(e)){const r=t[e];Q(r)&&Ge(i,r)}else if(Q(e))Ge(i,e.bind(s));else if(pe(e))if(K(e))e.forEach(r=>$o(r,t,s,n));else{const r=Q(e.handler)?e.handler.bind(s):t[e.handler];Q(r)&&Ge(i,r,e)}}function Do(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:i,optionsCache:r,config:{optionMergeStrategies:a}}=e.appContext,u=r.get(t);let d;return u?d=u:!i.length&&!s&&!n?d=t:(d={},i.length&&i.forEach(v=>ln(d,v,a,!0)),ln(d,t,a)),pe(t)&&r.set(t,d),d}function ln(e,t,s,n=!1){const{mixins:i,extends:r}=t;r&&ln(e,r,s,!0),i&&i.forEach(a=>ln(e,a,s,!0));for(const a in t)if(!(n&&a==="expose")){const u=fu[a]||s&&s[a];e[a]=u?u(e[a],t[a]):t[a]}return e}const fu={data:ll,props:ol,emits:ol,methods:hs,computed:hs,beforeCreate:He,created:He,beforeMount:He,mounted:He,beforeUpdate:He,updated:He,beforeDestroy:He,beforeUnmount:He,destroyed:He,unmounted:He,activated:He,deactivated:He,errorCaptured:He,serverPrefetch:He,components:hs,directives:hs,watch:mu,provide:ll,inject:pu};function ll(e,t){return t?e?function(){return Pe(Q(e)?e.call(this,this):e,Q(t)?t.call(this,this):t)}:t:e}function pu(e,t){return hs(ti(e),ti(t))}function ti(e){if(K(e)){const t={};for(let s=0;st==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Lt(t)}Modifiers`]||e[`${qt(t)}Modifiers`];function bu(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||me;let i=s;const r=t.startsWith("update:"),a=r&&gu(n,t.slice(7));a&&(a.trim&&(i=s.map(m=>_e(m)?m.trim():m)),a.number&&(i=s.map(dn)));let u,d=n[u=In(t)]||n[u=In(Lt(t))];!d&&r&&(d=n[u=In(qt(t))]),d&&at(d,e,6,i);const v=n[u+"Once"];if(v){if(!e.emitted)e.emitted={};else if(e.emitted[u])return;e.emitted[u]=!0,at(v,e,6,i)}}const yu=new WeakMap;function Lo(e,t,s=!1){const n=s?yu:t.emitsCache,i=n.get(e);if(i!==void 0)return i;const r=e.emits;let a={},u=!1;if(!Q(e)){const d=v=>{const m=Lo(v,t,!0);m&&(u=!0,Pe(a,m))};!s&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}return!r&&!u?(pe(e)&&n.set(e,null),null):(K(r)?r.forEach(d=>a[d]=null):Pe(a,r),pe(e)&&n.set(e,a),a)}function gn(e,t){return!e||!un(t)?!1:(t=t.slice(2).replace(/Once$/,""),ae(e,t[0].toLowerCase()+t.slice(1))||ae(e,qt(t))||ae(e,t))}function rl(e){const{type:t,vnode:s,proxy:n,withProxy:i,propsOptions:[r],slots:a,attrs:u,emit:d,render:v,renderCache:m,props:b,data:w,setupState:C,ctx:O,inheritAttrs:g}=e,N=tn(e);let M,D;try{if(s.shapeFlag&4){const _=i||n,W=_;M=mt(v.call(W,_,m,b,C,w,O)),D=u}else{const _=t;M=mt(_.length>1?_(b,{attrs:u,slots:a,emit:d}):_(b,null)),D=t.props?u:xu(u)}}catch(_){Ss.length=0,mn(_,e,1),M=Ae(Ve)}let L=M;if(D&&g!==!1){const _=Object.keys(D),{shapeFlag:W}=L;_.length&&W&7&&(r&&_.some(oi)&&(D=_u(D,r)),L=Ot(L,D,!1,!0))}return s.dirs&&(L=Ot(L,null,!1,!0),L.dirs=L.dirs?L.dirs.concat(s.dirs):s.dirs),s.transition&&Ms(L,s.transition),M=L,tn(N),M}const xu=e=>{let t;for(const s in e)(s==="class"||s==="style"||un(s))&&((t||(t={}))[s]=e[s]);return t},_u=(e,t)=>{const s={};for(const n in e)(!oi(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function wu(e,t,s){const{props:n,children:i,component:r}=e,{props:a,children:u,patchFlag:d}=t,v=r.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&d>=0){if(d&1024)return!0;if(d&16)return n?al(n,a,v):!!a;if(d&8){const m=t.dynamicProps;for(let b=0;bObject.create(Oo),jo=e=>Object.getPrototypeOf(e)===Oo;function Su(e,t,s,n=!1){const i={},r=Ho();e.propsDefaults=Object.create(null),Uo(e,t,i,r);for(const a in e.propsOptions[0])a in i||(i[a]=void 0);s?e.props=n?i:Ea(i):e.type.props?e.props=i:e.props=r,e.attrs=r}function ku(e,t,s,n){const{props:i,attrs:r,vnode:{patchFlag:a}}=e,u=re(i),[d]=e.propsOptions;let v=!1;if((n||a>0)&&!(a&16)){if(a&8){const m=e.vnode.dynamicProps;for(let b=0;b{d=!0;const[w,C]=Vo(b,t,!0);Pe(a,w),C&&u.push(...C)};!s&&t.mixins.length&&t.mixins.forEach(m),e.extends&&m(e.extends),e.mixins&&e.mixins.forEach(m)}if(!r&&!d)return pe(e)&&n.set(e,os),os;if(K(r))for(let m=0;me==="_"||e==="_ctx"||e==="$stable",_i=e=>K(e)?e.map(mt):[mt(e)],Pu=(e,t,s)=>{if(t._n)return t;const n=yo((...i)=>_i(t(...i)),s);return n._c=!1,n},Bo=(e,t,s)=>{const n=e._ctx;for(const i in e){if(xi(i))continue;const r=e[i];if(Q(r))t[i]=Pu(i,r,n);else if(r!=null){const a=_i(r);t[i]=()=>a}}},Wo=(e,t)=>{const s=_i(t);e.slots.default=()=>s},Go=(e,t,s)=>{for(const n in t)(s||!xi(n))&&(e[n]=t[n])},Mu=(e,t,s)=>{const n=e.slots=Ho();if(e.vnode.shapeFlag&32){const i=t._;i?(Go(n,t,s),s&&zl(n,"_",i,!0)):Bo(t,n)}else t&&Wo(e,t)},Iu=(e,t,s)=>{const{vnode:n,slots:i}=e;let r=!0,a=me;if(n.shapeFlag&32){const u=t._;u?s&&u===1?r=!1:Go(i,t,s):(r=!t.$stable,Bo(t,i)),a=t}else t&&(Wo(e,t),a={default:1});if(r)for(const u in i)!xi(u)&&a[u]==null&&delete i[u]},je=$u;function Tu(e){return Eu(e)}function Eu(e,t){const s=fn();s.__VUE__=!0;const{insert:n,remove:i,patchProp:r,createElement:a,createText:u,createComment:d,setText:v,setElementText:m,parentNode:b,nextSibling:w,setScopeId:C=vt,insertStaticContent:O}=e,g=(p,h,x,S=null,k=null,A=null,F=void 0,y=null,E=!!h.dynamicChildren)=>{if(p===h)return;p&&!zt(p,h)&&(S=ut(p),Fe(p,k,A,!0),p=null),h.patchFlag===-2&&(E=!1,h.dynamicChildren=null);const{type:P,ref:G,shapeFlag:j}=h;switch(P){case bn:N(p,h,x,S);break;case Ve:M(p,h,x,S);break;case Js:p==null&&D(h,x,S,F);break;case ie:U(p,h,x,S,k,A,F,y,E);break;default:j&1?W(p,h,x,S,k,A,F,y,E):j&6?ue(p,h,x,S,k,A,F,y,E):(j&64||j&128)&&P.process(p,h,x,S,k,A,F,y,E,jt)}G!=null&&k?_s(G,p&&p.ref,A,h||p,!h):G==null&&p&&p.ref!=null&&_s(p.ref,null,A,p,!0)},N=(p,h,x,S)=>{if(p==null)n(h.el=u(h.children),x,S);else{const k=h.el=p.el;h.children!==p.children&&v(k,h.children)}},M=(p,h,x,S)=>{p==null?n(h.el=d(h.children||""),x,S):h.el=p.el},D=(p,h,x,S)=>{[p.el,p.anchor]=O(p.children,h,x,S,p.el,p.anchor)},L=({el:p,anchor:h},x,S)=>{let k;for(;p&&p!==h;)k=w(p),n(p,x,S),p=k;n(h,x,S)},_=({el:p,anchor:h})=>{let x;for(;p&&p!==h;)x=w(p),i(p),p=x;i(h)},W=(p,h,x,S,k,A,F,y,E)=>{if(h.type==="svg"?F="svg":h.type==="math"&&(F="mathml"),p==null)J(h,x,S,k,A,F,y,E);else{const P=p.el&&p.el._isVueCE?p.el:null;try{P&&P._beginPatch(),H(p,h,k,A,F,y,E)}finally{P&&P._endPatch()}}},J=(p,h,x,S,k,A,F,y)=>{let E,P;const{props:G,shapeFlag:j,transition:V,dirs:Z}=p;if(E=p.el=a(p.type,A,G&&G.is,G),j&8?m(E,p.children):j&16&&ve(p.children,E,null,S,k,Fn(p,A),F,y),Z&&Vt(p,null,S,"created"),le(E,p,p.scopeId,F,S),G){for(const de in G)de!=="value"&&!gs(de)&&r(E,de,null,G[de],A,S);"value"in G&&r(E,"value",null,G.value,A),(P=G.onVnodeBeforeMount)&&ft(P,S,p)}Z&&Vt(p,null,S,"beforeMount");const se=Ru(k,V);se&&V.beforeEnter(E),n(E,h,x),((P=G&&G.onVnodeMounted)||se||Z)&&je(()=>{P&&ft(P,S,p),se&&V.enter(E),Z&&Vt(p,null,S,"mounted")},k)},le=(p,h,x,S,k)=>{if(x&&C(p,x),S)for(let A=0;A{for(let P=E;P{const y=h.el=p.el;let{patchFlag:E,dynamicChildren:P,dirs:G}=h;E|=p.patchFlag&16;const j=p.props||me,V=h.props||me;let Z;if(x&&Bt(x,!1),(Z=V.onVnodeBeforeUpdate)&&ft(Z,x,h,p),G&&Vt(h,p,x,"beforeUpdate"),x&&Bt(x,!0),(j.innerHTML&&V.innerHTML==null||j.textContent&&V.textContent==null)&&m(y,""),P?z(p.dynamicChildren,P,y,x,S,Fn(h,k),A):F||ce(p,h,y,null,x,S,Fn(h,k),A,!1),E>0){if(E&16)ee(y,j,V,x,k);else if(E&2&&j.class!==V.class&&r(y,"class",null,V.class,k),E&4&&r(y,"style",j.style,V.style,k),E&8){const se=h.dynamicProps;for(let de=0;de{Z&&ft(Z,x,h,p),G&&Vt(h,p,x,"updated")},S)},z=(p,h,x,S,k,A,F)=>{for(let y=0;y{if(h!==x){if(h!==me)for(const A in h)!gs(A)&&!(A in x)&&r(p,A,h[A],null,k,S);for(const A in x){if(gs(A))continue;const F=x[A],y=h[A];F!==y&&A!=="value"&&r(p,A,y,F,k,S)}"value"in x&&r(p,"value",h.value,x.value,k)}},U=(p,h,x,S,k,A,F,y,E)=>{const P=h.el=p?p.el:u(""),G=h.anchor=p?p.anchor:u("");let{patchFlag:j,dynamicChildren:V,slotScopeIds:Z}=h;Z&&(y=y?y.concat(Z):Z),p==null?(n(P,x,S),n(G,x,S),ve(h.children||[],x,G,k,A,F,y,E)):j>0&&j&64&&V&&p.dynamicChildren&&p.dynamicChildren.length===V.length?(z(p.dynamicChildren,V,x,k,A,F,y),(h.key!=null||k&&h===k.subTree)&&wi(p,h,!0)):ce(p,h,x,G,k,A,F,y,E)},ue=(p,h,x,S,k,A,F,y,E)=>{h.slotScopeIds=y,p==null?h.shapeFlag&512?k.ctx.activate(h,x,S,F,E):we(h,x,S,k,A,F,E):be(p,h,E)},we=(p,h,x,S,k,A,F)=>{const y=p.component=Vu(p,S,k);if(vn(p)&&(y.ctx.renderer=jt),Bu(y,!1,F),y.asyncDep){if(k&&k.registerDep(y,Ce,F),!p.el){const E=y.subTree=Ae(Ve);M(null,E,h,x),p.placeholder=E.el}}else Ce(y,p,h,x,k,A,F)},be=(p,h,x)=>{const S=h.component=p.component;if(wu(p,h,x))if(S.asyncDep&&!S.asyncResolved){ne(S,h,x);return}else S.next=h,S.update();else h.el=p.el,S.vnode=h},Ce=(p,h,x,S,k,A,F)=>{const y=()=>{if(p.isMounted){let{next:j,bu:V,u:Z,parent:se,vnode:de}=p;{const Qe=Ko(p);if(Qe){j&&(j.el=de.el,ne(p,j,F)),Qe.asyncDep.then(()=>{p.isUnmounted||y()});return}}let oe=j,Ee;Bt(p,!1),j?(j.el=de.el,ne(p,j,F)):j=de,V&&zs(V),(Ee=j.props&&j.props.onVnodeBeforeUpdate)&&ft(Ee,se,j,de),Bt(p,!0);const Re=rl(p),qe=p.subTree;p.subTree=Re,g(qe,Re,b(qe.el),ut(qe),p,k,A),j.el=Re.el,oe===null&&Cu(p,Re.el),Z&&je(Z,k),(Ee=j.props&&j.props.onVnodeUpdated)&&je(()=>ft(Ee,se,j,de),k)}else{let j;const{el:V,props:Z}=h,{bm:se,m:de,parent:oe,root:Ee,type:Re}=p,qe=ws(h);Bt(p,!1),se&&zs(se),!qe&&(j=Z&&Z.onVnodeBeforeMount)&&ft(j,oe,h),Bt(p,!0);{Ee.ce&&Ee.ce._def.shadowRoot!==!1&&Ee.ce._injectChildStyle(Re);const Qe=p.subTree=rl(p);g(null,Qe,x,S,p,k,A),h.el=Qe.el}if(de&&je(de,k),!qe&&(j=Z&&Z.onVnodeMounted)){const Qe=h;je(()=>ft(j,oe,Qe),k)}(h.shapeFlag&256||oe&&ws(oe.vnode)&&oe.vnode.shapeFlag&256)&&p.a&&je(p.a,k),p.isMounted=!0,h=x=S=null}};p.scope.on();const E=p.effect=new ql(y);p.scope.off();const P=p.update=E.run.bind(E),G=p.job=E.runIfDirty.bind(E);G.i=p,G.id=p.uid,E.scheduler=()=>bi(G),Bt(p,!0),P()},ne=(p,h,x)=>{h.component=p;const S=p.vnode.props;p.vnode=h,p.next=null,ku(p,h.props,S,x),Iu(p,h.children,x),kt(),qi(p),At()},ce=(p,h,x,S,k,A,F,y,E=!1)=>{const P=p&&p.children,G=p?p.shapeFlag:0,j=h.children,{patchFlag:V,shapeFlag:Z}=h;if(V>0){if(V&128){Ie(P,j,x,S,k,A,F,y,E);return}else if(V&256){Me(P,j,x,S,k,A,F,y,E);return}}Z&8?(G&16&&nt(P,k,A),j!==P&&m(x,j)):G&16?Z&16?Ie(P,j,x,S,k,A,F,y,E):nt(P,k,A,!0):(G&8&&m(x,""),Z&16&&ve(j,x,S,k,A,F,y,E))},Me=(p,h,x,S,k,A,F,y,E)=>{p=p||os,h=h||os;const P=p.length,G=h.length,j=Math.min(P,G);let V;for(V=0;VG?nt(p,k,A,!0,!1,j):ve(h,x,S,k,A,F,y,E,j)},Ie=(p,h,x,S,k,A,F,y,E)=>{let P=0;const G=h.length;let j=p.length-1,V=G-1;for(;P<=j&&P<=V;){const Z=p[P],se=h[P]=E?$t(h[P]):mt(h[P]);if(zt(Z,se))g(Z,se,x,null,k,A,F,y,E);else break;P++}for(;P<=j&&P<=V;){const Z=p[j],se=h[V]=E?$t(h[V]):mt(h[V]);if(zt(Z,se))g(Z,se,x,null,k,A,F,y,E);else break;j--,V--}if(P>j){if(P<=V){const Z=V+1,se=ZV)for(;P<=j;)Fe(p[P],k,A,!0),P++;else{const Z=P,se=P,de=new Map;for(P=se;P<=V;P++){const Le=h[P]=E?$t(h[P]):mt(h[P]);Le.key!=null&&de.set(Le.key,P)}let oe,Ee=0;const Re=V-se+1;let qe=!1,Qe=0;const Ut=new Array(Re);for(P=0;P=Re){Fe(Le,k,A,!0);continue}let ze;if(Le.key!=null)ze=de.get(Le.key);else for(oe=se;oe<=V;oe++)if(Ut[oe-se]===0&&zt(Le,h[oe])){ze=oe;break}ze===void 0?Fe(Le,k,A,!0):(Ut[ze-se]=P+1,ze>=Qe?Qe=ze:qe=!0,g(Le,h[ze],x,null,k,A,F,y,E),Ee++)}const Ls=qe?Nu(Ut):os;for(oe=Ls.length-1,P=Re-1;P>=0;P--){const Le=se+P,ze=h[Le],Xt=h[Le+1],ps=Le+1{const{el:A,type:F,transition:y,children:E,shapeFlag:P}=p;if(P&6){We(p.component.subTree,h,x,S);return}if(P&128){p.suspense.move(h,x,S);return}if(P&64){F.move(p,h,x,jt);return}if(F===ie){n(A,h,x);for(let j=0;jy.enter(A),k);else{const{leave:j,delayLeave:V,afterLeave:Z}=y,se=()=>{p.ctx.isUnmounted?i(A):n(A,h,x)},de=()=>{A._isLeaving&&A[_t](!0),j(A,()=>{se(),Z&&Z()})};V?V(A,se,de):de()}else n(A,h,x)},Fe=(p,h,x,S=!1,k=!1)=>{const{type:A,props:F,ref:y,children:E,dynamicChildren:P,shapeFlag:G,patchFlag:j,dirs:V,cacheIndex:Z}=p;if(j===-2&&(k=!1),y!=null&&(kt(),_s(y,null,x,p,!0),At()),Z!=null&&(h.renderCache[Z]=void 0),G&256){h.ctx.deactivate(p);return}const se=G&1&&V,de=!ws(p);let oe;if(de&&(oe=F&&F.onVnodeBeforeUnmount)&&ft(oe,h,p),G&6)gt(p.component,x,S);else{if(G&128){p.suspense.unmount(x,S);return}se&&Vt(p,null,h,"beforeUnmount"),G&64?p.type.remove(p,h,x,jt,S):P&&!P.hasOnce&&(A!==ie||j>0&&j&64)?nt(P,h,x,!1,!0):(A===ie&&j&384||!k&&G&16)&&nt(E,h,x),S&&Je(p)}(de&&(oe=F&&F.onVnodeUnmounted)||se)&&je(()=>{oe&&ft(oe,h,p),se&&Vt(p,null,h,"unmounted")},x)},Je=p=>{const{type:h,el:x,anchor:S,transition:k}=p;if(h===ie){Y(x,S);return}if(h===Js){_(p);return}const A=()=>{i(x),k&&!k.persisted&&k.afterLeave&&k.afterLeave()};if(p.shapeFlag&1&&k&&!k.persisted){const{leave:F,delayLeave:y}=k,E=()=>F(x,A);y?y(p.el,A,E):E()}else A()},Y=(p,h)=>{let x;for(;p!==h;)x=w(p),i(p),p=x;i(h)},gt=(p,h,x)=>{const{bum:S,scope:k,job:A,subTree:F,um:y,m:E,a:P}=p;cl(E),cl(P),S&&zs(S),k.stop(),A&&(A.flags|=8,Fe(F,p,h,x)),y&&je(y,h),je(()=>{p.isUnmounted=!0},h)},nt=(p,h,x,S=!1,k=!1,A=0)=>{for(let F=A;F{if(p.shapeFlag&6)return ut(p.component.subTree);if(p.shapeFlag&128)return p.suspense.next();const h=w(p.anchor||p.el),x=h&&h[wo];return x?w(x):h};let Ht=!1;const ct=(p,h,x)=>{let S;p==null?h._vnode&&(Fe(h._vnode,null,null,!0),S=h._vnode.component):g(h._vnode||null,p,h,null,null,null,x),h._vnode=p,Ht||(Ht=!0,qi(S),ho(),Ht=!1)},jt={p:g,um:Fe,m:We,r:Je,mt:we,mc:ve,pc:ce,pbc:z,n:ut,o:e};return{render:ct,hydrate:void 0,createApp:hu(ct)}}function Fn({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function Bt({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Ru(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function wi(e,t,s=!1){const n=e.children,i=t.children;if(K(n)&&K(i))for(let r=0;r>1,e[s[u]]0&&(t[n]=s[r-1]),s[r]=n)}}for(r=s.length,a=s[r-1];r-- >0;)s[r]=a,a=t[a];return s}function Ko(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Ko(t)}function cl(e){if(e)for(let t=0;te.__isSuspense;function $u(e,t){t&&t.pendingBranch?K(e)?t.effects.push(...e):t.effects.push(e):Va(e)}const ie=Symbol.for("v-fgt"),bn=Symbol.for("v-txt"),Ve=Symbol.for("v-cmt"),Js=Symbol.for("v-stc"),Ss=[];let Ze=null;function R(e=!1){Ss.push(Ze=e?null:[])}function Du(){Ss.pop(),Ze=Ss[Ss.length-1]||null}let Is=1;function on(e,t=!1){Is+=e,e<0&&Ze&&t&&(Ze.hasOnce=!0)}function Yo(e){return e.dynamicChildren=Is>0?Ze||os:null,Du(),Is>0&&Ze&&Ze.push(e),e}function $(e,t,s,n,i,r){return Yo(o(e,t,s,n,i,r,!0))}function yn(e,t,s,n,i){return Yo(Ae(e,t,s,n,i,!0))}function rn(e){return e?e.__v_isVNode===!0:!1}function zt(e,t){return e.type===t.type&&e.key===t.key}const Jo=({key:e})=>e??null,qs=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?_e(e)||De(e)||Q(e)?{i:tt,r:e,k:t,f:!!s}:e:null);function o(e,t=null,s=null,n=0,i=null,r=e===ie?0:1,a=!1,u=!1){const d={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Jo(t),ref:t&&qs(t),scopeId:bo,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:r,patchFlag:n,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:tt};return u?(Ci(d,s),r&128&&e.normalize(d)):s&&(d.shapeFlag|=_e(s)?8:16),Is>0&&!a&&Ze&&(d.patchFlag>0||r&6)&&d.patchFlag!==32&&Ze.push(d),d}const Ae=Fu;function Fu(e,t=null,s=null,n=0,i=null,r=!1){if((!e||e===au)&&(e=Ve),rn(e)){const u=Ot(e,t,!0);return s&&Ci(u,s),Is>0&&!r&&Ze&&(u.shapeFlag&6?Ze[Ze.indexOf(e)]=u:Ze.push(u)),u.patchFlag=-2,u}if(zu(e)&&(e=e.__vccOpts),t){t=Lu(t);let{class:u,style:d}=t;u&&!_e(u)&&(t.class=Se(u)),pe(d)&&(gi(d)&&!K(d)&&(d=Pe({},d)),t.style=Jt(d))}const a=_e(e)?1:Zo(e)?128:Co(e)?64:pe(e)?4:Q(e)?2:0;return o(e,t,s,n,i,a,r,!0)}function Lu(e){return e?gi(e)||jo(e)?Pe({},e):e:null}function Ot(e,t,s=!1,n=!1){const{props:i,ref:r,patchFlag:a,children:u,transition:d}=e,v=t?Hu(i||{},t):i,m={__v_isVNode:!0,__v_skip:!0,type:e.type,props:v,key:v&&Jo(v),ref:t&&t.ref?s&&r?K(r)?r.concat(qs(t)):[r,qs(t)]:qs(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:u,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ie?a===-1?16:a|16:a,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:d,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Ot(e.ssContent),ssFallback:e.ssFallback&&Ot(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return d&&n&&Ms(m,d.clone(m)),m}function Ye(e=" ",t=0){return Ae(bn,null,e,t)}function Ou(e,t){const s=Ae(Js,null,e);return s.staticCount=t,s}function Te(e="",t=!1){return t?(R(),yn(Ve,null,e)):Ae(Ve,null,e)}function mt(e){return e==null||typeof e=="boolean"?Ae(Ve):K(e)?Ae(ie,null,e.slice()):rn(e)?$t(e):Ae(bn,null,String(e))}function $t(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Ot(e)}function Ci(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(K(t))s=16;else if(typeof t=="object")if(n&65){const i=t.default;i&&(i._c&&(i._d=!1),Ci(e,i()),i._c&&(i._d=!0));return}else{s=32;const i=t._;!i&&!jo(t)?t._ctx=tt:i===3&&tt&&(tt.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else Q(t)?(t={default:t,_ctx:tt},s=32):(t=String(t),n&64?(s=16,t=[Ye(t)]):s=8);e.children=t,e.shapeFlag|=s}function Hu(...e){const t={};for(let s=0;sBe||tt;let an,ni;{const e=fn(),t=(s,n)=>{let i;return(i=e[s])||(i=e[s]=[]),i.push(n),r=>{i.length>1?i.forEach(a=>a(r)):i[0](r)}};an=t("__VUE_INSTANCE_SETTERS__",s=>Be=s),ni=t("__VUE_SSR_SETTERS__",s=>Ts=s)}const Fs=e=>{const t=Be;return an(e),e.scope.on(),()=>{e.scope.off(),an(t)}},dl=()=>{Be&&Be.scope.off(),an(null)};function Qo(e){return e.vnode.shapeFlag&4}let Ts=!1;function Bu(e,t=!1,s=!1){t&&ni(t);const{props:n,children:i}=e.vnode,r=Qo(e);Su(e,n,r,t),Mu(e,i,s||t);const a=r?Wu(e,t):void 0;return t&&ni(!1),a}function Wu(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,uu);const{setup:n}=s;if(n){kt();const i=e.setupContext=n.length>1?Ku(e):null,r=Fs(e),a=Ds(n,e,0,[e.props,i]),u=Bl(a);if(At(),r(),(u||e.sp)&&!ws(e)&&Eo(e),u){if(a.then(dl,dl),t)return a.then(d=>{fl(e,d)}).catch(d=>{mn(d,e,0)});e.asyncDep=a}else fl(e,a)}else Xo(e)}function fl(e,t,s){Q(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:pe(t)&&(e.setupState=po(t)),Xo(e)}function Xo(e,t,s){const n=e.type;e.render||(e.render=n.render||vt);{const i=Fs(e);kt();try{cu(e)}finally{At(),i()}}}const Gu={get(e,t){return $e(e,"get",""),e[t]}};function Ku(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,Gu),slots:e.slots,emit:e.emit,expose:t}}function xn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(po(Ra(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Cs)return Cs[s](e)},has(t,s){return s in t||s in Cs}})):e.proxy}function zu(e){return Q(e)&&"__vccOpts"in e}const B=(e,t)=>La(e,t,Ts);function Zu(e,t,s){try{on(-1);const n=arguments.length;return n===2?pe(t)&&!K(t)?rn(t)?Ae(e,null,[t]):Ae(e,t):Ae(e,null,t):(n>3?s=Array.prototype.slice.call(arguments,2):n===3&&rn(s)&&(s=[s]),Ae(e,t,s))}finally{on(1)}}const Yu="3.5.26";/** +* @vue/runtime-dom v3.5.26 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ii;const pl=typeof window<"u"&&window.trustedTypes;if(pl)try{ii=pl.createPolicy("vue",{createHTML:e=>e})}catch{}const er=ii?e=>ii.createHTML(e):e=>e,Ju="http://www.w3.org/2000/svg",qu="http://www.w3.org/1998/Math/MathML",xt=typeof document<"u"?document:null,ml=xt&&xt.createElement("template"),Qu={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const i=t==="svg"?xt.createElementNS(Ju,e):t==="mathml"?xt.createElementNS(qu,e):s?xt.createElement(e,{is:s}):xt.createElement(e);return e==="select"&&n&&n.multiple!=null&&i.setAttribute("multiple",n.multiple),i},createText:e=>xt.createTextNode(e),createComment:e=>xt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>xt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,i,r){const a=s?s.previousSibling:t.lastChild;if(i&&(i===r||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),s),!(i===r||!(i=i.nextSibling)););else{ml.innerHTML=er(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const u=ml.content;if(n==="svg"||n==="mathml"){const d=u.firstChild;for(;d.firstChild;)u.appendChild(d.firstChild);u.removeChild(d)}t.insertBefore(u,s)}return[a?a.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Et="transition",vs="animation",Es=Symbol("_vtc"),tr={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Xu=Pe({},Ao,tr),ec=e=>(e.displayName="Transition",e.props=Xu,e),tc=ec((e,{slots:t})=>Zu(qa,sc(e),t)),Wt=(e,t=[])=>{K(e)?e.forEach(s=>s(...t)):e&&e(...t)},vl=e=>e?K(e)?e.some(t=>t.length>1):e.length>1:!1;function sc(e){const t={};for(const U in e)U in tr||(t[U]=e[U]);if(e.css===!1)return t;const{name:s="v",type:n,duration:i,enterFromClass:r=`${s}-enter-from`,enterActiveClass:a=`${s}-enter-active`,enterToClass:u=`${s}-enter-to`,appearFromClass:d=r,appearActiveClass:v=a,appearToClass:m=u,leaveFromClass:b=`${s}-leave-from`,leaveActiveClass:w=`${s}-leave-active`,leaveToClass:C=`${s}-leave-to`}=e,O=nc(i),g=O&&O[0],N=O&&O[1],{onBeforeEnter:M,onEnter:D,onEnterCancelled:L,onLeave:_,onLeaveCancelled:W,onBeforeAppear:J=M,onAppear:le=D,onAppearCancelled:ve=L}=t,H=(U,ue,we,be)=>{U._enterCancelled=be,Gt(U,ue?m:u),Gt(U,ue?v:a),we&&we()},z=(U,ue)=>{U._isLeaving=!1,Gt(U,b),Gt(U,C),Gt(U,w),ue&&ue()},ee=U=>(ue,we)=>{const be=U?le:D,Ce=()=>H(ue,U,we);Wt(be,[ue,Ce]),hl(()=>{Gt(ue,U?d:r),yt(ue,U?m:u),vl(be)||gl(ue,n,g,Ce)})};return Pe(t,{onBeforeEnter(U){Wt(M,[U]),yt(U,r),yt(U,a)},onBeforeAppear(U){Wt(J,[U]),yt(U,d),yt(U,v)},onEnter:ee(!1),onAppear:ee(!0),onLeave(U,ue){U._isLeaving=!0;const we=()=>z(U,ue);yt(U,b),U._enterCancelled?(yt(U,w),xl(U)):(xl(U),yt(U,w)),hl(()=>{U._isLeaving&&(Gt(U,b),yt(U,C),vl(_)||gl(U,n,N,we))}),Wt(_,[U,we])},onEnterCancelled(U){H(U,!1,void 0,!0),Wt(L,[U])},onAppearCancelled(U){H(U,!0,void 0,!0),Wt(ve,[U])},onLeaveCancelled(U){z(U),Wt(W,[U])}})}function nc(e){if(e==null)return null;if(pe(e))return[Ln(e.enter),Ln(e.leave)];{const t=Ln(e);return[t,t]}}function Ln(e){return na(e)}function yt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.add(s)),(e[Es]||(e[Es]=new Set)).add(t)}function Gt(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.remove(n));const s=e[Es];s&&(s.delete(t),s.size||(e[Es]=void 0))}function hl(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let ic=0;function gl(e,t,s,n){const i=e._endId=++ic,r=()=>{i===e._endId&&n()};if(s!=null)return setTimeout(r,s);const{type:a,timeout:u,propCount:d}=lc(e,t);if(!a)return n();const v=a+"end";let m=0;const b=()=>{e.removeEventListener(v,w),r()},w=C=>{C.target===e&&++m>=d&&b()};setTimeout(()=>{m(s[O]||"").split(", "),i=n(`${Et}Delay`),r=n(`${Et}Duration`),a=bl(i,r),u=n(`${vs}Delay`),d=n(`${vs}Duration`),v=bl(u,d);let m=null,b=0,w=0;t===Et?a>0&&(m=Et,b=a,w=r.length):t===vs?v>0&&(m=vs,b=v,w=d.length):(b=Math.max(a,v),m=b>0?a>v?Et:vs:null,w=m?m===Et?r.length:d.length:0);const C=m===Et&&/\b(?:transform|all)(?:,|$)/.test(n(`${Et}Property`).toString());return{type:m,timeout:b,propCount:w,hasTransform:C}}function bl(e,t){for(;e.lengthyl(s)+yl(e[n])))}function yl(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function xl(e){return(e?e.ownerDocument:document).body.offsetHeight}function oc(e,t,s){const n=e[Es];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const _l=Symbol("_vod"),rc=Symbol("_vsh"),ac=Symbol(""),uc=/(?:^|;)\s*display\s*:/;function cc(e,t,s){const n=e.style,i=_e(s);let r=!1;if(s&&!i){if(t)if(_e(t))for(const a of t.split(";")){const u=a.slice(0,a.indexOf(":")).trim();s[u]==null&&Qs(n,u,"")}else for(const a in t)s[a]==null&&Qs(n,a,"");for(const a in s)a==="display"&&(r=!0),Qs(n,a,s[a])}else if(i){if(t!==s){const a=n[ac];a&&(s+=";"+a),n.cssText=s,r=uc.test(s)}}else t&&e.removeAttribute("style");_l in e&&(e[_l]=r?n.display:"",e[rc]&&(n.display="none"))}const wl=/\s*!important$/;function Qs(e,t,s){if(K(s))s.forEach(n=>Qs(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=dc(e,t);wl.test(s)?e.setProperty(qt(n),s.replace(wl,""),"important"):e[n]=s}}const Cl=["Webkit","Moz","ms"],On={};function dc(e,t){const s=On[t];if(s)return s;let n=Lt(t);if(n!=="filter"&&n in e)return On[t]=n;n=Kl(n);for(let i=0;iHn||(vc.then(()=>Hn=0),Hn=Date.now());function gc(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;at(bc(n,s.value),t,5,[n])};return s.value=e,s.attached=hc(),s}function bc(e,t){if(K(t)){const s=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{s.call(e),e._stopped=!0},t.map(n=>i=>!i._stopped&&n&&n(i))}else return t}const Il=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,yc=(e,t,s,n,i,r)=>{const a=i==="svg";t==="class"?oc(e,n,a):t==="style"?cc(e,s,n):un(t)?oi(t)||pc(e,t,s,n,r):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):xc(e,t,n,a))?(Al(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&kl(e,t,n,a,r,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!_e(n))?Al(e,Lt(t),n,r,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),kl(e,t,n,a))};function xc(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Il(t)&&Q(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return Il(t)&&_e(s)?!1:t in e}const ds=e=>{const t=e.props["onUpdate:modelValue"]||!1;return K(t)?s=>zs(t,s):t};function _c(e){e.target.composing=!0}function Tl(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const St=Symbol("_assign");function El(e,t,s){return t&&(e=e.trim()),s&&(e=dn(e)),e}const he={created(e,{modifiers:{lazy:t,trim:s,number:n}},i){e[St]=ds(i);const r=n||i.props&&i.props.type==="number";Dt(e,t?"change":"input",a=>{a.target.composing||e[St](El(e.value,s,r))}),(s||r)&&Dt(e,"change",()=>{e.value=El(e.value,s,r)}),t||(Dt(e,"compositionstart",_c),Dt(e,"compositionend",Tl),Dt(e,"change",Tl))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:i,number:r}},a){if(e[St]=ds(a),e.composing)return;const u=(r||e.type==="number")&&!/^0\d/.test(e.value)?dn(e.value):e.value,d=t??"";u!==d&&(document.activeElement===e&&e.type!=="range"&&(n&&t===s||i&&e.value.trim()===d)||(e.value=d))}},jn={deep:!0,created(e,t,s){e[St]=ds(s),Dt(e,"change",()=>{const n=e._modelValue,i=Rs(e),r=e.checked,a=e[St];if(K(n)){const u=ui(n,i),d=u!==-1;if(r&&!d)a(n.concat(i));else if(!r&&d){const v=[...n];v.splice(u,1),a(v)}}else if(fs(n)){const u=new Set(n);r?u.add(i):u.delete(i),a(u)}else a(sr(e,r))})},mounted:Rl,beforeUpdate(e,t,s){e[St]=ds(s),Rl(e,t,s)}};function Rl(e,{value:t,oldValue:s},n){e._modelValue=t;let i;if(K(t))i=ui(t,n.props.value)>-1;else if(fs(t))i=t.has(n.props.value);else{if(t===s)return;i=$s(t,sr(e,!0))}e.checked!==i&&(e.checked=i)}const Oe={deep:!0,created(e,{value:t,modifiers:{number:s}},n){const i=fs(t);Dt(e,"change",()=>{const r=Array.prototype.filter.call(e.options,a=>a.selected).map(a=>s?dn(Rs(a)):Rs(a));e[St](e.multiple?i?new Set(r):r:r[0]),e._assigning=!0,ls(()=>{e._assigning=!1})}),e[St]=ds(n)},mounted(e,{value:t}){Nl(e,t)},beforeUpdate(e,t,s){e[St]=ds(s)},updated(e,{value:t}){e._assigning||Nl(e,t)}};function Nl(e,t){const s=e.multiple,n=K(t);if(!(s&&!n&&!fs(t))){for(let i=0,r=e.options.length;iString(v)===String(u)):a.selected=ui(t,u)>-1}else a.selected=t.has(u);else if($s(Rs(a),t)){e.selectedIndex!==i&&(e.selectedIndex=i);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Rs(e){return"_value"in e?e._value:e.value}function sr(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const wc=["ctrl","shift","alt","meta"],Cc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>wc.some(s=>e[`${s}Key`]&&!t.includes(s))},Gs=(e,t)=>{const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=(i,...r)=>{for(let a=0;a{const t=kc().createApp(...e),{mount:s}=t;return t.mount=n=>{const i=Mc(n);if(!i)return;const r=t._component;!Q(r)&&!r.render&&!r.template&&(r.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const a=s(i,!1,Pc(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),a},t};function Pc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Mc(e){return _e(e)?document.querySelector(e):e}const Ic="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZjFmMWYiIC8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNlYmRiYjIiIGZvbnQtZmFtaWx5PSJTcGFjZSBNb25vLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjAiPkFNPC90ZXh0Pgo8L3N2Zz4K",Tc="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZjFmMWYiIC8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNlYmRiYjIiIGZvbnQtZmFtaWx5PSJTcGFjZSBNb25vLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjAiPkNEPC90ZXh0Pgo8L3N2Zz4K",Ec="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZjFmMWYiIC8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNlYmRiYjIiIGZvbnQtZmFtaWx5PSJTcGFjZSBNb25vLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjAiPkFFPC90ZXh0Pgo8L3N2Zz4K",Rc="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZjFmMWYiIC8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNlYmRiYjIiIGZvbnQtZmFtaWx5PSJTcGFjZSBNb25vLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMjAiPkJNPC90ZXh0Pgo8L3N2Zz4K",Nc={amazon:Ic,cdiscount:Tc,aliexpress:Ec,backmarket:Rc},$c=e=>e?e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""):"",Dc=e=>{const t=$c(e);return Nc[t]||null},Fc={class:"w-full h-12 mb-2",viewBox:"0 0 120 40",preserveAspectRatio:"none"},Lc=["points"],Oc=["cx","cy"],Hc={class:"grid grid-cols-2 gap-3 text-[0.75rem]"},jc=Qt({__name:"PriceHistoryHover",props:{visible:{type:Boolean,default:!1},position:{type:Object,default:()=>({top:0,left:0})},history:{type:Array,default:()=>[]},currentPrice:{type:Number,default:0},minPrice:{type:Number,default:0},maxPrice:{type:Number,default:0},delta:{type:Number,default:0}},emits:["mouseenter","mouseleave"],setup(e,{emit:t}){const s=e,n=B(()=>{const w=Number(s.delta??0);return!Number.isFinite(w)||w===0?"—":`${w>0?"▲":"▼"} ${Math.abs(w).toFixed(1)}%`}),i=B(()=>{if(!s.history.length)return"0,30 30,20 60,15 90,25 120,20";const w=Math.max(...s.history),C=Math.min(...s.history),O=w-C||1;return s.history.map((g,N)=>{const M=N/(s.history.length-1||1)*120,D=40-(g-C)/O*40;return`${M.toFixed(1)},${D.toFixed(1)}`}).join(" ")}),r=B(()=>{if(!s.history.length)return[];const w=Math.max(...s.history),C=Math.min(...s.history),O=w-C||1;return s.history.map((g,N)=>{const M=N/(s.history.length-1||1)*120,D=40-(g-C)/O*40;return{cx:M.toFixed(1),cy:D.toFixed(1)}})}),a=t,u=B(()=>({position:"fixed",top:`${s.position.top}px`,left:`${s.position.left}px`,width:"280px",zIndex:50})),d=n,v=w=>Number.isFinite(w)?`${w.toFixed(2)} €`:"n/a";function m(){a("mouseenter")}function b(){a("mouseleave")}return(w,C)=>e.visible?(R(),yn(Za,{key:0,to:"body"},[o("div",{class:"price-history-popup panel p-4 shadow-lg",style:Jt(u.value),onMouseenter:m,onMouseleave:b},[C[8]||(C[8]=o("div",{class:"flex items-center justify-between mb-2"},[o("div",{class:"section-title text-sm"},"Historique 30j")],-1)),(R(),$("svg",Fc,[o("polyline",{points:i.value,class:"sparkline",fill:"none"},null,8,Lc),(R(!0),$(ie,null,xe(r.value,(O,g)=>(R(),$("circle",{key:`history-point-${g}`,cx:O.cx,cy:O.cy,r:"1.2",stroke:"currentColor",fill:"currentColor"},null,8,Oc))),128))])),o("div",Hc,[o("div",null,[C[0]||(C[0]=Ye("Actuel",-1)),C[1]||(C[1]=o("br",null,null,-1)),o("strong",null,I(v(e.currentPrice)),1)]),o("div",null,[C[2]||(C[2]=Ye("Min",-1)),C[3]||(C[3]=o("br",null,null,-1)),o("strong",null,I(v(e.minPrice)),1)]),o("div",null,[C[4]||(C[4]=Ye("Max",-1)),C[5]||(C[5]=o("br",null,null,-1)),o("strong",null,I(v(e.maxPrice)),1)]),o("div",null,[C[6]||(C[6]=Ye("Delta",-1)),C[7]||(C[7]=o("br",null,null,-1)),o("strong",null,I(fo(d)),1)])])],36)])):Te("",!0)}}),Uc={class:"price-history-chart panel p-3"},Vc={class:"flex items-center justify-between mb-2"},Bc={class:"label text-xs"},Wc={class:"w-full h-20 mb-2",viewBox:"0 0 120 50",preserveAspectRatio:"none"},Gc=["points"],Kc=["cx","cy"],zc={class:"grid grid-cols-2 gap-3 text-xs"},Zc=Qt({__name:"PriceHistoryChart",props:{history:{type:Array,default:()=>[]},currentPrice:{type:Number,default:0},minPrice:{type:Number,default:0},maxPrice:{type:Number,default:0},deltaLabel:{type:String,default:"—"}},setup(e){const t=e,s=B(()=>{if(!t.history.length)return"0,40 30,30 60,35 90,25 120,28";const r=Math.max(...t.history),a=Math.min(...t.history),u=r-a||1;return t.history.map((d,v)=>{const m=v/(t.history.length-1||1)*120,b=50-(d-a)/u*50;return`${m.toFixed(1)},${b.toFixed(1)}`}).join(" ")}),n=B(()=>{if(!t.history.length)return[];const r=Math.max(...t.history),a=Math.min(...t.history),u=r-a||1;return t.history.map((d,v)=>{const m=v/(t.history.length-1||1)*120,b=50-(d-a)/u*50;return{cx:m.toFixed(1),cy:b.toFixed(1)}})}),i=r=>Number.isFinite(r)?`${r.toFixed(2)} €`:"n/a";return(r,a)=>(R(),$("div",Uc,[o("div",Vc,[a[0]||(a[0]=o("div",{class:"section-title text-sm"},"Historique",-1)),o("div",Bc,I(e.deltaLabel),1)]),(R(),$("svg",Wc,[o("polyline",{points:s.value,class:"sparkline",fill:"none"},null,8,Gc),(R(!0),$(ie,null,xe(n.value,(u,d)=>(R(),$("circle",{key:`history-detail-point-${d}`,cx:u.cx,cy:u.cy,r:"1.3",stroke:"currentColor",fill:"currentColor"},null,8,Kc))),128))])),o("div",zc,[o("div",null,[a[1]||(a[1]=Ye("Actuel",-1)),a[2]||(a[2]=o("br",null,null,-1)),o("strong",null,I(i(e.currentPrice)),1)]),o("div",null,[a[3]||(a[3]=Ye("Min",-1)),a[4]||(a[4]=o("br",null,null,-1)),o("strong",null,I(i(e.minPrice)),1)]),o("div",null,[a[5]||(a[5]=Ye("Max",-1)),a[6]||(a[6]=o("br",null,null,-1)),o("strong",null,I(i(e.maxPrice)),1)]),o("div",null,[a[7]||(a[7]=Ye("Delta",-1)),a[8]||(a[8]=o("br",null,null,-1)),o("strong",null,I(e.deltaLabel),1)])])]))}}),Yc={class:"mini-line-chart-wrapper"},Jc=["width","height","viewBox"],qc=["x1","x2","y1","y2"],Qc=["x1","x2","y1","y2"],Xc=["x1","x2","y1","y2"],ed=["x","y"],td=["x1","x2","y1","y2"],sd=["x","y"],nd=["points"],id=["cx","cy"],ld={key:1,class:"history-placeholder","aria-hidden":"true"},od=Qt({__name:"MiniLineChart",props:{points:{type:Array,default:()=>[]},width:{type:Number,default:280},height:{type:Number,default:140},yTicks:{type:Number,default:4},xTicks:{type:Number,default:4},formatY:{type:Function,default:e=>new Intl.NumberFormat("fr-FR",{style:"currency",currency:"EUR",maximumFractionDigits:0}).format(e)},formatX:{type:Function,default:e=>String(e)}},setup(e){const t=e,s={left:44,right:8,top:8,bottom:22},n=B(()=>t.points.map(M=>{const D=Number(M.v);return{t:M.t,v:Number.isFinite(D)?D:NaN}}).filter(M=>Number.isFinite(M.v))),i=B(()=>n.value.every(M=>{if(typeof M.t=="number")return!0;const D=Date.parse(String(M.t));return!Number.isNaN(D)})),r=B(()=>n.value.map(M=>{if(typeof M.t=="number")return M.t;const D=Date.parse(String(M.t));return Number.isNaN(D)?null:D})),a=B(()=>{if(!n.value.length)return{min:0,max:0};const M=n.value.map(J=>J.v),D=Math.min(...M),L=Math.max(...M),W=Math.max(L-D,1)*.05;return{min:D-W,max:L+W}}),u=B(()=>({width:t.width-s.left-s.right,height:t.height-s.top-s.bottom})),d=B(()=>{const M=n.value;if(M.length===0)return[];const{min:D,max:L}=a.value,_=L-D||1,{width:W,height:J}=u.value,le=i.value&&r.value.some(H=>H!==null)?r.value.reduce((H,z)=>(z===null||(H.min=H.min===null?z:Math.min(H.min,z),H.max=H.max===null?z:Math.max(H.max,z)),H),{min:null,max:null}):{min:null,max:null},ve=r.value.map((H,z)=>{if(i.value&&H!==null&&le.min!==null&&le.max!==null){const ee=Math.max(le.max-le.min,1);return(H-le.min)/ee}return M.length>1?z/(M.length-1):0});return M.map((H,z)=>{const ee=s.left+W*ve[z],U=1-(H.v-D)/_,ue=s.top+J*U;return{x:ee,y:ue,value:H.v,raw:H.t}})}),v=B(()=>d.value.length>0),m=B(()=>{if(!d.value.length)return[];if(d.value.length===1){const M=d.value[0],D=s.left+u.value.width;return[{x:s.left,y:M.y},{x:D,y:M.y}]}return d.value}),b=B(()=>m.value.map(M=>`${M.x},${M.y}`).join(" ")),w=B(()=>{const M=Math.max(2,t.yTicks),{min:D,max:L}=a.value,_=(L-D)/(M-1||1);return Array.from({length:M},(W,J)=>({value:D+_*J,position:s.top+u.value.height*(1-(D+_*J-D)/(L-D||1))}))}),C=B(()=>{const M=d.value,D=Math.max(2,Math.min(M.length,t.xTicks));return M.length?Array.from({length:D},(L,_)=>{const W=Math.round((M.length-1)*(_/(D-1||1)));return M[W]}):[]}),O=B(()=>C.value.map(M=>({label:t.formatX(M.raw),x:M.x}))),g=B(()=>w.value.map(M=>({label:t.formatY(M.value),y:M.position}))),N=B(()=>"");return(M,D)=>(R(),$("div",Yc,[v.value?(R(),$("svg",{key:0,width:e.width,height:e.height,viewBox:`0 0 ${e.width} ${e.height}`,role:"presentation","aria-hidden":"true"},[o("line",{x1:s.left,x2:s.left,y1:s.top,y2:s.top+u.value.height,stroke:"currentColor","stroke-width":"1",opacity:"0.35"},null,8,qc),o("line",{x1:s.left,x2:s.left+u.value.width,y1:s.top+u.value.height,y2:s.top+u.value.height,stroke:"currentColor","stroke-width":"1",opacity:"0.35"},null,8,Qc),(R(!0),$(ie,null,xe(g.value,L=>(R(),$("g",{key:L.label},[o("line",{x1:s.left-6,x2:s.left,y1:L.y,y2:L.y,stroke:"currentColor","stroke-width":"1",opacity:"0.35"},null,8,Xc),o("text",{x:s.left-10,y:L.y+4,class:"text-[10px]","text-anchor":"end",opacity:.65},I(L.label),9,ed)]))),128)),(R(!0),$(ie,null,xe(O.value,L=>(R(),$("g",{key:L.label},[o("line",{x1:L.x,x2:L.x,y1:s.top+u.value.height,y2:s.top+u.value.height+6,stroke:"currentColor","stroke-width":"1",opacity:"0.35"},null,8,td),o("text",{x:L.x,y:e.height-4,class:"text-[10px]","text-anchor":"middle",opacity:.65},I(L.label),9,sd)]))),128)),o("polyline",{points:b.value,stroke:"currentColor",fill:"none","stroke-width":"2"},null,8,nd),(R(!0),$(ie,null,xe(d.value,(L,_)=>(R(),$("circle",{key:`${L.x}-${L.y}-${_}`,cx:L.x,cy:L.y,r:"2",stroke:"currentColor","stroke-width":"1",fill:"currentColor",class:Se({"mini-line-chart__point--last":_===d.value.length-1})},null,10,id))),128))],8,Jc)):(R(),$("div",ld,I(N.value),1))]))}}),rd={class:"price-block__current"},ad={key:0,class:"price-block__msrp"},ud={key:1,class:"price-block__discount"},cd={key:2,class:"price-block__ref"},dd=Qt({__name:"PriceBlock",props:{price:{},currency:{},msrp:{},discountAmount:{},discountPercent:{},stockStatus:{},reference:{},url:{},compact:{type:Boolean}},setup(e){const t=e,s=(d,v)=>d==null||!Number.isFinite(d)?"—":new Intl.NumberFormat("fr-FR",{style:"currency",currency:v||"EUR",minimumFractionDigits:0,maximumFractionDigits:0}).format(d),n=B(()=>s(t.price,t.currency)),i=B(()=>t.msrp?s(t.msrp,t.currency):null),r=B(()=>{if(t.discountAmount===null||t.discountAmount===void 0||t.discountPercent===null||t.discountPercent===void 0)return null;const d=s(t.discountAmount,t.currency);return`-${Math.round(t.discountPercent)}% (${d})`}),a=B(()=>({in_stock:"En stock",out_of_stock:"Rupture",unknown:"Inconnu",error:"Erreur"})[t.stockStatus]||t.stockStatus),u=B(()=>t.stockStatus==="in_stock"?"text-[var(--success)]":t.stockStatus==="out_of_stock"?"text-[var(--danger)]":"text-[var(--muted)]");return(d,v)=>(R(),$("div",{class:Se(["price-block",{"price-block--compact":e.compact}])},[o("div",rd,I(n.value),1),i.value?(R(),$("div",ad,I(i.value),1)):Te("",!0),r.value?(R(),$("div",ud,I(r.value),1)):Te("",!0),o("div",{class:Se(["price-block__stock",u.value])},I(a.value),3),e.reference&&!e.compact?(R(),$("div",cd," Ref: "+I(e.reference),1)):Te("",!0)],2))}}),Si=(e,t)=>{const s=e.__vccOpts||e;for(const[n,i]of t)s[n]=i;return s},fd=Si(dd,[["__scopeId","data-v-f8f63757"]]),pd={class:"card-actions"},md=Qt({__name:"CardActions",props:{productId:{},compareIds:{},showSecondary:{type:Boolean}},emits:["refresh","compare","edit","delete","open"],setup(e,{emit:t}){const s=t;function n(d){d.stopPropagation(),s("refresh")}function i(d){d.stopPropagation(),s("compare")}function r(d){d.stopPropagation(),s("edit")}function a(d){d.stopPropagation(),s("delete")}function u(d){d.stopPropagation(),s("open")}return(d,v)=>(R(),$("div",pd,[o("button",{class:"card-actions__btn card-actions__btn--primary",title:"Rafraichir","aria-label":"Rafraichir le produit",onClick:n},[...v[0]||(v[0]=[o("i",{class:"fa-solid fa-rotate"},null,-1)])]),o("button",{class:"card-actions__btn",title:"Modifier","aria-label":"Modifier le produit",onClick:r},[...v[1]||(v[1]=[o("i",{class:"fa-solid fa-pen"},null,-1)])]),o("button",{class:"card-actions__btn",title:"Supprimer","aria-label":"Supprimer le produit",onClick:a},[...v[2]||(v[2]=[o("i",{class:"fa-solid fa-trash"},null,-1)])]),o("button",{class:"card-actions__btn",title:"Ouvrir","aria-label":"Ouvrir dans un nouvel onglet",onClick:u},[...v[3]||(v[3]=[o("i",{class:"fa-solid fa-up-right-from-square"},null,-1)])]),o("button",{class:Se(["card-actions__btn",{"card-actions__btn--active":e.compareIds.includes(e.productId)}]),title:"Comparer","aria-label":"Comparer le produit",onClick:i},[o("i",{class:Se(["fa-solid",e.compareIds.includes(e.productId)?"fa-square-check":"fa-code-compare"])},null,2)],2)]))}}),vd=Si(md,[["__scopeId","data-v-d4ee6168"]]),hd={class:"product-card__header"},gd={class:"product-card__identity"},bd={class:"product-card__store-icon"},yd=["src"],xd={key:1,class:"product-card__store-initials"},_d={class:"product-card__identity-text"},wd=["title"],Cd={class:"product-card__store-name"},Sd={class:"product-card__thumbnail"},kd={key:0},Ad=["srcset"],Pd=["srcset"],Md=["src"],Id=["src"],Td={class:"product-card__price-zone"},Ed={class:"product-card__history-zone"},Rd={key:1,class:"product-card__no-history"},Nd={class:"product-card__history-stats"},$d={class:"product-card__stat"},Dd={class:"product-card__stat-value"},Fd={class:"product-card__stat"},Ld={class:"product-card__stat-value"},Od={class:"product-card__stat"},Hd={class:"product-card__trend-delta"},jd={class:"product-card__update"},Ud=Qt({__name:"ProductCard",props:{product:{},historyData:{},compareIds:{},storeLogo:{},storeLabel:{},storeInitials:{},chartPeriodLabel:{},imageMode:{},placeholderImage:{}},emits:["click","refresh","compare","edit","delete","open","hover","leave"],setup(e,{emit:t}){const s=e,n=t,i=(g,N)=>g==null||!Number.isFinite(g)?"—":new Intl.NumberFormat("fr-FR",{style:"currency",currency:N||"EUR",minimumFractionDigits:0,maximumFractionDigits:0}).format(g),r=g=>{let N=null;if(typeof g=="number")N=g;else if(typeof g=="string"){const L=Date.parse(g);Number.isNaN(L)||(N=L)}if(N===null)return typeof g=="string"?g:"";const D=Math.max(0,Math.round((Date.now()-N)/864e5));return D===0?"J":`J-${D}`},a=g=>{if(g===null||!Number.isFinite(g))return"a l instant";const N=Date.now()-g;if(N<6e4)return"a l instant";const M=Math.floor(N/6e4);if(M<60)return`il y a ${M} min`;const D=Math.floor(N/36e5);return D<24?`il y a ${D} h`:`il y a ${Math.floor(N/864e5)} j`},u=B(()=>({"product-card":!0,"product-card--accent":s.product.delta<0})),d=B(()=>s.product.imageJpg||s.product.imageWebp||s.placeholderImage),v=B(()=>!!(s.product.imageWebp||s.product.imageJpg)),m=B(()=>{var N;const g=(N=s.historyData)==null?void 0:N.lastTimestamp;if(g)return a(g);if(s.product.updatedAt){const M=Date.parse(s.product.updatedAt);return Number.isNaN(M)?s.product.updatedAt:a(M)}return"—"});function b(){n("click")}function w(g){(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),n("click"))}function C(g){n("hover",g)}function O(){n("leave")}return(g,N)=>{var M,D,L,_,W;return R(),$("article",{class:Se(u.value),role:"button",tabindex:"0",onClick:b,onKeydown:w,onMouseenter:C,onMouseleave:O,onFocusin:C,onFocusout:O},[o("div",hd,[o("div",gd,[o("div",bd,[e.storeLogo?(R(),$("img",{key:0,src:e.storeLogo,alt:""},null,8,yd)):(R(),$("span",xd,I(e.storeInitials),1))]),o("div",_d,[o("h3",{class:"product-card__title",title:e.product.title},I(e.product.title),9,wd),o("div",Cd,I(e.storeLabel),1)])])]),o("div",Sd,[v.value?(R(),$("picture",kd,[e.product.imageWebp?(R(),$("source",{key:0,srcset:e.product.imageWebp,type:"image/webp"},null,8,Ad)):Te("",!0),e.product.imageJpg?(R(),$("source",{key:1,srcset:e.product.imageJpg,type:"image/jpeg"},null,8,Pd)):Te("",!0),o("img",{src:d.value,class:Se(["product-card__image",`product-card__image--${e.imageMode}`]),alt:"Image produit",loading:"lazy"},null,10,Md)])):(R(),$("img",{key:1,src:e.placeholderImage,class:"product-card__image product-card__image--contain",alt:"Image indisponible",loading:"lazy"},null,8,Id))]),o("div",Td,[Ae(fd,{price:e.product.price,currency:e.product.currency,msrp:e.product.msrp,"discount-amount":e.product.discountAmount,"discount-percent":e.product.discountPercent,"stock-status":e.product.stockStatus,reference:e.product.reference,compact:""},null,8,["price","currency","msrp","discount-amount","discount-percent","stock-status","reference"])]),o("div",Ed,[o("div",{class:"product-card__chart-container",style:Jt({color:((M=e.historyData)==null?void 0:M.trendColor)||"var(--muted)"})},[e.historyData&&e.historyData.points.length>0?(R(),yn(od,{key:0,points:e.historyData.points,height:120,formatY:J=>i(J,e.product.currency),formatX:r,yTicks:3,xTicks:4},null,8,["points","formatY"])):(R(),$("div",Rd," Pas d'historique "))],4),o("div",Nd,[o("div",$d,[N[5]||(N[5]=o("span",{class:"product-card__stat-label"},"Min",-1)),o("span",Dd,I(e.historyData&&e.historyData.min!==null?i(e.historyData.min,e.product.currency):"—"),1)]),o("div",Fd,[N[6]||(N[6]=o("span",{class:"product-card__stat-label"},"Max",-1)),o("span",Ld,I(e.historyData&&e.historyData.max!==null?i(e.historyData.max,e.product.currency):"—"),1)]),o("div",Od,[N[7]||(N[7]=o("span",{class:"product-card__stat-label"},"Tendance",-1)),o("span",{class:"product-card__stat-value product-card__trend",style:Jt({color:((D=e.historyData)==null?void 0:D.trendColor)||"var(--muted)"})},[Ye(I(((L=e.historyData)==null?void 0:L.trendIcon)||"→")+" "+I(((_=e.historyData)==null?void 0:_.trendLabel)||"—")+" ",1),o("span",Hd,I(((W=e.historyData)==null?void 0:W.trendDeltaLabel)||"—"),1)],4)])]),o("div",jd," Derniere maj: "+I(m.value)+" ("+I(e.storeLabel)+") ",1)]),Ae(vd,{"product-id":e.product.id,"compare-ids":e.compareIds,onRefresh:N[0]||(N[0]=J=>n("refresh")),onCompare:N[1]||(N[1]=J=>n("compare")),onEdit:N[2]||(N[2]=J=>n("edit")),onDelete:N[3]||(N[3]=J=>n("delete")),onOpen:N[4]||(N[4]=J=>n("open"))},null,8,["product-id","compare-ids"])],34)}}}),Vd=Si(Ud,[["__scopeId","data-v-3a31b9af"]]),Bd=({rect:e,popupWidth:t,popupHeight:s,viewportWidth:n,viewportHeight:i,margin:r=8})=>{const a=i-e.bottom,u=e.top,d=r;let v=e.bottom+d;a=s+d&&(v=e.top-s-d),aNo image",qm=864e5,Qm=Qt({__name:"App",setup(e){const t=te("gruvbox-dark"),s=te("dense"),n=te("cards"),i=te("auto"),r=te(!1),a=te(!1),u=te(!1),d=te(!1),v=te("frontend"),m=te(""),b=[{id:"compact",label:"Compact",value:.85},{id:"standard",label:"Standard",value:1},{id:"etendu",label:"Étendu",value:1.2}],w=[{value:30,label:"30 jours"},{value:90,label:"90 jours"},{value:180,label:"180 jours"}],C=[{value:"contain",label:"Contain (par défaut)"},{value:"cover",label:"Cover"}],O={info:"▶",success:"✔",warn:"⚠",error:"✖"},g=te({apiBase:"/api",apiToken:"",refreshMinutes:60,pageSize:24,preferWebp:!0,iconSize:"md",showDebug:!1,enableAnimations:!0,fontTitle:"space-mono",fontBody:"jetbrains",fontMono:"jetbrains",fontSize:16,showHistoryHover:!0,cardRatio:Ks,cardColumns:Dl,cardImageHeight:Un,chartPeriod:w[0].value,imageMode:"contain",logDuration:Ol,trendThresholdDown:Bn,trendThresholdUp:Wn}),N=te(Ym),M=B(()=>`${N.value}px`),D=te(null),L=te({visible:!1,messages:[],hideTimer:null,animating:!1}),_=te({store:"all",stock:"all",priceMin:null,priceMax:null,sort:"updated",sortOrder:"desc"}),W=te({loading:!1,error:""}),J=te({db:!1,redis:!1}),le=te("n/a"),ve=[{id:"amazon",name:"Amazon",domain:"amazon.fr"},{id:"cdiscount",name:"Cdiscount",domain:"cdiscount.com"},{id:"backmarket",name:"Backmarket",domain:"backmarket.fr"}],H=[{id:"7",label:"7j",points:10,days:7},{id:"30",label:"30j",points:24,days:30},{id:"90",label:"90j",points:40,days:90},{id:"max",label:"Max",points:60,days:365}],z=te(jl),ee=te(Ul),U=te(null),ue=te(null);let we=!1;const be=te([...ve]),Ce=B(()=>Object.fromEntries(be.value.map(c=>[c.id,c.name]))),ne=te([]),ce=te({}),Me=te([]),Ie=te(null),We=te(""),Fe=te(""),Je=te({url:"",usePlaywright:!1}),Y=te({loading:!1,error:"",snapshot:null}),gt=te([]),nt=te([]),ut=te([]),Ht=te([]),ct=[{id:"jetbrains",label:"JetBrains Mono",stack:'"JetBrains Mono", "Fira Code", "IBM Plex Mono", "SFMono-Regular", Menlo, monospace'},{id:"space-mono",label:"Space Mono",stack:'"Space Mono", "JetBrains Mono", "Fira Code", monospace'},{id:"ibm-plex",label:"IBM Plex Mono",stack:'"IBM Plex Mono", "JetBrains Mono", monospace'}],jt=B(()=>`theme-${t.value}`),ki=B(()=>`density-${s.value}`),p=B(()=>`layout-${i.value}`),h=B(()=>{const c=l=>{var f;return(f=ct.find(T=>T.id===l))==null?void 0:f.stack};return{"--font-title":c(g.value.fontTitle)||ct[0].stack,"--font-body":c(g.value.fontBody)||ct[0].stack,"--font-mono":c(g.value.fontMono)||ct[0].stack,"--font-size":`${g.value.fontSize||16}px`}}),x={in_stock:"En stock",out_of_stock:"Rupture",unknown:"Inconnu",error:"Erreur scrape",in_stocked:"En stock"},S=te({visible:!1,position:{top:0,left:0},history:[],productId:null,currentPrice:0,minPrice:0,maxPrice:0,delta:0});te(null);const k=te(null),A=te(!1),F=B(()=>ne.value.filter(c=>!(_.value.store!=="all"&&c.storeId!==_.value.store||_.value.stock!=="all"&&c.stockStatus!==_.value.stock||_.value.priceMin&&(c.price===null||c.price===void 0||c.price<_.value.priceMin)||_.value.priceMax&&(c.price===null||c.price===void 0||c.price>_.value.priceMax)||m.value&&!c.title.toLowerCase().includes(m.value.toLowerCase()))).sort((c,l)=>{const f=_.value.sortOrder==="asc"?1:-1;return _.value.sort==="price"?f*((c.price??Number.MAX_SAFE_INTEGER)-(l.price??Number.MAX_SAFE_INTEGER)):_.value.sort==="delta"?f*(c.delta-l.delta):_.value.sort==="stock"?f*c.stockStatus.localeCompare(l.stockStatus):f*c.updatedAt.localeCompare(l.updatedAt)})),y=B(()=>ne.value.find(c=>c.id===Ie.value)),E=B(()=>g.value.showHistoryHover&&S.value.visible),P=B(()=>{var c,l;return!!((l=(c=y.value)==null?void 0:c.description)!=null&&l.trim())}),G=B(()=>{var l;const c=(l=y.value)==null?void 0:l.specs;return c?Object.keys(c).length>0:!1}),j=B(()=>{var l;const c=(l=y.value)==null?void 0:l.specs;return c?Object.entries(c):[]}),V=B(()=>{var dt,Ne;const c=H.find(Tt=>Tt.id===z.value)??H[1],f=kn(((dt=y.value)==null?void 0:dt.history)??[],c.points,c.days??w[1].value).map(Tt=>Tt.price),T=f.length?f[f.length-1]:Number(((Ne=y.value)==null?void 0:Ne.price)??NaN),X=f.length?Math.min(...f):T,fe=f.length?Math.max(...f):T,ke=f.length?f[0]:T,Xe=Number.isFinite(ke)&&ke>0?(T-ke)/ke*100:0;return{history:f,minPrice:X,maxPrice:fe,currentPrice:Number.isFinite(T)?T:0,delta:Xe}}),Z=B(()=>or(V.value.delta)),se=B(()=>({current:V.value.currentPrice,min:V.value.minPrice,max:V.value.maxPrice,delta:V.value.delta})),de=B(()=>ne.value.filter(c=>Me.value.includes(c.id))),oe=B(()=>ne.value.length),Ee=B(()=>{const c=[],l=_.value.store;l&&l!=="all"&&c.push({key:"store",label:`Store: ${ts(l)}`,tooltip:"Retirer le filtre store"});const f=_.value.stock;if(f&&f!=="all"){const T={in_stock:"En stock",out_of_stock:"Rupture",unknown:"Inconnu"};c.push({key:"stock",label:`Stock: ${T[f]??f}`,tooltip:"Retirer le filtre stock"})}return _.value.priceMin&&c.push({key:"priceMin",label:`Min: ${_.value.priceMin}`,tooltip:"Retirer le minimum"}),_.value.priceMax&&c.push({key:"priceMax",label:`Max: ${_.value.priceMax}`,tooltip:"Retirer le maximum"}),m.value.trim()&&c.push({key:"search",label:`Recherche: ${m.value.trim()}`,tooltip:"Retirer la recherche"}),c}),Re=B(()=>{const c=Number(g.value.cardRatio??Ks);if(!Number.isFinite(c))return`Standard (${Ks.toFixed(2)})`;const l=b.find(T=>Math.abs(T.value-c)<.03);return`${l?l.label:"Personnalisé"} • ${c.toFixed(2)}`}),qe=B(()=>`${g.value.cardColumns} colonnes`),Qe=B(()=>`${Math.round(g.value.cardImageHeight??Un)}px`),Ut=B(()=>{const c=Number(g.value.chartPeriod??w[0].value),l=w.find(f=>f.value===c);return l?l.label:`${c} jours`}),Ls=B(()=>{const c=new Map;return ne.value.forEach(l=>{c.set(l.id,Cr(l))}),c}),Le=B(()=>!Y.value.snapshot||!Y.value.snapshot.images?0:Y.value.snapshot.images.length),ze=B(()=>!Y.value.snapshot||!Array.isArray(Y.value.snapshot.images)?[]:Y.value.snapshot.images.filter(Boolean)),Xt=B(()=>ze.value[0]||Gn),ps=B(()=>ze.value.slice(0,4)),nr=B(()=>new Set(gt.value));function ir(c){if(gt.value.includes(c)){gt.value=gt.value.filter(f=>f!==c);return}gt.value=[...gt.value,c]}const lr=B(()=>!Y.value.snapshot||!Y.value.snapshot.specs?0:Object.keys(Y.value.snapshot.specs).length),Ai=B(()=>!Y.value.snapshot||!Y.value.snapshot.specs?[]:Object.entries(Y.value.snapshot.specs).slice(0,10));if(typeof window<"u"){const c=Number(window.localStorage.getItem(Hl));Number.isFinite(c)&&(N.value=Math.max(16,Math.min(40,c)))}const Pi=B(()=>W.value.loading?"API...":W.value.error?"API ERR":"API OK"),_n=B(()=>v.value==="backend"?ut.value.map((c,l)=>({id:`backend-${l}`,time:c.time,level:c.level,message:c.message,source:"backend"})):v.value==="uvicorn"?Ht.value.map((c,l)=>({id:`uvicorn-${l}`,time:"",level:"INFO",message:c.line,source:"uvicorn"})):v.value==="errors"?nt.value.filter(c=>c.level==="ERROR"):nt.value);function es(c,l="EUR"){return c==null?"n/a":`${c} ${l??"EUR"}`}function Os(c,l="EUR"){return c==null?"n/a":new Intl.NumberFormat("fr-FR",{style:"currency",currency:l??"EUR",minimumFractionDigits:2}).format(c)}function or(c){if(c==null||Number.isNaN(c))return"—";const l=Number(c);if(l===0)return"—";const f=l>0?"▲":"▼",T=Math.abs(l).toFixed(1).replace(/\.0$/,"");return`${f} ${T}%`}function rr(c){return c?x[c]||c.replaceAll("_"," ").replace(/\b\w/g,l=>l.toUpperCase()):x.unknown}function ar(c,l){return c==null||l===null||l===void 0?"n/a":`${c.toFixed(2)} (-${l.toFixed(1)}%)`}function ts(c){return Ce.value[c]||c||"Inconnu"}function wn(){a.value=!a.value}function Cn(){d.value=!d.value,d.value&&Gi()}function ur(){Je.value={url:"",usePlaywright:!1},Y.value={loading:!1,error:"",snapshot:null},u.value=!0}function Mi(){u.value=!1}function Sn(){typeof window<"u"&&window.location.reload()}function cr(){typeof window<"u"&&(localStorage.removeItem("pw_webui_settings"),localStorage.removeItem("pw_webui_product_meta"),localStorage.removeItem("pw_webui_stores"),window.location.reload())}function dr(){ji(),Ui(),Vi(),Sn()}function fr(){const c=["gruvbox-dark","gruvbox-light","monokai-dark","monokai-light"],l=c.indexOf(t.value);t.value=c[(l+1)%c.length]}function Ii(c){Ie.value=c}function Ti(c){Ii(c)}function Ei(c){if(Me.value.includes(c)){Me.value=Me.value.filter(l=>l!==c);return}Me.value.length>=4||Me.value.push(c)}function pr(c){if(c==="store"){_.value.store="all";return}if(c==="stock"){_.value.stock="all";return}if(c==="priceMin"){_.value.priceMin=null;return}if(c==="priceMax"){_.value.priceMax=null;return}c==="search"&&(m.value="")}function mr(){_.value.store="all",_.value.stock="all",_.value.priceMin=null,_.value.priceMax=null,_.value.sort="updated",_.value.sortOrder="desc",m.value=""}function vr(){if(!We.value.trim())return;const c=We.value.toLowerCase().replace(/\s+/g,"-");be.value.some(l=>l.id===c)||(be.value.push({id:c,name:We.value.trim(),domain:Fe.value.trim()||"custom"}),We.value="",Fe.value="")}function hr(c){be.value=be.value.filter(l=>l.id!==c)}function gr(c){const l=new Set(be.value.map(T=>T.id)),f=[];c.forEach(T=>{T.storeId&&!l.has(T.storeId)&&(l.add(T.storeId),f.push({id:T.storeId,name:T.storeId.toUpperCase(),domain:"custom"}))}),f.length&&(be.value=[...be.value,...f])}function Ri(){if(!U.value)return[];const c=U.value.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])');return Array.from(c).filter(l=>l.offsetParent!==null||l===document.activeElement)}function br(){var l;(l=Ri()[0])==null||l.focus()}function Ni(c){if(!Ie.value)return;if(c.key==="Escape"){Ie.value=null,c.preventDefault();return}if(c.key!=="Tab"||!U.value)return;const l=Ri();if(!l.length)return;const f=l[0],T=l[l.length-1];c.shiftKey&&document.activeElement===f?(c.preventDefault(),T.focus()):!c.shiftKey&&document.activeElement===T&&(c.preventDefault(),f.focus())}function yr(){typeof document>"u"||we||(document.addEventListener("keydown",Ni),we=!0)}function $i(){typeof document>"u"||!we||(document.removeEventListener("keydown",Ni),we=!1)}Ge(()=>y.value,c=>{c&&(ee.value=P.value?"description":G.value?"specs":Ul,z.value=jl)}),Ge(()=>Ie.value,c=>{c?(typeof document<"u"&&(ue.value=document.activeElement),yr(),ls(()=>{br()})):($i(),ls(()=>{var l;(l=ue.value)==null||l.focus()}))}),nn(()=>{$i()});function Di(c){if(!c)return null;if(typeof c=="number")return c;if(typeof c=="string"){const l=Date.parse(c);return Number.isNaN(l)?null:l}return null}function xr(c=[]){return Array.isArray(c)?c.map(l=>{if(!l||typeof l!="object")return null;const f=l,T=Number(f.price);if(!Number.isFinite(T))return null;const X=Di(f.fetched_at??f.fetchedAt??f.timestamp??f.time);return Number.isFinite(X??NaN)?{price:T,timestamp:X}:null}).filter(l=>!!l).sort((l,f)=>l.timestamp-f.timestamp):[]}function kn(c=[],l=Vn,f=w[0].value){if(!Array.isArray(c)||l<=0)return[];const T=xr(c);if(!T.length)return[];const X=Date.now()-Math.max(0,f)*qm,fe=T.filter(Xe=>Xe.timestamp>=X),ke=fe.length?fe:T;return ke.length<=l?ke:ke.slice(-l)}function _r(c){return c.map(l=>({t:l.timestamp,v:l.price}))}function wr(c){if(!Number.isFinite(c))return"var(--muted)";const l=Number(g.value.trendThresholdDown??Bn),f=Number(g.value.trendThresholdUp??Wn);return c<=l?"var(--success)":c>=f?"var(--danger)":"var(--warning)"}function Cr(c){const l=Number(g.value.chartPeriod??w[0].value),f=kn(c.history??[],Vn,l),T=_r(f),X=f.map(Qr=>Qr.price),fe=X.length?Math.min(...X):null,ke=X.length?Math.max(...X):null,Xe=X.length?X[0]:Number(c.price??NaN),dt=X.length?X[X.length-1]:Number(c.price??NaN),Ne=Number.isFinite(Xe)&&Xe>0&&Number.isFinite(dt)?(dt-Xe)/Xe*100:NaN,Tt=Number(g.value.trendThresholdDown??Bn),Ki=Number(g.value.trendThresholdUp??Wn),Zr=Ne<=Tt?"↘":Ne>=Ki?"↗":"→",Yr=Ne<=Tt?"baisse":Ne>=Ki?"hausse":"stable",Jr=Number.isFinite(Ne)?`${Ne>=0?"+":""}${Ne.toFixed(1)}%`:"—",qr=f.length>0?f[f.length-1].timestamp:Di(c.updatedAt)??null;return{points:T,min:fe,max:ke,delta:Number.isFinite(Ne)?Ne:null,trendIcon:Zr,trendLabel:Yr,trendDeltaLabel:Jr,trendColor:wr(Number.isFinite(Ne)?Ne:NaN),lastTimestamp:qr}}function Sr(c){if(!c||typeof window>"u")return;const l=c.getBoundingClientRect(),f=Bd({rect:l,popupWidth:280,popupHeight:140,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight});S.value.position=f}function kr(c,l){if(!l)return;const f=Number(g.value.chartPeriod??w[0].value),T=kn(c.history??[],Vn,f);if(!T.length){Fi();return}const X=T.map(Tt=>Tt.price),fe=X[X.length-1],ke=Math.min(...X),Xe=Math.max(...X),dt=X[0],Ne=Number.isFinite(dt)&&dt>0?(fe-dt)/dt*100:0;S.value.history=X,S.value.currentPrice=Number.isFinite(fe)?fe:0,S.value.minPrice=ke,S.value.maxPrice=Xe,S.value.delta=Ne,S.value.productId=c.id,S.value.visible=!0,ls(()=>Sr(l)),A.value=!0,k.value&&(window.clearTimeout(k.value),k.value=null)}function Fi(){A.value||(S.value.visible=!1)}function Li(c=120){k.value&&window.clearTimeout(k.value),k.value=window.setTimeout(()=>{A.value||Fi()},c)}function Ar(c,l){const f=l.currentTarget;kr(c,f)}function Pr(){A.value=!0,k.value&&(window.clearTimeout(k.value),k.value=null)}function Mr(){A.value=!1,Li(0)}function Ir(c){if(typeof document>"u")return;const l=Math.max(16,Math.min(40,c));document.documentElement.style.setProperty("--pw-store-icon",`${l}px`)}function Tr(c){if(typeof document>"u")return;const l=Number(c),f=Number.isFinite(l)?l:Ks,T=Math.min(1.3,Math.max(.75,f)),X=Math.min(T,1.1);document.documentElement.style.setProperty("--pw-card-height-factor",`${T}`),document.documentElement.style.setProperty("--pw-card-mobile-height-factor",`${X}`)}function Er(c){if(typeof document>"u")return;const l=Number(c),f=Number.isFinite(l)?l:Un,T=Math.min(220,Math.max(120,f));document.documentElement.style.setProperty("--pw-card-media-height",`${T}px`)}function Rr(c){if(typeof document>"u")return;const l=Number(c),f=Number.isFinite(l)?l:Dl,T=Math.min(Ll,Math.max(Fl,f));document.documentElement.style.setProperty("--pw-card-columns",`${T}`)}function Nr(c){return Dc(c)||void 0}function $r(c){const l=ts(c);return l?l.split(" ").map(f=>f.charAt(0).toUpperCase()).join("").slice(0,2):"NA"}function An(c){if(!c)return"n/a";try{return new Date(c).toLocaleString("fr-FR")}catch{return c}}function it(c){const l=g.value.apiBase||"/api";return l.startsWith("http")?new URL(l.replace(/\/$/,"")+c).toString():`${l.endsWith("/")?l.slice(0,-1):l}${c}`}function lt(){const c={"Content-Type":"application/json"};return g.value.apiToken&&(c.Authorization=`Bearer ${g.value.apiToken}`),c}function Dr(c){const l=c.id,f=ce.value[l]||{},T=c.images,X=Array.isArray(T)&&T.length>0?T[0]:Gn,fe=Array.isArray(c.history)?c.history:[];return{id:l,title:c.title||c.reference||"Sans titre",storeId:c.source||"unknown",reference:c.reference||"",price:c.latest_price??null,delta:0,stockStatus:c.latest_stock_status||"unknown",updatedAt:c.last_updated_at||"",updatedAtLabel:An(c.last_updated_at),refreshHours:f.refreshHours??24,url:c.url||"",category:c.category||"",currency:c.currency||"EUR",description:c.description||"",specs:c.specs||{},msrp:c.msrp??null,discountAmount:c.discount_amount??null,discountPercent:c.discount_percent??null,imageWebp:X,imageJpg:X,images:T||[],history:fe}}async function It(){var c;W.value.loading=!0,W.value.error="";try{const l=new URL(it("/products"),window.location.origin);_.value.store!=="all"&&l.searchParams.set("source",_.value.store),_.value.priceMin&&l.searchParams.set("price_min",String(_.value.priceMin)),_.value.priceMax&&l.searchParams.set("price_max",String(_.value.priceMax)),_.value.stock!=="all"&&l.searchParams.set("stock_status",_.value.stock),g.value.pageSize&&l.searchParams.set("limit",String(g.value.pageSize));const f=await fetch(l.toString(),{headers:lt()});if(!f.ok)throw new Error(`API ${f.status}`);const X=(await f.json()).map(Dr);ne.value=X,gr(X),Ie.value&&!ne.value.find(fe=>fe.id===Ie.value)&&(Ie.value=((c=ne.value[0])==null?void 0:c.id)??null)}catch(l){W.value.error=l instanceof Error?l.message:"Erreur API",ye("ERROR","frontend",W.value.error)}finally{W.value.loading=!1}}async function Fr(){if(!y.value)return;const c={url:y.value.url,title:y.value.title,category:y.value.category,currency:y.value.currency};try{const l=await fetch(it(`/products/${y.value.id}`),{method:"PATCH",headers:lt(),body:JSON.stringify(c)});if(!l.ok)throw new Error(`API ${l.status}`);await It()}catch(l){W.value.error=l instanceof Error?l.message:"Erreur API",ye("ERROR","frontend",W.value.error)}}async function Pn(c){const l=ne.value.find(T=>T.id===c);if(!l||!l.url){ye("WARN","frontend","Impossible de relancer le scrap: URL manquante");return}const f=Date.now();Hs("info","Démarrage scraping Amazon.fr"),Hs("info","Chargement page produit");try{const T=await fetch(it("/enqueue"),{method:"POST",headers:lt(),body:JSON.stringify({url:l.url,save_db:!0})});if(!T.ok)throw new Error(`API ${T.status}`);ye("INFO","backend",`Scrap relancé pour ${l.reference||l.title}`),await It();const X=((Date.now()-f)/1e3).toFixed(1),fe=ne.value.find(Xe=>Xe.id===c),ke=fe?Os(fe.price,fe.currency):"n/a";Hs("success",`Scraping terminé (${X}s) • Prix détecté : ${ke}`)}catch(T){W.value.error=T instanceof Error?T.message:"Erreur API",ye("ERROR","frontend",W.value.error),Hs("error",`${W.value.error}`)}}async function Mn(c){try{const l=await fetch(it(`/products/${c}`),{method:"DELETE",headers:lt()});if(!l.ok)throw new Error(`API ${l.status}`);Ie.value===c&&(Ie.value=null),ye("INFO","backend","Produit supprimé"),await It()}catch(l){W.value.error=l instanceof Error?l.message:"Erreur API",ye("ERROR","backend",W.value.error)}}async function Lr(){y.value&&await Pn(y.value.id)}async function Or(){y.value&&await Mn(y.value.id)}function Oi(c){if(!(c!=null&&c.url)){ye("WARN","frontend","Aucune URL disponible");return}typeof window>"u"||window.open(c.url,"_blank","noopener")}async function Hr(){var c;if(!((c=y.value)!=null&&c.url)){ye("WARN","frontend","Aucune URL à copier");return}if(typeof navigator>"u"||!navigator.clipboard){ye("WARN","frontend","Clipboard non disponible");return}try{await navigator.clipboard.writeText(y.value.url),ye("INFO","frontend","URL copiée")}catch{ye("ERROR","frontend","Erreur copie de l'URL")}}async function jr(){if(Je.value.url){Y.value.loading=!0,Y.value.error="",Y.value.snapshot=null;try{const c=await fetch(it("/scrape/preview"),{method:"POST",headers:lt(),body:JSON.stringify({url:Je.value.url,use_playwright:Je.value.usePlaywright})});if(!c.ok)throw new Error(`API ${c.status}`);const l=await c.json();if(!l.success){Y.value.error=l.error||"Aucune donnee",ye("ERROR","backend",Y.value.error);return}if(!l.snapshot){Y.value.error="Aucune donnee",ye("ERROR","backend",Y.value.error);return}!l.snapshot.title&&!l.snapshot.price&&(Y.value.error="Donnees incompletes"),Y.value.snapshot=l.snapshot;const f=Array.isArray(l.snapshot.images)?l.snapshot.images.length:0;gt.value=f?[0]:[],ye("INFO","backend","Preview scraping OK")}catch(c){Y.value.error=c instanceof Error?c.message:"Erreur API",ye("ERROR","backend",Y.value.error)}finally{Y.value.loading=!1}}}async function Ur(){if(Y.value.snapshot)try{const c=await fetch(it("/scrape/commit"),{method:"POST",headers:lt(),body:JSON.stringify({snapshot:Y.value.snapshot})});if(!c.ok)throw new Error(`API ${c.status}`);u.value=!1,await It(),ye("INFO","backend","Produit enregistre")}catch(c){Y.value.error=c instanceof Error?c.message:"Erreur API",ye("ERROR","backend",Y.value.error)}}let ss=null;function Hi(){ss&&(clearInterval(ss),ss=null);const c=Number(g.value.refreshMinutes);Number.isFinite(c)&&c>0&&(ss=setInterval(It,c*60*1e3))}function ji(){if(typeof window>"u")return;const c={theme:t.value,density:s.value,layoutMode:i.value,settings:g.value,viewMode:n.value,filters:_.value};localStorage.setItem("pw_webui_settings",JSON.stringify(c))}function Vr(){if(typeof window>"u")return;const c=localStorage.getItem("pw_webui_settings");if(c)try{const l=JSON.parse(c);l.theme&&(t.value=l.theme),l.density&&(s.value=l.density),l.layoutMode&&(i.value=l.layoutMode),l.settings&&(g.value={...g.value,...l.settings}),l.viewMode&&(n.value=l.viewMode),l.filters&&(_.value={..._.value,...l.filters})}catch{return}}function Ui(){typeof window>"u"||localStorage.setItem("pw_webui_stores",JSON.stringify(be.value))}function Br(){if(typeof window>"u")return;const c=localStorage.getItem("pw_webui_stores");if(c)try{const l=JSON.parse(c);Array.isArray(l)&&(be.value=l)}catch{return}}function Vi(){if(typeof window>"u")return;const c={};ne.value.forEach(l=>{c[l.id]={refreshHours:l.refreshHours}}),localStorage.setItem("pw_webui_product_meta",JSON.stringify(c))}async function Bi(){try{const c=await fetch(it("/health"),{headers:lt()});if(!c.ok)throw new Error(`API ${c.status}`);const l=await c.json();J.value=l}catch{J.value={db:!1,redis:!1}}}async function Wi(){try{const c=await fetch(it("/version"),{headers:lt()});if(!c.ok)throw new Error(`API ${c.status}`);const l=await c.json();le.value=l.api_version||"n/a"}catch{le.value="n/a"}}async function Gi(){ut.value=[],Ht.value=[];try{const c=new URL(it("/logs"),window.location.origin);c.searchParams.set("limit","10");const l=await fetch(c.toString(),{headers:lt()});if(l.ok){const X=await l.json();ut.value=X.map((fe,ke)=>({time:An(fe.fetched_at),level:(fe.fetch_status||"info").toUpperCase(),message:`${fe.source} ${fe.url} (${fe.fetch_method})`}))}const f=await fetch(it("/logs/backend"),{headers:lt()});if(f.ok){const fe=(await f.json()).map(ke=>({time:An(ke.time),level:ke.level||"INFO",message:ke.message||""}));ut.value=[...fe,...ut.value]}const T=await fetch(it("/logs/uvicorn"),{headers:lt()});if(T.ok){const X=await T.json();Ht.value=X.map(fe=>({line:fe.line||""}))}}catch{ye("ERROR","backend","Impossible de charger les logs")}}async function Wr(){if(typeof navigator>"u"||!navigator.clipboard){ye("WARN","frontend","Clipboard indisponible pour copier les logs");return}const c=_n.value.map(l=>`${l.time} [${l.source}] ${l.level} - ${l.message}`).join(` +`);if(!c){ye("WARN","frontend","Aucun log à copier");return}try{await navigator.clipboard.writeText(c),ye("INFO","frontend","Logs copiés")}catch{ye("ERROR","frontend","Erreur lors de la copie des logs")}}function ye(c,l,f){nt.value.unshift({id:`${Date.now()}-${Math.random().toString(16).slice(2)}`,time:new Date().toLocaleTimeString("fr-FR"),level:c,source:l,message:f}),nt.value.length>200&&nt.value.pop()}function Gr(){typeof window>"u"||L.value.hideTimer&&(window.clearTimeout(L.value.hideTimer),L.value.hideTimer=null)}function Kr(){if(Gr(),typeof window>"u")return;const c=Math.max(500,Number(g.value.logDuration??Ol));L.value.hideTimer=window.setTimeout(()=>{L.value.visible=!1,L.value.hideTimer=null},c)}function Hs(c,l){const f=new Date().toLocaleTimeString("fr-FR"),T={id:`${Date.now()}-${Math.random().toString(16).slice(2)}`,time:f,level:c,text:l},X=[...L.value.messages.slice(-4),T];L.value.messages=X,L.value.visible=!0,ls(()=>{D.value&&(D.value.scrollTop=D.value.scrollHeight)}),Kr()}function zr(){if(typeof window>"u")return;const c=localStorage.getItem("pw_webui_product_meta");if(c)try{const l=JSON.parse(c);l&&typeof l=="object"&&(ce.value=l)}catch{return}}return Vr(),Br(),zr(),Ge([t,s,i,g,n,_],ji,{deep:!0}),Ge(be,Ui,{deep:!0}),Ge(ne,Vi,{deep:!0}),Ge(()=>g.value.refreshMinutes,Hi),Ge([()=>g.value.apiBase,()=>g.value.apiToken,()=>g.value.pageSize,_],()=>{It(),Bi(),Wi()},{deep:!0}),Ge(N,c=>{Ir(c),typeof window<"u"&&window.localStorage.setItem(Hl,String(Math.max(16,Math.min(40,c))))},{immediate:!0}),Ge(()=>g.value.cardRatio,c=>{Tr(c)},{immediate:!0}),Ge(()=>g.value.cardImageHeight,c=>{Er(c)},{immediate:!0}),Ge(()=>g.value.cardColumns,c=>{Rr(c)},{immediate:!0}),yi(()=>{It(),Hi(),Bi(),Wi()}),nn(()=>{ss&&clearInterval(ss),k.value&&window.clearTimeout(k.value),L.value.hideTimer&&window.clearTimeout(L.value.hideTimer)}),(c,l)=>(R(),$("div",{class:Se(["app-root",jt.value,ki.value,p.value]),style:Jt(h.value)},[o("header",Wd,[o("div",Gd,[l[61]||(l[61]=Ou('
PriceWatch
Vintage control deck
',1)),Ae(tc,{name:"scrape-log"},{default:yo(()=>[L.value.visible?(R(),$("div",{key:0,ref_key:"scrapeLogRef",ref:D,class:"scrape-log-bar z-40"},[(R(!0),$(ie,null,xe(L.value.messages,f=>(R(),$("div",{key:f.id,class:"scrape-log-line"},[o("span",Kd,"["+I(f.time)+"]",1),o("span",zd,I(O[f.level]),1),o("span",Zd,I(f.text),1)]))),128))],512)):Te("",!0)]),_:1}),o("div",Yd,[o("div",{class:"label"},"FE v"+I(Jm)),o("div",Jd,"BE v"+I(le.value),1)]),o("div",qd,[q(o("input",{class:"input pr-10",placeholder:"Rechercher","onUpdate:modelValue":l[0]||(l[0]=f=>m.value=f)},null,512),[[he,m.value]]),l[54]||(l[54]=o("i",{class:"fa-solid fa-magnifying-glass absolute right-3 top-3 text-[var(--muted)]"},null,-1))]),o("div",Qd,[o("button",{class:"icon-btn add-product-btn",title:"Ajouter un produit","aria-label":"Ajouter un produit",onClick:ur},[...l[55]||(l[55]=[o("i",{class:"fa-solid fa-plus"},null,-1)])]),o("div",Xd,[o("button",{class:Se(["icon-btn header-icon",{"active-view":n.value==="cards"}]),title:"Vue cartes","aria-label":"Vue cartes",onClick:l[1]||(l[1]=f=>n.value="cards")},[...l[56]||(l[56]=[o("i",{class:"fa-solid fa-border-all"},null,-1)])],2),o("button",{class:Se(["icon-btn header-icon",{"active-view":n.value==="table"}]),title:"Vue tableau","aria-label":"Vue tableau",onClick:l[2]||(l[2]=f=>n.value="table")},[...l[57]||(l[57]=[o("i",{class:"fa-solid fa-table"},null,-1)])],2)]),o("button",{class:"icon-btn header-icon",title:"Parametres","aria-label":"Paramètres",onClick:wn},[...l[58]||(l[58]=[o("i",{class:"fa-solid fa-gear"},null,-1)])]),o("button",{class:"icon-btn header-icon",title:"Recharger","aria-label":"Recharger",onClick:Sn},[...l[59]||(l[59]=[o("i",{class:"fa-solid fa-rotate"},null,-1)])]),o("button",{class:"icon-btn header-icon",title:"Theme","aria-label":"Basculer mode clair/sombre",onClick:fr},[...l[60]||(l[60]=[o("i",{class:"fa-solid fa-circle-half-stroke"},null,-1)])])]),o("div",ef,I(Pi.value),1)])]),o("div",tf,[o("aside",sf,[o("div",nf,[l[74]||(l[74]=o("div",{class:"section-title text-base mb-3"},"Filtres & tri",-1)),o("div",lf,[o("div",of,"Affichés "+I(F.value.length)+" / "+I(oe.value)+" produits",1),o("button",{class:"icon-btn",title:"Réinitialiser","aria-label":"Réinitialiser les filtres",onClick:mr},[...l[62]||(l[62]=[o("i",{class:"fa-solid fa-retweet"},null,-1)])])]),o("div",rf,[(R(!0),$(ie,null,xe(Ee.value,f=>(R(),$("button",{key:f.key,class:"filter-chip",title:f.tooltip,type:"button",onClick:T=>pr(f.key)},[Ye(I(f.label)+" ",1),l[63]||(l[63]=o("i",{class:"fa-solid fa-xmark ml-1"},null,-1))],8,af))),128))]),o("div",uf,[o("div",null,[l[65]||(l[65]=o("div",{class:"label"},"Store",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[3]||(l[3]=f=>_.value.store=f)},[l[64]||(l[64]=o("option",{value:"all"},"Tous",-1)),(R(!0),$(ie,null,xe(be.value,f=>(R(),$("option",{key:f.id,value:f.id},I(f.name),9,cf))),128))],512),[[Oe,_.value.store]])]),o("div",null,[l[67]||(l[67]=o("div",{class:"label"},"Stock",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[4]||(l[4]=f=>_.value.stock=f)},[...l[66]||(l[66]=[o("option",{value:"all"},"Tous",-1),o("option",{value:"in_stock"},"En stock",-1),o("option",{value:"out_of_stock"},"Rupture",-1),o("option",{value:"unknown"},"Inconnu",-1)])],512),[[Oe,_.value.stock]])]),o("div",df,[o("div",null,[l[68]||(l[68]=o("div",{class:"label"},"Prix min",-1)),q(o("input",{class:"input",type:"number","onUpdate:modelValue":l[5]||(l[5]=f=>_.value.priceMin=f),placeholder:"Min"},null,512),[[he,_.value.priceMin,void 0,{number:!0}]])]),o("div",null,[l[69]||(l[69]=o("div",{class:"label"},"Prix max",-1)),q(o("input",{class:"input",type:"number","onUpdate:modelValue":l[6]||(l[6]=f=>_.value.priceMax=f),placeholder:"Max"},null,512),[[he,_.value.priceMax,void 0,{number:!0}]])])]),o("div",null,[l[71]||(l[71]=o("div",{class:"label"},"Tri",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[7]||(l[7]=f=>_.value.sort=f)},[...l[70]||(l[70]=[o("option",{value:"updated"},"Derniere maj",-1),o("option",{value:"price"},"Prix actuel",-1),o("option",{value:"delta"},"Variation",-1),o("option",{value:"stock"},"Stock",-1)])],512),[[Oe,_.value.sort]])]),o("div",null,[l[73]||(l[73]=o("div",{class:"label"},"Ordre du tri",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[8]||(l[8]=f=>_.value.sortOrder=f)},[...l[72]||(l[72]=[o("option",{value:"desc"},"Décroissant",-1),o("option",{value:"asc"},"Croissant",-1)])],512),[[Oe,_.value.sortOrder]])])])]),o("div",ff,[l[76]||(l[76]=o("div",{class:"section-title text-base mb-3"},"Comparaison",-1)),o("div",pf,[o("div",mf,I(Me.value.length)+" selectionnes",1),o("button",{class:"icon-btn",title:"Activer comparaison",onClick:l[9]||(l[9]=f=>r.value=!r.value)},[...l[75]||(l[75]=[o("i",{class:"fa-solid fa-code-compare"},null,-1)])])]),l[77]||(l[77]=o("div",{class:"label mt-3"},"Split view 2-4 produits",-1))])]),o("main",vf,[W.value.error?(R(),$("div",hf,[o("div",gf,"Erreur API: "+I(W.value.error),1)])):Te("",!0),r.value?(R(),$("section",bf,[(R(!0),$(ie,null,xe(de.value,f=>(R(),$("div",{key:f.id,class:"card p-4"},[o("div",yf,[o("div",null,[o("div",xf,I(f.title),1),o("div",_f,I(ts(f.storeId)),1)]),o("div",wf,I(f.stockStatus),1)]),o("div",Cf,I(es(f.price,f.currency)),1),o("div",Sf,"Delta: "+I(f.delta)+"%",1),l[78]||(l[78]=o("div",{class:"mt-3 h-20 rounded-lg bg-[var(--surface-2)]"},null,-1))]))),128))])):n.value==="cards"?(R(),$("section",kf,[(R(!0),$(ie,null,xe(F.value,f=>(R(),yn(Vd,{key:f.id,product:f,"history-data":Ls.value.get(f.id)??null,"compare-ids":Me.value,"store-logo":Nr(f.storeId),"store-label":ts(f.storeId),"store-initials":$r(f.storeId),"chart-period-label":Ut.value,"image-mode":g.value.imageMode??"contain","placeholder-image":Gn,onClick:T=>Ii(f.id),onRefresh:T=>Pn(f.id),onCompare:T=>Ei(f.id),onEdit:T=>Ti(f.id),onDelete:T=>Mn(f.id),onOpen:T=>Oi(f),onHover:T=>Ar(f,T),onLeave:l[10]||(l[10]=T=>Li(0))},null,8,["product","history-data","compare-ids","store-logo","store-label","store-initials","chart-period-label","image-mode","onClick","onRefresh","onCompare","onEdit","onDelete","onOpen","onHover"]))),128))])):(R(),$("section",Af,[l[84]||(l[84]=o("div",{class:"section-title text-base mb-3"},"Tableau dense",-1)),o("div",Pf,[o("table",Mf,[l[83]||(l[83]=o("thead",{class:"text-left text-[var(--muted)]"},[o("tr",null,[o("th",{class:"pb-2"},"Produit"),o("th",{class:"pb-2"},"Store"),o("th",{class:"pb-2"},"Prix"),o("th",{class:"pb-2"},"Delta"),o("th",{class:"pb-2"},"Stock"),o("th",{class:"pb-2"},"Actions")])],-1)),o("tbody",null,[(R(!0),$(ie,null,xe(F.value,f=>(R(),$("tr",{key:f.id,class:"border-t border-white/5"},[o("td",If,I(f.title),1),o("td",Tf,I(ts(f.storeId)),1),o("td",Ef,I(es(f.price,f.currency)),1),o("td",Rf,I(f.delta)+"%",1),o("td",Nf,I(f.stockStatus),1),o("td",$f,[o("div",Df,[o("button",{class:"icon-btn",title:"Detail",onClick:T=>Ti(f.id)},[...l[79]||(l[79]=[o("i",{class:"fa-solid fa-eye"},null,-1)])],8,Ff),o("button",{class:"icon-btn",title:"Comparer",onClick:T=>Ei(f.id)},[o("i",{class:Se(["fa-solid",Me.value.includes(f.id)?"fa-square-check":"fa-square"])},null,2)],8,Lf),o("button",{class:"icon-btn",title:"Supprimer",onClick:T=>Mn(f.id)},[...l[80]||(l[80]=[o("i",{class:"fa-solid fa-trash"},null,-1)])],8,Of),o("button",{class:"icon-btn",title:"Relancer le scrap",onClick:T=>Pn(f.id)},[...l[81]||(l[81]=[o("i",{class:"fa-solid fa-rotate"},null,-1)])],8,Hf),o("button",{class:"icon-btn",title:"Ouvrir dans un nouvel onglet",onClick:T=>Oi(f)},[...l[82]||(l[82]=[o("i",{class:"fa-solid fa-up-right-from-square"},null,-1)])],8,jf)])])]))),128))])])])]))])]),a.value?(R(),$("div",{key:0,class:"fixed inset-0 bg-black/40 z-50",onClick:Gs(wn,["self"])},[o("div",Uf,[o("div",{class:"flex items-center justify-between mb-4"},[l[86]||(l[86]=o("div",{class:"section-title text-lg"},"Parametres",-1)),o("button",{class:"icon-btn",title:"Fermer",onClick:wn},[...l[85]||(l[85]=[o("i",{class:"fa-solid fa-xmark"},null,-1)])])]),o("div",Vf,[o("section",null,[l[110]||(l[110]=o("div",{class:"section-title text-sm mb-2"},"Apparence",-1)),o("div",Bf,[o("div",null,[l[88]||(l[88]=o("div",{class:"label"},"Theme",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[11]||(l[11]=f=>t.value=f)},[...l[87]||(l[87]=[o("option",{value:"gruvbox-dark"},"Gruvbox Dark",-1),o("option",{value:"gruvbox-light"},"Gruvbox Light",-1),o("option",{value:"monokai-dark"},"Monokai Dark",-1),o("option",{value:"monokai-light"},"Monokai Light",-1)])],512),[[Oe,t.value]])]),o("div",null,[l[90]||(l[90]=o("div",{class:"label"},"Densite",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[12]||(l[12]=f=>s.value=f)},[...l[89]||(l[89]=[o("option",{value:"dense"},"Dense",-1),o("option",{value:"comfort"},"Confort",-1)])],512),[[Oe,s.value]])]),o("div",null,[l[92]||(l[92]=o("div",{class:"label"},"Layout",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[13]||(l[13]=f=>i.value=f)},[...l[91]||(l[91]=[o("option",{value:"auto"},"Auto",-1),o("option",{value:"compact"},"Compact",-1),o("option",{value:"wide"},"Large",-1)])],512),[[Oe,i.value]])]),o("div",null,[l[93]||(l[93]=o("div",{class:"label"},"Police titre",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[14]||(l[14]=f=>g.value.fontTitle=f)},[(R(),$(ie,null,xe(ct,f=>o("option",{key:f.id,value:f.id},I(f.label),9,Wf)),64))],512),[[Oe,g.value.fontTitle]])]),o("div",null,[l[94]||(l[94]=o("div",{class:"label"},"Police corps",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[15]||(l[15]=f=>g.value.fontBody=f)},[(R(),$(ie,null,xe(ct,f=>o("option",{key:f.id,value:f.id},I(f.label),9,Gf)),64))],512),[[Oe,g.value.fontBody]])]),o("div",null,[l[95]||(l[95]=o("div",{class:"label"},"Police mono",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[16]||(l[16]=f=>g.value.fontMono=f)},[(R(),$(ie,null,xe(ct,f=>o("option",{key:f.id,value:f.id},I(f.label),9,Kf)),64))],512),[[Oe,g.value.fontMono]])]),o("div",null,[l[96]||(l[96]=o("div",{class:"label"},"Taille texte",-1)),o("div",zf,[q(o("input",{class:"input",type:"range",min:"12",max:"20",step:"1","onUpdate:modelValue":l[17]||(l[17]=f=>g.value.fontSize=f)},null,512),[[he,g.value.fontSize,void 0,{number:!0}]]),o("div",Zf,I(g.value.fontSize)+"px",1)])]),o("div",null,[l[98]||(l[98]=o("div",{class:"label"},"Taille icones",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[18]||(l[18]=f=>g.value.iconSize=f)},[...l[97]||(l[97]=[o("option",{value:"sm"},"Compact",-1),o("option",{value:"md"},"Normal",-1),o("option",{value:"lg"},"Large",-1)])],512),[[Oe,g.value.iconSize]])]),o("div",null,[o("div",Yf,[l[99]||(l[99]=o("span",null,"Taille icône boutique",-1)),o("span",Jf,I(M.value),1)]),q(o("input",{class:"input",type:"range",min:"16",max:"40","onUpdate:modelValue":l[19]||(l[19]=f=>N.value=f)},null,512),[[he,N.value,void 0,{number:!0}]])]),o("div",null,[o("div",qf,[l[100]||(l[100]=o("span",null,"Ratio des vignettes",-1)),o("span",Qf,I(Re.value),1)]),q(o("input",{class:"input",type:"range",min:"0.75",max:"1.3",step:"0.05","onUpdate:modelValue":l[20]||(l[20]=f=>g.value.cardRatio=f)},null,512),[[he,g.value.cardRatio,void 0,{number:!0}]]),l[101]||(l[101]=o("div",{class:"text-xs label flex justify-between mt-1"},[o("span",null,"Compact"),o("span",null,"Standard"),o("span",null,"Étendu")],-1))]),o("div",null,[o("div",Xf,[l[102]||(l[102]=o("span",null,"Colonnes de vignettes",-1)),o("span",ep,I(qe.value),1)]),q(o("input",{class:"input",type:"range",min:Fl,max:Ll,step:"1","onUpdate:modelValue":l[21]||(l[21]=f=>g.value.cardColumns=f)},null,512),[[he,g.value.cardColumns,void 0,{number:!0}]]),l[103]||(l[103]=o("div",{class:"text-xs label flex justify-between mt-1"},[o("span",null,"1"),o("span",null,"3"),o("span",null,"6")],-1))]),o("div",null,[o("div",tp,[l[104]||(l[104]=o("span",null,"Hauteur image produit",-1)),o("span",sp,I(Qe.value),1)]),q(o("input",{class:"input",type:"range",min:"120",max:"220",step:"5","onUpdate:modelValue":l[22]||(l[22]=f=>g.value.cardImageHeight=f)},null,512),[[he,g.value.cardImageHeight,void 0,{number:!0}]])]),o("div",null,[l[105]||(l[105]=o("div",{class:"label"},"Mode image",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[23]||(l[23]=f=>g.value.imageMode=f)},[(R(),$(ie,null,xe(C,f=>o("option",{key:f.value,value:f.value},I(f.label),9,np)),64))],512),[[Oe,g.value.imageMode]])]),o("div",null,[l[106]||(l[106]=o("div",{class:"label"},"Période du graphique",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[24]||(l[24]=f=>g.value.chartPeriod=f)},[(R(),$(ie,null,xe(w,f=>o("option",{key:f.value,value:f.value},I(f.label),9,ip)),64))],512),[[Oe,g.value.chartPeriod,void 0,{number:!0}]])]),o("div",null,[l[107]||(l[107]=o("div",{class:"label"},"Durée barre logs (ms)",-1)),q(o("input",{class:"input",type:"number",min:"500","onUpdate:modelValue":l[25]||(l[25]=f=>g.value.logDuration=f)},null,512),[[he,g.value.logDuration,void 0,{number:!0}]])]),o("div",lp,[o("div",null,[l[108]||(l[108]=o("div",{class:"label"},"Seuil baisse (%)",-1)),q(o("input",{class:"input",type:"number",step:"0.1","onUpdate:modelValue":l[26]||(l[26]=f=>g.value.trendThresholdDown=f)},null,512),[[he,g.value.trendThresholdDown,void 0,{number:!0}]])]),o("div",null,[l[109]||(l[109]=o("div",{class:"label"},"Seuil hausse (%)",-1)),q(o("input",{class:"input",type:"number",step:"0.1","onUpdate:modelValue":l[27]||(l[27]=f=>g.value.trendThresholdUp=f)},null,512),[[he,g.value.trendThresholdUp,void 0,{number:!0}]])])])])]),o("section",null,[l[115]||(l[115]=o("div",{class:"section-title text-sm mb-2"},"Donnees",-1)),o("div",op,[o("div",null,[l[111]||(l[111]=o("div",{class:"label"},"Frequence rafraichissement",-1)),q(o("input",{class:"input","onUpdate:modelValue":l[28]||(l[28]=f=>g.value.refreshMinutes=f),type:"number",min:"1"},null,512),[[he,g.value.refreshMinutes,void 0,{number:!0}]])]),o("div",null,[l[112]||(l[112]=o("div",{class:"label"},"Limite produits page",-1)),q(o("input",{class:"input","onUpdate:modelValue":l[29]||(l[29]=f=>g.value.pageSize=f),type:"number",min:"10"},null,512),[[he,g.value.pageSize,void 0,{number:!0}]])]),o("div",null,[l[114]||(l[114]=o("div",{class:"label"},"Preferer WebP",-1)),q(o("select",{class:"input","onUpdate:modelValue":l[30]||(l[30]=f=>g.value.preferWebp=f)},[...l[113]||(l[113]=[o("option",{value:!0},"Oui",-1),o("option",{value:!1},"Non",-1)])],512),[[Oe,g.value.preferWebp]])])])]),o("section",null,[l[118]||(l[118]=o("div",{class:"section-title text-sm mb-2"},"API",-1)),o("div",rp,[o("div",null,[l[116]||(l[116]=o("div",{class:"label"},"Base URL",-1)),q(o("input",{class:"input","onUpdate:modelValue":l[31]||(l[31]=f=>g.value.apiBase=f),placeholder:"/api"},null,512),[[he,g.value.apiBase]])]),o("div",null,[l[117]||(l[117]=o("div",{class:"label"},"Token",-1)),q(o("input",{class:"input",type:"password","onUpdate:modelValue":l[32]||(l[32]=f=>g.value.apiToken=f),placeholder:"Bearer token"},null,512),[[he,g.value.apiToken]])]),o("button",{class:"input text-left",onClick:It}," Recharger les donnees ")])]),o("section",null,[l[124]||(l[124]=o("div",{class:"section-title text-sm mb-2"},"Stores",-1)),o("div",ap,[(R(!0),$(ie,null,xe(be.value,f=>(R(),$("div",{key:f.id,class:"space-y-2 border border-white/5 rounded-lg p-2"},[l[120]||(l[120]=o("div",{class:"label"},"Nom",-1)),q(o("input",{class:"input","onUpdate:modelValue":T=>f.name=T},null,8,up),[[he,f.name]]),l[121]||(l[121]=o("div",{class:"label"},"Domaine",-1)),q(o("input",{class:"input","onUpdate:modelValue":T=>f.domain=T},null,8,cp),[[he,f.domain]]),o("div",dp,[o("button",{class:"icon-btn",title:"Supprimer",onClick:T=>hr(f.id)},[...l[119]||(l[119]=[o("i",{class:"fa-solid fa-trash"},null,-1)])],8,fp)])]))),128)),o("div",pp,[l[123]||(l[123]=o("div",{class:"label"},"Ajouter un store",-1)),o("div",mp,[q(o("input",{class:"input","onUpdate:modelValue":l[33]||(l[33]=f=>We.value=f),placeholder:"Nom"},null,512),[[he,We.value]]),q(o("input",{class:"input","onUpdate:modelValue":l[34]||(l[34]=f=>Fe.value=f),placeholder:"Domaine"},null,512),[[he,Fe.value]]),o("button",{class:"icon-btn",title:"Ajouter",onClick:vr},[...l[122]||(l[122]=[o("i",{class:"fa-solid fa-plus"},null,-1)])])])])])]),o("section",null,[l[128]||(l[128]=o("div",{class:"section-title text-sm mb-2"},"Debug",-1)),o("div",vp,[o("div",hp,[l[125]||(l[125]=o("div",{class:"label"},"Afficher erreurs scraping",-1)),q(o("input",{type:"checkbox","onUpdate:modelValue":l[35]||(l[35]=f=>g.value.showDebug=f)},null,512),[[jn,g.value.showDebug]])]),o("div",gp,[l[126]||(l[126]=o("div",{class:"label"},"Animations",-1)),q(o("input",{type:"checkbox","onUpdate:modelValue":l[36]||(l[36]=f=>g.value.enableAnimations=f)},null,512),[[jn,g.value.enableAnimations]])]),o("div",bp,[l[127]||(l[127]=o("div",{class:"label"},"Historique au survol",-1)),q(o("input",{type:"checkbox","onUpdate:modelValue":l[37]||(l[37]=f=>g.value.showHistoryHover=f)},null,512),[[jn,g.value.showHistoryHover]])])])]),o("section",null,[l[129]||(l[129]=o("div",{class:"section-title text-sm mb-2"},"Maintenance",-1)),o("div",{class:"space-y-2"},[o("button",{class:"input text-left",onClick:dr}," Appliquer et recharger "),o("button",{class:"input text-left",onClick:Sn}," Recharger la page "),o("button",{class:"input text-left",onClick:cr}," Vider le cache et recharger ")])])])])])):Te("",!0),u.value?(R(),$("div",{key:1,class:"fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4",onClick:Gs(Mi,["self"])},[o("div",yp,[o("div",{class:"flex items-center justify-between mb-4"},[l[131]||(l[131]=o("div",{class:"section-title text-lg"},"Ajouter un produit",-1)),o("button",{class:"icon-btn",title:"Fermer",onClick:Mi},[...l[130]||(l[130]=[o("i",{class:"fa-solid fa-xmark"},null,-1)])])]),o("div",xp,[q(o("input",{class:"input","onUpdate:modelValue":l[38]||(l[38]=f=>Je.value.url=f),placeholder:"URL"},null,512),[[he,Je.value.url]]),q(o("select",{class:"input","onUpdate:modelValue":l[39]||(l[39]=f=>Je.value.usePlaywright=f)},[...l[132]||(l[132]=[o("option",{value:!1},"HTTP uniquement",-1),o("option",{value:!0},"Playwright fallback",-1)])],512),[[Oe,Je.value.usePlaywright]]),o("button",{class:"input text-left",onClick:jr,disabled:!Je.value.url||Y.value.loading},I(Y.value.loading?"Analyse en cours...":"Ajouter"),9,_p),Y.value.error?(R(),$("div",wp,"Erreur: "+I(Y.value.error),1)):Te("",!0),Y.value.snapshot?(R(),$("div",Cp,[l[133]||(l[133]=o("div",{class:"section-title text-sm"},"Donnees scrappees",-1)),o("div",Sp,"Store: "+I(Y.value.snapshot.source),1),o("div",kp,"Reference: "+I(Y.value.snapshot.reference||"n/a"),1),o("div",Ap,"Titre: "+I(Y.value.snapshot.title||"n/a"),1),o("div",Pp,[o("picture",null,[o("source",{srcset:Xt.value,type:"image/webp"},null,8,Mp),o("source",{srcset:Xt.value,type:"image/jpeg"},null,8,Ip),o("img",{src:Xt.value,class:"h-20 w-20 rounded-lg object-cover border border-white/10",alt:"vignette",loading:"lazy"},null,8,Tp)]),o("div",Ep,[o("div",Rp,"Images: "+I(Le.value),1),ps.value.length?(R(),$("div",Np,[(R(!0),$(ie,null,xe(ps.value,(f,T)=>(R(),$("button",{key:`${f}-${T}`,type:"button",class:Se(["image-toggle",{selected:nr.value.has(T)}]),onClick:X=>ir(T)},[o("picture",null,[o("source",{srcset:f,type:"image/webp"},null,8,Dp),o("img",{src:f,class:"h-16 w-full object-cover rounded-md",alt:"miniature",loading:"lazy"},null,8,Fp)])],10,$p))),128))])):Te("",!0)])]),o("div",Lp,"Prix: "+I(es(Y.value.snapshot.price,Y.value.snapshot.currency)),1),o("div",Op,"Prix conseille: "+I(es(Y.value.snapshot.msrp,Y.value.snapshot.currency)),1),o("div",Hp,"Stock: "+I(Y.value.snapshot.stock_status||"n/a"),1),o("div",jp,"Categorie: "+I(Y.value.snapshot.category||"n/a"),1),o("div",Up,"Description: "+I(Y.value.snapshot.description||"n/a"),1),o("div",Vp,"Caracteristiques: "+I(lr.value),1),Ai.value.length===0?(R(),$("div",Bp,"n/a")):(R(),$("div",Wp,[(R(!0),$(ie,null,xe(Ai.value,([f,T])=>(R(),$("div",{key:f,class:"label"},I(f)+": "+I(T),1))),128))])),o("button",{class:"input text-left",onClick:Ur}," Enregistrer ")])):Te("",!0)])])])):Te("",!0),y.value?(R(),$("div",{key:2,class:"fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4",onClick:l[49]||(l[49]=Gs(f=>Ie.value=null,["self"]))},[o("div",{ref_key:"detailModalRef",ref:U,class:"w-full max-w-6xl detail-dialog panel detail-popup flex flex-col overflow-hidden",role:"dialog","aria-label":`Détails produit ${y.value.title||""}`},[o("div",Kp,[o("div",null,[o("div",zp,[l[134]||(l[134]=Ye(" Nom : ",-1)),o("span",{class:"detail-title",title:y.value.title},I(y.value.title||"Sans titre"),9,Zp)]),o("div",Yp,I(ts(y.value.storeId))+" • "+I(y.value.reference||"Réf. inconnue"),1)]),o("button",{class:"icon-btn",title:"Fermer","aria-label":"Fermer le panneau détail",onClick:l[40]||(l[40]=f=>Ie.value=null),type:"button"},[...l[135]||(l[135]=[o("i",{class:"fa-solid fa-xmark"},null,-1)])])]),o("div",Jp,[o("div",qp,[o("div",Qp,[o("section",Xp,[l[136]||(l[136]=o("div",{class:"section-title text-sm"},"Résumé",-1)),o("picture",em,[o("source",{srcset:y.value.imageWebp,type:"image/webp"},null,8,tm),o("source",{srcset:y.value.imageJpg,type:"image/jpeg"},null,8,sm),o("img",{src:y.value.imageJpg,class:"w-full h-full object-contain rounded-xl",alt:"image produit",loading:"lazy"},null,8,nm)]),o("div",{class:"label detail-summary-title",title:y.value.title},I(y.value.title||"Sans titre"),9,im),o("div",lm,"Stock actuel: "+I(rr(y.value.stockStatus)),1)]),o("section",om,[o("div",rm,[o("button",{class:Se(["detail-tab-button",{active:ee.value==="description"}]),disabled:!P.value,role:"tab",onClick:l[41]||(l[41]=f=>ee.value="description"),"aria-selected":ee.value==="description",type:"button"}," Description ",10,am),o("button",{class:Se(["detail-tab-button",{active:ee.value==="specs"}]),disabled:!G.value,role:"tab",onClick:l[42]||(l[42]=f=>ee.value="specs"),"aria-selected":ee.value==="specs",type:"button"}," Caractéristiques ",10,um)]),o("div",cm,[ee.value==="description"?(R(),$("div",dm,[P.value?(R(),$("div",{key:0,class:"label detail-text",title:y.value.description},I(y.value.description),9,fm)):(R(),$("div",pm,"Aucune description."))])):(R(),$("div",mm,[G.value?(R(),$("div",vm,[(R(!0),$(ie,null,xe(j.value,(f,T)=>(R(),$("div",{key:`${f[0]}-${T}`,class:"label"},[o("span",hm,I(f[0]),1),Ye(": "+I(f[1]),1)]))),128))])):(R(),$("div",gm,"Aucune caractéristique."))]))])])]),o("div",bm,[o("section",ym,[l[137]||(l[137]=o("div",{class:"section-title text-sm"},"Prix",-1)),o("div",xm,[o("div",_m,I(es(y.value.price,y.value.currency)),1),o("div",wm,"Dernière mise à jour: "+I(y.value.updatedAtLabel),1)]),o("div",Cm,"Prix conseillé: "+I(es(y.value.msrp,y.value.currency)),1),o("div",Sm,"Réduction: "+I(ar(y.value.discountAmount,y.value.discountPercent)),1)]),o("section",km,[l[139]||(l[139]=o("div",{class:"section-title text-sm"},"Source",-1)),o("div",Am,[y.value.url?(R(),$("a",{key:0,href:y.value.url,target:"_blank",rel:"noreferrer",class:"label link flex-1",title:"Ouvrir la page produit"}," Ouvrir la page produit ",8,Pm)):Te("",!0),o("button",{class:Se(["icon-btn",y.value.url?"":"opacity-50 pointer-events-none"]),title:"Copier l'URL","aria-label":"Copier l'URL",onClick:Hr,type:"button"},[...l[138]||(l[138]=[o("i",{class:"fa-solid fa-copy"},null,-1)])],2)]),o("div",Mm,"URL: "+I(y.value.url||"n/a"),1),o("div",Im,"ASIN / Référence: "+I(y.value.reference||"n/a"),1)]),o("section",Tm,[l[144]||(l[144]=o("div",{class:"section-title text-sm"},"Historique",-1)),o("div",Em,[(R(),$(ie,null,xe(H,f=>o("button",{key:f.id,class:Se(["detail-period-button",{selected:z.value===f.id}]),type:"button",onClick:T=>z.value=f.id},I(f.label),11,Rm)),64))]),o("div",Nm,[o("div",null,[l[140]||(l[140]=o("div",{class:"label"},"Actuel",-1)),o("div",$m,I(Os(se.value.current,y.value.currency)),1)]),o("div",null,[l[141]||(l[141]=o("div",{class:"label"},"Min",-1)),o("div",Dm,I(Os(se.value.min,y.value.currency)),1)]),o("div",null,[l[142]||(l[142]=o("div",{class:"label"},"Max",-1)),o("div",Fm,I(Os(se.value.max,y.value.currency)),1)]),o("div",null,[l[143]||(l[143]=o("div",{class:"label"},"Delta",-1)),o("div",Lm,I(Z.value),1)])]),Ae(Zc,{class:"mt-2",history:V.value.history,"current-price":V.value.currentPrice,"min-price":V.value.minPrice,"max-price":V.value.maxPrice,"delta-label":Z.value},null,8,["history","current-price","min-price","max-price","delta-label"])]),o("section",Om,[l[148]||(l[148]=o("div",{class:"section-title text-sm"},"Edition produit",-1)),o("div",Hm,[q(o("input",{class:"input","onUpdate:modelValue":l[43]||(l[43]=f=>y.value.title=f),placeholder:"Nom produit"},null,512),[[he,y.value.title]]),q(o("input",{class:"input","onUpdate:modelValue":l[44]||(l[44]=f=>y.value.url=f),placeholder:"URL"},null,512),[[he,y.value.url]]),q(o("input",{class:"input","onUpdate:modelValue":l[45]||(l[45]=f=>y.value.category=f),placeholder:"Catégorie"},null,512),[[he,y.value.category]]),q(o("input",{class:"input","onUpdate:modelValue":l[46]||(l[46]=f=>y.value.currency=f),placeholder:"Devise"},null,512),[[he,y.value.currency]]),q(o("input",{class:"input","onUpdate:modelValue":l[47]||(l[47]=f=>y.value.reference=f),placeholder:"Référence",disabled:""},null,512),[[he,y.value.reference]]),q(o("input",{class:"input","onUpdate:modelValue":l[48]||(l[48]=f=>y.value.refreshHours=f),type:"number",min:"1",placeholder:"Fréquence (h)"},null,512),[[he,y.value.refreshHours,void 0,{number:!0}]])]),o("div",{class:"flex items-center gap-2 actions-section"},[o("button",{class:"icon-btn",title:"Enregistrer","aria-label":"Enregistrer le produit",onClick:Fr,type:"button"},[...l[145]||(l[145]=[o("i",{class:"fa-solid fa-floppy-disk"},null,-1)])]),o("button",{class:"icon-btn",title:"Rafraîchir","aria-label":"Rafraîchir le produit",onClick:Lr,type:"button"},[...l[146]||(l[146]=[o("i",{class:"fa-solid fa-rotate"},null,-1)])]),o("button",{class:"icon-btn",title:"Supprimer","aria-label":"Supprimer le produit",onClick:Or,type:"button"},[...l[147]||(l[147]=[o("i",{class:"fa-solid fa-trash"},null,-1)])])])])])])])],8,Gp)])):Te("",!0),o("div",jm,[o("div",Um,"API "+I(Pi.value),1),o("div",Vm,"DB "+I(J.value.db?"OK":"KO"),1),o("div",Bm,"Redis "+I(J.value.redis?"OK":"KO"),1),o("button",{class:"icon-btn",title:"Logs",onClick:Cn},[...l[149]||(l[149]=[o("i",{class:"fa-solid fa-scroll"},null,-1)])])]),d.value?(R(),$("div",{key:3,class:"fixed inset-0 bg-black/40 z-50",onClick:Gs(Cn,["self"])},[o("div",Wm,[o("div",{class:"flex items-center justify-between mb-2"},[l[151]||(l[151]=o("div",{class:"section-title text-lg"},"Logs",-1)),o("button",{class:"icon-btn",title:"Fermer",onClick:Cn},[...l[150]||(l[150]=[o("i",{class:"fa-solid fa-xmark"},null,-1)])])]),o("div",Gm,[(R(!0),$(ie,null,xe(_n.value,f=>(R(),$("div",{key:f.id,class:Se(["panel p-2 log-entry",{"log-entry-error":f.level==="ERROR"}])},[o("div",Km,I(f.time)+" - "+I(f.source)+" - "+I(f.level),1),o("div",null,I(f.message),1)],2))),128)),_n.value.length===0?(R(),$("div",zm,"Aucun log")):Te("",!0)]),o("div",Zm,[o("button",{class:"icon-btn",title:"Frontend","aria-label":"Afficher logs frontend",onClick:l[50]||(l[50]=f=>v.value="frontend")},[...l[152]||(l[152]=[o("i",{class:"fa-solid fa-desktop"},null,-1)])]),o("button",{class:"icon-btn",title:"Backend","aria-label":"Afficher logs backend",onClick:l[51]||(l[51]=f=>v.value="backend")},[...l[153]||(l[153]=[o("i",{class:"fa-solid fa-server"},null,-1)])]),o("button",{class:"icon-btn",title:"Uvicorn","aria-label":"Afficher logs uvicorn",onClick:l[52]||(l[52]=f=>v.value="uvicorn")},[...l[154]||(l[154]=[o("i",{class:"fa-solid fa-terminal"},null,-1)])]),o("button",{class:"icon-btn",title:"Erreurs","aria-label":"Afficher les erreurs uniquement",onClick:l[53]||(l[53]=f=>v.value="errors")},[...l[155]||(l[155]=[o("i",{class:"fa-solid fa-triangle-exclamation"},null,-1)])]),o("button",{class:"icon-btn",title:"Rafraichir","aria-label":"Rafraîchir les logs",onClick:Gi},[...l[156]||(l[156]=[o("i",{class:"fa-solid fa-rotate"},null,-1)])]),o("button",{class:"icon-btn",title:"Copier logs","aria-label":"Copier les logs",onClick:Wr},[...l[157]||(l[157]=[o("i",{class:"fa-solid fa-copy"},null,-1)])])])])])):Te("",!0),Ae(jc,{visible:E.value,position:S.value.position,history:S.value.history,"current-price":S.value.currentPrice,"min-price":S.value.minPrice,"max-price":S.value.maxPrice,delta:S.value.delta,onMouseenter:Pr,onMouseleave:Mr},null,8,["visible","position","history","current-price","min-price","max-price","delta"])],6))}});Ac(Qm).mount("#app"); diff --git a/webui/dist/favicon.svg b/webui/dist/favicon.svg new file mode 100644 index 0000000..d4a38d9 --- /dev/null +++ b/webui/dist/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webui/dist/index.html b/webui/dist/index.html new file mode 100644 index 0000000..65cfe2b --- /dev/null +++ b/webui/dist/index.html @@ -0,0 +1,14 @@ + + + + + + PriceWatch Web UI + + + + + +
+ + diff --git a/webui/src/App.vue b/webui/src/App.vue index 0d8b563..35f3bb2 100644 --- a/webui/src/App.vue +++ b/webui/src/App.vue @@ -183,143 +183,27 @@
- +
@@ -477,7 +361,25 @@ Standard Étendu -
{{ cardColumnsLabel }}
+ +
+
+ Colonnes de vignettes + {{ cardColumnsLabel }} +
+ +
+ {{ MIN_CARD_COLUMNS }} + {{ cardColumnsMidLabel }} + {{ cardColumnsMax }} +
@@ -547,6 +449,53 @@
+
+
Classification
+
+
+ Aucune regle disponible. +
+
+
+
+ {{ rule.category || "Sans categorie" }} • {{ rule.type || "Sans type" }} +
+
+ {{ rule.keywords.join(", ") }} +
+
+ +
+
+
+
Categorie
+ +
+
+
Type
+ +
+
+
+
Mots-cles (titre)
+ +
+ +
+
+
API
@@ -628,29 +577,59 @@
-
-
+
+
Ajouter un produit
-
+
- +
+
+
Categorie
+ +
+
+
Type
+ +
+
Erreur: {{ addProductState.error }}
-
-
Donnees scrappees
+
Store: {{ addProductState.snapshot.source }}
Reference: {{ addProductState.snapshot.reference || "n/a" }}
+
ASIN: {{ addProductState.snapshot.asin || "n/a" }}
Titre: {{ addProductState.snapshot.title || "n/a" }}
+
+ Note: {{ addProductState.snapshot.rating_value ?? "n/a" }} + ({{ addProductState.snapshot.rating_count ?? "n/a" }} avis) +
+
+ Choix Amazon: {{ + addProductState.snapshot.amazon_choice === null || addProductState.snapshot.amazon_choice === undefined + ? "n/a" + : addProductState.snapshot.amazon_choice + ? "Oui" + : "Non" + }} +
@@ -664,12 +643,12 @@
Images: {{ addProductImagesCount }}
-
+
-
+
+ Aperçu prêt. Activez le mode debug pour afficher les données scrappées. +
+
+
+ + + + + + +
+ @@ -1011,6 +1026,7 @@ import { getStoreLogo } from "@/utils/storeLogos"; import PriceHistoryHover from "@/components/PriceHistoryHover.vue"; import PriceHistoryChart from "@/components/PriceHistoryChart.vue"; import MiniLineChart from "@/components/MiniLineChart.vue"; +import ProductCard from "@/components/ProductCard.vue"; import { computeFloatingPosition } from "@/utils/computeFloatingPosition"; // === Types === @@ -1028,20 +1044,35 @@ interface UIProduct { currency: string; msrp: number | null; stockStatus: string; + stockText?: string; + inStock?: boolean; updatedAt: string; updatedAtLabel?: string; delta: number; discountAmount: number | null; discountPercent: number | null; - imageWebp: string | null; - imageJpg: string | null; + discountText?: string; + imageWebp?: string; + imageJpg?: string; history: HistoryEntry[]; - description: string | null; + description?: string; specs: Record; images: string[]; reference?: string; + asin?: string; category?: string; + type?: string; refreshHours?: number; + notes?: string; + analysis?: string; + ratingValue?: number; + ratingCount?: number; + amazonChoice?: boolean; + amazonChoiceLabel?: string; + modelNumber?: string; + modelName?: string; + mainImage?: string; + galleryImages?: string[]; } interface Store { @@ -1063,9 +1094,20 @@ interface ProductSnapshot { currency: string | null; msrp: number | null; stock_status: string | null; + stock_text?: string | null; + in_stock?: boolean | null; category: string | null; + type?: string | null; description: string | null; + rating_value?: number | null; + rating_count?: number | null; + amazon_choice?: boolean | null; + amazon_choice_label?: string | null; + discount_text?: string | null; + asin?: string | null; images: string[]; + main_image?: string | null; + gallery_images?: string[]; specs: Record; } @@ -1075,8 +1117,17 @@ interface AddProductState { snapshot: ProductSnapshot | null; } -interface LogEntry { +type ClassificationRule = { id: number; + category: string | null; + type: string | null; + keywords: string[]; + sort_order: number; + is_active: boolean; +}; + +interface LogEntry { + id: number | string; time: string; level: string; message: string; @@ -1093,6 +1144,14 @@ interface UvicornLogEntry { line: string; } +interface DisplayLogEntry { + id: number | string; + time: string; + level: string; + message: string; + source?: string; +} + const theme = ref("gruvbox-dark"); const density = ref("dense"); const viewMode = ref("cards"); @@ -1109,7 +1168,9 @@ const CARD_RATIO_PRESETS = [ { id: "standard", label: "Standard", value: 1 }, { id: "etendu", label: "Étendu", value: 1.2 }, ]; -const CARD_COLUMNS = 3; +const DEFAULT_CARD_COLUMNS = 3; +const MIN_CARD_COLUMNS = 1; +const MAX_CARD_COLUMNS = 6; const DEFAULT_IMAGE_HEIGHT = 160; const CARD_HISTORY_LIMIT = 12; const CHART_PERIOD_OPTIONS = [ @@ -1153,6 +1214,7 @@ const settings = ref({ fontSize: 16, showHistoryHover: true, cardRatio: DEFAULT_CARD_RATIO, + cardColumns: DEFAULT_CARD_COLUMNS, cardImageHeight: DEFAULT_IMAGE_HEIGHT, chartPeriod: CHART_PERIOD_OPTIONS[0].value, imageMode: "contain", @@ -1161,6 +1223,19 @@ const settings = ref({ trendThresholdUp: DEFAULT_TREND_UP, }); +const cardColumnsMax = ref(MAX_CARD_COLUMNS); +const classificationRules = ref([]); +const classificationOptions = ref({ + categories: [] as string[], + types: [] as string[], +}); +const classificationDraft = ref({ + category: "", + type: "", + keywords: "", + sortOrder: 0, +}); + const STORE_ICON_KEY = "pw_store_icon_size"; const DEFAULT_STORE_ICON = 40; const storeIconSize = ref(DEFAULT_STORE_ICON); @@ -1238,11 +1313,12 @@ const addProductState = ref({ snapshot: null, }); -const addProductSelectedImages = ref([]); +const addProductSelectedImageIndex = ref(null); const logEntries = ref([]); const backendLogs = ref([]); const uvicornLogs = ref([]); +const autoScheduleDone = ref(false); const fontPresets = [ { @@ -1270,7 +1346,7 @@ const densityClass = computed(() => `density-${density.value}`); const layoutClass = computed(() => `layout-${layoutMode.value}`); const rootStyle = computed(() => { - const findStack = (id) => fontPresets.find((font) => font.id === id)?.stack; + const findStack = (id: string) => fontPresets.find((font) => font.id === id)?.stack; return { "--font-title": findStack(settings.value.fontTitle) || fontPresets[0].stack, "--font-body": findStack(settings.value.fontBody) || fontPresets[0].stack, @@ -1279,7 +1355,7 @@ const rootStyle = computed(() => { }; }); -const statusDisplayMap = { +const statusDisplayMap: Record = { in_stock: "En stock", out_of_stock: "Rupture", unknown: "Inconnu", @@ -1418,7 +1494,7 @@ const activeFilterChips = computed(() => { } const stockValue = filters.value.stock; if (stockValue && stockValue !== "all") { - const translation = { + const translation: Record = { in_stock: "En stock", out_of_stock: "Rupture", unknown: "Inconnu", @@ -1463,7 +1539,10 @@ const cardRatioLabel = computed(() => { return `${label} • ${ratio.toFixed(2)}`; }); -const cardColumnsLabel = computed(() => `${CARD_COLUMNS} colonnes (Bureau)`); +const cardColumnsLabel = computed(() => `${settings.value.cardColumns} colonnes`); +const cardColumnsMidLabel = computed(() => + Math.max(MIN_CARD_COLUMNS, Math.round(cardColumnsMax.value / 2)) +); const cardImageHeightLabel = computed(() => `${Math.round(settings.value.cardImageHeight ?? DEFAULT_IMAGE_HEIGHT)}px`); const chartPeriodLabel = computed(() => { const period = Number(settings.value.chartPeriod ?? CHART_PERIOD_OPTIONS[0].value); @@ -1483,32 +1562,36 @@ const productHistorySnapshots = computed(() => { }); const addProductImagesCount = computed(() => { - if (!addProductState.value.snapshot || !addProductState.value.snapshot.images) { - return 0; - } - return addProductState.value.snapshot.images.length; + return addProductImages.value.length; }); const addProductImages = computed(() => { - if (!addProductState.value.snapshot || !Array.isArray(addProductState.value.snapshot.images)) { + const snapshot = addProductState.value.snapshot; + if (!snapshot) { return []; } - return addProductState.value.snapshot.images.filter(Boolean); + const images = Array.isArray(snapshot.images) ? snapshot.images : []; + const mainImage = snapshot.main_image ? [snapshot.main_image] : []; + const gallery = Array.isArray(snapshot.gallery_images) ? snapshot.gallery_images : []; + const candidates = [...mainImage, ...gallery, ...images].filter(Boolean); + return [...new Set(candidates)]; }); -const addProductImagePreview = computed(() => addProductImages.value[0] || placeholderImage); - -const addProductImageThumbs = computed(() => addProductImages.value.slice(0, 4)); - -const selectedImagesSet = computed(() => new Set(addProductSelectedImages.value)); - -function toggleAddProductImage(index) { - const selected = addProductSelectedImages.value.includes(index); - if (selected) { - addProductSelectedImages.value = addProductSelectedImages.value.filter((item) => item !== index); - return; +const addProductImagePreview = computed(() => { + if (addProductSelectedImageIndex.value === null) { + return addProductImages.value[0] || placeholderImage; } - addProductSelectedImages.value = [...addProductSelectedImages.value, index]; + return addProductImages.value[addProductSelectedImageIndex.value] || placeholderImage; +}); + +const addProductImageThumbs = computed(() => addProductImages.value.slice(0, 15)); + +const selectedImagesSet = computed(() => + addProductSelectedImageIndex.value === null ? new Set() : new Set([addProductSelectedImageIndex.value]) +); + +function toggleAddProductImage(index: number): void { + addProductSelectedImageIndex.value = index; } const addProductSpecsCount = computed(() => { @@ -1542,12 +1625,24 @@ const apiStatus = computed(() => { return "API OK"; }); -const filteredLogs = computed(() => { +const filteredLogs = computed((): DisplayLogEntry[] => { if (logTab.value === "backend") { - return backendLogs.value; + return backendLogs.value.map((entry, index) => ({ + id: `backend-${index}`, + time: entry.time, + level: entry.level, + message: entry.message, + source: "backend", + })); } if (logTab.value === "uvicorn") { - return uvicornLogs.value; + return uvicornLogs.value.map((entry, index) => ({ + id: `uvicorn-${index}`, + time: "", + level: "INFO", + message: entry.line, + source: "uvicorn", + })); } if (logTab.value === "errors") { return logEntries.value.filter((entry) => entry.level === "ERROR"); @@ -1555,25 +1650,35 @@ const filteredLogs = computed(() => { return logEntries.value; }); -function formatPrice(value, currency = "EUR") { +function formatPrice(value: number | null | undefined, currency: string | null | undefined = "EUR"): string { if (value === null || value === undefined) { return "n/a"; } - return `${value} ${currency}`; + return `${value} ${currency ?? "EUR"}`; } -function formatShortPrice(value, currency = "EUR") { - if (value === null || value === undefined) { - return "n/a"; +function formatShortPrice(value: number | null | undefined, currency: string | null | undefined = "EUR"): string { + if (value === null || value === undefined || !Number.isFinite(value)) { + return "—"; + } + const numeric = Number(value); + const rounded = Math.round(numeric * 100) / 100; + const centsValue = Math.round(rounded * 100) - Math.floor(rounded) * 100; + const hasCents = centsValue !== 0; + if ((currency || "EUR") === "EUR") { + const euros = Math.floor(rounded); + const cents = String(Math.abs(centsValue)).padStart(2, "0"); + return hasCents ? `${euros}€${cents}` : `${euros}€`; } return new Intl.NumberFormat("fr-FR", { style: "currency", - currency, - minimumFractionDigits: 2, - }).format(value); + currency: currency || "EUR", + minimumFractionDigits: hasCents ? 2 : 0, + maximumFractionDigits: hasCents ? 2 : 0, + }).format(rounded); } -function formatCardDelta(delta) { +function formatCardDelta(delta: number | null | undefined): string { if (delta === null || delta === undefined || Number.isNaN(delta)) { return "—"; } @@ -1586,28 +1691,28 @@ function formatCardDelta(delta) { return `${arrow} ${absolute}%`; } -function statusLabel(status) { +function statusLabel(status: string | null | undefined): string { if (!status) { return statusDisplayMap.unknown; } - return statusDisplayMap[status] || status.replaceAll("_", " ").replace(/\b\w/g, (chr) => chr.toUpperCase()); + return statusDisplayMap[status] || status.replaceAll("_", " ").replace(/\b\w/g, (chr: string) => chr.toUpperCase()); } -function statusClass(status) { +function statusClass(status: string | null | undefined): string { if (!status) { return "status-unknown"; } return `status-${status.replaceAll(/[^a-zA-Z0-9]/g, "-")}`; } -function formatDiscount(amount, percent) { +function formatDiscount(amount: number | null | undefined, percent: number | null | undefined): string { if (amount === null || amount === undefined || percent === null || percent === undefined) { return "n/a"; } return `${amount.toFixed(2)} (-${percent.toFixed(1)}%)`; } -function storeLabel(storeId) { +function storeLabel(storeId: string): string { return storeLookup.value[storeId] || storeId || "Inconnu"; } @@ -1667,15 +1772,15 @@ function cycleTheme() { theme.value = themes[(index + 1) % themes.length]; } -function openProductDetails(productId) { +function openProductDetails(productId: number): void { activeProductId.value = productId; } -function selectProduct(productId) { +function selectProduct(productId: number): void { openProductDetails(productId); } -function toggleCompare(productId) { +function toggleCompare(productId: number): void { if (compareIds.value.includes(productId)) { compareIds.value = compareIds.value.filter((id) => id !== productId); return; @@ -1686,7 +1791,7 @@ function toggleCompare(productId) { compareIds.value.push(productId); } -function removeFilter(key) { +function removeFilter(key: string): void { if (key === "store") { filters.value.store = "all"; return; @@ -1735,13 +1840,13 @@ function addStore() { storeDraftDomain.value = ""; } -function removeStore(storeId) { +function removeStore(storeId: string): void { stores.value = stores.value.filter((store) => store.id !== storeId); } -function mergeStoresFromProducts(items) { +function mergeStoresFromProducts(items: UIProduct[]): void { const known = new Set(stores.value.map((store) => store.id)); - const additions = []; + const additions: Store[] = []; items.forEach((product) => { if (product.storeId && !known.has(product.storeId)) { known.add(product.storeId); @@ -1875,15 +1980,18 @@ function formatHistoryDateLabel(value: number | string) { return `J-${diff}`; } -function parseHistoryTimestamp(raw) { +function parseHistoryTimestamp(raw: unknown): number | null { if (!raw) { return null; } if (typeof raw === "number") { return raw; } - const parsed = Date.parse(raw); - return Number.isNaN(parsed) ? null : parsed; + if (typeof raw === "string") { + const parsed = Date.parse(raw); + return Number.isNaN(parsed) ? null : parsed; + } + return null; } type NormalizedHistoryEntry = { @@ -2007,9 +2115,9 @@ function getCardHistorySnapshot(product: any): CardHistorySnapshot { }; } -function formatRelativeTimeAgo(timestamp: number | null) { - if (!Number.isFinite(timestamp ?? NaN)) { - return "à l’instant"; +function formatRelativeTimeAgo(timestamp: number | null): string { + if (timestamp === null || !Number.isFinite(timestamp)) { + return "à l'instant"; } const diff = Date.now() - timestamp; if (diff < 60_000) { @@ -2110,7 +2218,7 @@ function handlePopupLeave() { scheduleHideHistory(0); } -function applyStoreIconSize(value) { +function applyStoreIconSize(value: number): void { if (typeof document === "undefined") { return; } @@ -2118,7 +2226,7 @@ function applyStoreIconSize(value) { document.documentElement.style.setProperty("--pw-store-icon", `${size}px`); } -function applyCardRatio(value) { +function applyCardRatio(value: number): void { if (typeof document === "undefined") { return; } @@ -2130,7 +2238,7 @@ function applyCardRatio(value) { document.documentElement.style.setProperty("--pw-card-mobile-height-factor", `${mobileFactor}`); } -function applyCardImageHeight(value) { +function applyCardImageHeight(value: number): void { if (typeof document === "undefined") { return; } @@ -2140,23 +2248,55 @@ function applyCardImageHeight(value) { document.documentElement.style.setProperty("--pw-card-media-height", `${clamped}px`); } -function storeLogo(storeId) { - return getStoreLogo(storeId) || null; +function computeCardColumnsMax(width: number): number { + if (width <= 640) { + return 1; + } + if (width <= 900) { + return 2; + } + return MAX_CARD_COLUMNS; } -function storeInitials(storeId) { +function updateCardColumnsMax(): void { + if (typeof window === "undefined") { + return; + } + const nextMax = computeCardColumnsMax(window.innerWidth); + cardColumnsMax.value = nextMax; + if (settings.value.cardColumns > nextMax) { + settings.value.cardColumns = nextMax; + } +} + +function applyCardColumns(value: number): void { + if (typeof document === "undefined") { + return; + } + const numeric = Number(value); + const columns = Number.isFinite(numeric) ? numeric : DEFAULT_CARD_COLUMNS; + const maxColumns = Math.min(MAX_CARD_COLUMNS, cardColumnsMax.value); + const clamped = Math.min(maxColumns, Math.max(MIN_CARD_COLUMNS, columns)); + document.documentElement.style.setProperty("--pw-card-columns", `${clamped}`); +} + +function storeLogo(storeId: string): string | undefined { + return getStoreLogo(storeId) || undefined; +} + +function storeInitials(storeId: string): string { const label = storeLabel(storeId); if (!label) { return "NA"; } return label .split(" ") - .map((part) => part.charAt(0).toUpperCase()) + .map((part: string) => part.charAt(0).toUpperCase()) .join("") .slice(0, 2); } -function formatDate(value) { +function formatDate(value: string | null | undefined): string { if (!value) { return "n/a"; } @@ -2167,7 +2307,7 @@ function formatDate(value) { } } -function resolveApiUrl(path) { +function resolveApiUrl(path: string): string { const base = settings.value.apiBase || "/api"; if (base.startsWith("http")) { return new URL(base.replace(/\/$/, "") + path).toString(); @@ -2176,8 +2316,8 @@ function resolveApiUrl(path) { return `${basePath}${path}`; } -function apiHeaders() { - const headers = { +function apiHeaders(): Record { + const headers: Record = { "Content-Type": "application/json", }; if (settings.value.apiToken) { @@ -2186,33 +2326,50 @@ function apiHeaders() { return headers; } -function mapProduct(apiProduct) { - const meta = productMeta.value[apiProduct.id] || {}; - const imageUrl = Array.isArray(apiProduct.images) && apiProduct.images.length > 0 - ? apiProduct.images[0] - : placeholderImage; - const history = Array.isArray(apiProduct.history) ? apiProduct.history : []; +function mapProduct(apiProduct: Record): UIProduct { + const productId = apiProduct.id as number; + const meta = productMeta.value[productId] || {}; + const images = apiProduct.images as string[] | undefined; + const galleryImages = Array.isArray(apiProduct.gallery_images) ? apiProduct.gallery_images as string[] : []; + const mainImage = (apiProduct.main_image as string | undefined) || + (Array.isArray(images) && images.length > 0 ? images[0] : undefined); + const imageUrl = mainImage || placeholderImage; + const history = Array.isArray(apiProduct.history) ? apiProduct.history as HistoryEntry[] : []; return { - id: apiProduct.id, - title: apiProduct.title || apiProduct.reference || "Sans titre", - storeId: apiProduct.source || "unknown", - reference: apiProduct.reference || "", - price: apiProduct.latest_price ?? null, + id: productId, + title: (apiProduct.title as string) || (apiProduct.reference as string) || "Sans titre", + storeId: (apiProduct.source as string) || "unknown", + reference: (apiProduct.reference as string) || "", + asin: (apiProduct.asin as string) || (apiProduct.reference as string) || "", + price: (apiProduct.latest_price as number | null) ?? null, delta: 0, - stockStatus: apiProduct.latest_stock_status || "unknown", - updatedAt: apiProduct.last_updated_at || "", - updatedAtLabel: formatDate(apiProduct.last_updated_at), + stockStatus: (apiProduct.latest_stock_status as string) || "unknown", + stockText: (apiProduct.stock_text as string) || "", + inStock: (apiProduct.in_stock as boolean) ?? undefined, + updatedAt: (apiProduct.last_updated_at as string) || "", + updatedAtLabel: formatDate(apiProduct.last_updated_at as string | null), refreshHours: meta.refreshHours ?? 24, - url: apiProduct.url || "", - category: apiProduct.category || "", - currency: apiProduct.currency || "EUR", - description: apiProduct.description || "", - specs: apiProduct.specs || {}, - msrp: apiProduct.msrp ?? null, - discountAmount: apiProduct.discount_amount ?? null, - discountPercent: apiProduct.discount_percent ?? null, + url: (apiProduct.url as string) || "", + category: (apiProduct.category as string) || "", + type: (apiProduct.type as string) || "", + currency: (apiProduct.currency as string) || "EUR", + description: (apiProduct.description as string) || "", + specs: (apiProduct.specs as Record) || {}, + msrp: (apiProduct.msrp as number | null) ?? null, + discountAmount: (apiProduct.discount_amount as number | null) ?? null, + discountPercent: (apiProduct.discount_percent as number | null) ?? null, + discountText: (apiProduct.discount_text as string) || "", imageWebp: imageUrl, imageJpg: imageUrl, + images: images || [], + mainImage: (apiProduct.main_image as string) || imageUrl, + galleryImages: galleryImages, + ratingValue: (apiProduct.rating_value as number) ?? undefined, + ratingCount: (apiProduct.rating_count as number) ?? undefined, + amazonChoice: (apiProduct.amazon_choice as boolean) ?? undefined, + amazonChoiceLabel: (apiProduct.amazon_choice_label as string) || "", + modelNumber: (apiProduct.model_number as string) || "", + modelName: (apiProduct.model_name as string) || "", history, }; } @@ -2247,6 +2404,10 @@ async function fetchProducts() { const mapped = payload.map(mapProduct); products.value = mapped; mergeStoresFromProducts(mapped); + if (!autoScheduleDone.value && mapped.length > 0) { + autoScheduleDone.value = true; + scheduleAllProducts(mapped); + } if (activeProductId.value && !products.value.find((item) => item.id === activeProductId.value)) { activeProductId.value = products.value[0]?.id ?? null; } @@ -2258,6 +2419,94 @@ async function fetchProducts() { } } +async function scheduleAllProducts(list: UIProduct[]): Promise { + for (const product of list) { + if (product.url && product.refreshHours) { + await scheduleProduct(product.url, product.refreshHours); + } + } +} + +function normalizeKeywordList(raw: string): string[] { + return raw + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +async function fetchClassificationRules(): Promise { + try { + const response = await fetch(resolveApiUrl("/classification/rules"), { + headers: apiHeaders(), + }); + if (!response.ok) { + throw new Error(`API ${response.status}`); + } + classificationRules.value = await response.json(); + } catch (error) { + addLog("ERROR", "backend", "Impossible de charger les regles de classification"); + } +} + +async function fetchClassificationOptions(): Promise { + try { + const response = await fetch(resolveApiUrl("/classification/options"), { + headers: apiHeaders(), + }); + if (!response.ok) { + throw new Error(`API ${response.status}`); + } + classificationOptions.value = await response.json(); + } catch (error) { + classificationOptions.value = { categories: [], types: [] }; + } +} + +async function addClassificationRule(): Promise { + const keywords = normalizeKeywordList(classificationDraft.value.keywords); + if (!keywords.length) { + addLog("WARN", "frontend", "Ajouter au moins un mot-cle"); + return; + } + try { + const response = await fetch(resolveApiUrl("/classification/rules"), { + method: "POST", + headers: apiHeaders(), + body: JSON.stringify({ + category: classificationDraft.value.category || null, + type: classificationDraft.value.type || null, + keywords, + sort_order: Number(classificationDraft.value.sortOrder) || 0, + is_active: true, + }), + }); + if (!response.ok) { + throw new Error(`API ${response.status}`); + } + classificationDraft.value = { category: "", type: "", keywords: "", sortOrder: 0 }; + await fetchClassificationRules(); + await fetchClassificationOptions(); + } catch (error) { + addLog("ERROR", "backend", "Impossible d'ajouter la regle de classification"); + } +} + +async function deleteClassificationRule(ruleId: number): Promise { + try { + const response = await fetch(resolveApiUrl(`/classification/rules/${ruleId}`), { + method: "DELETE", + headers: apiHeaders(), + }); + if (!response.ok) { + throw new Error(`API ${response.status}`); + } + await fetchClassificationRules(); + await fetchClassificationOptions(); + } catch (error) { + addLog("ERROR", "backend", "Impossible de supprimer la regle de classification"); + } +} + async function saveActiveProduct() { if (!activeProduct.value) { return; @@ -2266,6 +2515,7 @@ async function saveActiveProduct() { url: activeProduct.value.url, title: activeProduct.value.title, category: activeProduct.value.category, + type: activeProduct.value.type, currency: activeProduct.value.currency, }; try { @@ -2278,13 +2528,16 @@ async function saveActiveProduct() { throw new Error(`API ${response.status}`); } await fetchProducts(); + if (activeProduct.value.url && activeProduct.value.refreshHours) { + await scheduleProduct(activeProduct.value.url, activeProduct.value.refreshHours); + } } catch (error) { apiState.value.error = error instanceof Error ? error.message : "Erreur API"; addLog("ERROR", "frontend", apiState.value.error); } } -async function refreshProduct(productId) { +async function refreshProduct(productId: number): Promise { const product = products.value.find((item) => item.id === productId); if (!product || !product.url) { addLog("WARN", "frontend", "Impossible de relancer le scrap: URL manquante"); @@ -2318,7 +2571,26 @@ async function refreshProduct(productId) { } } -async function deleteProduct(productId) { +async function scheduleProduct(url: string, intervalHours: number): Promise { + try { + const response = await fetch(resolveApiUrl("/schedule"), { + method: "POST", + headers: apiHeaders(), + body: JSON.stringify({ + url, + interval_hours: Math.max(1, Math.round(intervalHours)), + save_db: true, + }), + }); + if (!response.ok) { + throw new Error(`API ${response.status}`); + } + } catch (error) { + addLog("WARN", "backend", "Planification automatique impossible"); + } +} + +async function deleteProduct(productId: number): Promise { try { const response = await fetch(resolveApiUrl(`/products/${productId}`), { method: "DELETE", @@ -2352,7 +2624,7 @@ async function deleteActiveProduct() { await deleteProduct(activeProduct.value.id); } -function openProductUrl(product) { +function openProductUrl(product: UIProduct | undefined): void { if (!product?.url) { addLog("WARN", "frontend", "Aucune URL disponible"); return; @@ -2415,7 +2687,7 @@ async function previewAddProduct() { } addProductState.value.snapshot = payload.snapshot; const imageCount = Array.isArray(payload.snapshot.images) ? payload.snapshot.images.length : 0; - addProductSelectedImages.value = imageCount ? [0] : []; + addProductSelectedImageIndex.value = imageCount ? 0 : null; addLog("INFO", "backend", "Preview scraping OK"); } catch (error) { addProductState.value.error = error instanceof Error ? error.message : "Erreur API"; @@ -2429,19 +2701,38 @@ async function commitAddProduct() { if (!addProductState.value.snapshot) { return; } + const selectedImage = + addProductSelectedImageIndex.value !== null + ? addProductImages.value[addProductSelectedImageIndex.value] + : addProductImages.value[0]; + const snapshotPayload = { + ...addProductState.value.snapshot, + images: selectedImage ? [selectedImage] : [], + main_image: selectedImage || null, + gallery_images: [], + }; try { const response = await fetch(resolveApiUrl("/scrape/commit"), { method: "POST", headers: apiHeaders(), body: JSON.stringify({ - snapshot: addProductState.value.snapshot, + snapshot: snapshotPayload, }), }); if (!response.ok) { throw new Error(`API ${response.status}`); } addProductOpen.value = false; + const payload = await response.json(); await fetchProducts(); + const productId = payload?.product_id as number | undefined; + const addedUrl = addProductDraft.value.url; + const interval = productId && productMeta.value[productId]?.refreshHours + ? productMeta.value[productId]?.refreshHours + : 24; + if (addedUrl) { + await scheduleProduct(addedUrl, interval); + } addLog("INFO", "backend", "Produit enregistre"); } catch (error) { addProductState.value.error = error instanceof Error ? error.message : "Erreur API"; @@ -2535,11 +2826,11 @@ function loadStores() { } } -function persistProductMeta() { +function persistProductMeta(): void { if (typeof window === "undefined") { return; } - const payload = {}; + const payload: Record = {}; products.value.forEach((product) => { payload[product.id] = { refreshHours: product.refreshHours, @@ -2578,7 +2869,7 @@ async function fetchVersion() { } } -async function loadLogs() { +async function loadLogs(): Promise { backendLogs.value = []; uvicornLogs.value = []; try { @@ -2587,25 +2878,33 @@ async function loadLogs() { const response = await fetch(url.toString(), { headers: apiHeaders() }); if (response.ok) { const payload = await response.json(); - backendLogs.value = payload.map((entry, index) => ({ - id: `backend-${entry.id}-${index}`, - time: formatDate(entry.fetched_at), - level: entry.fetch_status?.toUpperCase() || "INFO", - source: "backend", - message: `${entry.source} ${entry.url} (${entry.fetch_method})`, - })); + backendLogs.value = payload.map((entry: Record, index: number) => { + const notes = Array.isArray(entry.notes) ? entry.notes.filter(Boolean) : []; + const errors = Array.isArray(entry.errors) ? entry.errors.filter(Boolean) : []; + const extras: string[] = []; + if (notes.length) { + extras.push(`Notes: ${notes.join(" / ")}`); + } + if (errors.length) { + extras.push(`Erreurs: ${errors.join(" / ")}`); + } + const extraText = extras.length ? ` • ${extras.join(" • ")}` : ""; + return { + time: formatDate(entry.fetched_at as string | null), + level: ((entry.fetch_status as string) || "info").toUpperCase(), + message: `${entry.source} ${entry.url} (${entry.fetch_method})${extraText}`, + }; + }); } const backendResponse = await fetch(resolveApiUrl("/logs/backend"), { headers: apiHeaders(), }); if (backendResponse.ok) { const backendPayload = await backendResponse.json(); - const backendEntries = backendPayload.map((entry, index) => ({ - id: `backend-log-${index}`, - time: formatDate(entry.time), - level: entry.level, - source: "backend", - message: entry.message, + const backendEntries = backendPayload.map((entry: Record) => ({ + time: formatDate(entry.time as string | null), + level: (entry.level as string) || "INFO", + message: (entry.message as string) || "", })); backendLogs.value = [...backendEntries, ...backendLogs.value]; } @@ -2614,12 +2913,8 @@ async function loadLogs() { }); if (uvicornResponse.ok) { const uvicornPayload = await uvicornResponse.json(); - uvicornLogs.value = uvicornPayload.map((entry, index) => ({ - id: `uvicorn-log-${index}`, - time: "", - level: "INFO", - source: "uvicorn", - message: entry.line, + uvicornLogs.value = uvicornPayload.map((entry: Record) => ({ + line: (entry.line as string) || "", })); } } catch (error) { @@ -2647,7 +2942,7 @@ async function copyLogs() { } } -function addLog(level, source, message) { +function addLog(level: string, source: string, message: string): void { logEntries.value.unshift({ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, time: new Date().toLocaleTimeString("fr-FR"), @@ -2764,11 +3059,25 @@ watch( { immediate: true } ); +watch( + () => settings.value.cardColumns, + (value) => { + applyCardColumns(value); + }, + { immediate: true } +); + onMounted(() => { fetchProducts(); startAutoRefresh(); fetchHealth(); fetchVersion(); + fetchClassificationRules(); + fetchClassificationOptions(); + updateCardColumnsMax(); + if (typeof window !== "undefined") { + window.addEventListener("resize", updateCardColumnsMax); + } }); onBeforeUnmount(() => { @@ -2781,5 +3090,8 @@ onBeforeUnmount(() => { if (scrapeLog.value.hideTimer) { window.clearTimeout(scrapeLog.value.hideTimer); } + if (typeof window !== "undefined") { + window.removeEventListener("resize", updateCardColumnsMax); + } }); diff --git a/webui/src/assets/stores/aliexpress.svg b/webui/src/assets/stores/aliexpress.svg index 010d139..2bb1887 100644 --- a/webui/src/assets/stores/aliexpress.svg +++ b/webui/src/assets/stores/aliexpress.svg @@ -1,4 +1,51 @@ - - - AE - + +AE diff --git a/webui/src/assets/stores/amazon.svg b/webui/src/assets/stores/amazon.svg index 0830c07..e36e835 100644 --- a/webui/src/assets/stores/amazon.svg +++ b/webui/src/assets/stores/amazon.svg @@ -1,4 +1,51 @@ - - - AM - + +AM diff --git a/webui/src/assets/stores/backmarket.svg b/webui/src/assets/stores/backmarket.svg index a588839..0da1c0f 100644 --- a/webui/src/assets/stores/backmarket.svg +++ b/webui/src/assets/stores/backmarket.svg @@ -1,4 +1,51 @@ - - - BM - + +BM diff --git a/webui/src/assets/stores/cdiscount.svg b/webui/src/assets/stores/cdiscount.svg index 72143fa..e3f0319 100644 --- a/webui/src/assets/stores/cdiscount.svg +++ b/webui/src/assets/stores/cdiscount.svg @@ -1,4 +1,51 @@ - - - CD - + +CD diff --git a/webui/src/components/CardActions.vue b/webui/src/components/CardActions.vue new file mode 100644 index 0000000..c874cb7 --- /dev/null +++ b/webui/src/components/CardActions.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/webui/src/components/MiniLineChart.vue b/webui/src/components/MiniLineChart.vue index 3b051d0..8ffbf6e 100644 --- a/webui/src/components/MiniLineChart.vue +++ b/webui/src/components/MiniLineChart.vue @@ -78,11 +78,9 @@ const yBounds = computed(() => { const values = validPoints.value.map((item) => item.v); const rawMin = Math.min(...values); const rawMax = Math.max(...values); - const delta = Math.max(rawMax - rawMin, 1); - const pad = delta * 0.05; return { - min: rawMin - pad, - max: rawMax + pad, + min: rawMin, + max: rawMax, }; }); @@ -132,21 +130,14 @@ const chartPoints = computed(() => { const hasPoints = computed(() => chartPoints.value.length > 0); const linePoints = computed(() => { - if (!chartPoints.value.length) { + if (chartPoints.value.length <= 1) { return []; } - if (chartPoints.value.length === 1) { - const point = chartPoints.value[0]; - const endX = margins.left + chartDimensions.value.width; - return [ - { x: margins.left, y: point.y }, - { x: endX, y: point.y }, - ]; - } return chartPoints.value; }); const polylinePoints = computed(() => linePoints.value.map((point) => `${point.x},${point.y}`).join(" ")); +const showLine = computed(() => linePoints.value.length > 1); const yTickValues = computed(() => { const count = Math.max(2, props.yTicks); @@ -256,6 +247,7 @@ const placeholderLabel = computed(() => ""); +import { computed } from 'vue' + +const props = defineProps<{ + price: number | null + currency: string + msrp?: number | null + discountAmount?: number | null + discountPercent?: number | null + discountText?: string | null + deltaLabel?: string | null + deltaLabelTitle?: string | null + stockStatus: string + stockText?: string | null + inStock?: boolean | null + reference?: string | null + url?: string | null + ratingValue?: number | null + ratingCount?: number | null + amazonChoice?: boolean | null + compact?: boolean +}>() + +const formatPrice = (value: number | null, currency: string): string => { + if (value === null || value === undefined || !Number.isFinite(value)) { + return '—' + } + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: currency || 'EUR', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value) +} + +const formatShortPrice = (value: number | null, currency: string): string => { + if (value === null || value === undefined || !Number.isFinite(value)) { + return '—' + } + const numeric = Number(value) + const rounded = Math.round(numeric * 100) / 100 + const centsValue = Math.round(rounded * 100) - Math.floor(rounded) * 100 + const hasCents = centsValue !== 0 + if ((currency || 'EUR') === 'EUR') { + const euros = Math.floor(rounded) + const cents = String(Math.abs(centsValue)).padStart(2, '0') + return hasCents ? `${euros}€${cents}` : `${euros}€` + } + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: currency || 'EUR', + minimumFractionDigits: hasCents ? 2 : 0, + maximumFractionDigits: hasCents ? 2 : 0, + }).format(rounded) +} + +type PriceParts = { + euros: string + cents: string | null + symbol: string + raw: string | null +} + +const formatPriceParts = (value: number | null, currency: string): PriceParts => { + if (value === null || value === undefined || !Number.isFinite(value)) { + return { euros: '', cents: null, symbol: '', raw: '—' } + } + const numeric = Number(value) + const rounded = Math.round(numeric * 100) / 100 + const centsValue = Math.round(rounded * 100) - Math.floor(rounded) * 100 + const hasCents = centsValue !== 0 + if ((currency || 'EUR') !== 'EUR') { + return { euros: '', cents: null, symbol: '', raw: formatShortPrice(rounded, currency) } + } + return { + euros: String(Math.floor(rounded)), + cents: hasCents ? String(Math.abs(centsValue)).padStart(2, '0') : null, + symbol: '€', + raw: null, + } +} + +const formattedPrice = computed(() => formatPriceParts(props.price, props.currency)) +const formattedMsrp = computed(() => (props.msrp ? formatPriceParts(props.msrp, props.currency) : null)) + +const discountDisplay = computed(() => { + if (props.discountText?.trim()) { + return props.discountText.trim() + } + if (props.discountAmount === null || props.discountAmount === undefined || + props.discountPercent === null || props.discountPercent === undefined) { + return null + } + const amount = formatShortPrice(props.discountAmount, props.currency) + const percent = Math.round(props.discountPercent) + return `-${percent}% (${amount})` +}) + +const stockLabel = computed(() => { + if (props.stockText?.trim()) { + return props.stockText.trim() + } + const map: Record = { + in_stock: 'En stock', + out_of_stock: 'Rupture', + unknown: 'Inconnu', + error: 'Erreur', + } + return map[props.stockStatus] || props.stockStatus +}) + +const stockClass = computed(() => { + if (props.inStock === true) return 'text-[var(--success)]' + if (props.inStock === false) return 'text-[var(--danger)]' + if (props.stockStatus === 'in_stock') return 'text-[var(--success)]' + if (props.stockStatus === 'out_of_stock') return 'text-[var(--danger)]' + return 'text-[var(--muted)]' +}) + +const deltaDisplay = computed(() => { + if (props.deltaLabel && props.deltaLabel.trim()) { + return props.deltaLabel + } + return '—' +}) + +const deltaTitle = computed(() => { + if (props.deltaLabelTitle && props.deltaLabelTitle.trim()) { + return props.deltaLabelTitle + } + return 'Evol.' +}) + +const ratingDisplay = computed(() => { + if (props.ratingValue === null || props.ratingValue === undefined) { + return '—' + } + const value = props.ratingValue.toFixed(1).replace('.', ',') + if (props.ratingCount === null || props.ratingCount === undefined) { + return value + } + const count = new Intl.NumberFormat('fr-FR').format(props.ratingCount) + return `${value} (${count})` +}) + +const amazonChoiceDisplay = computed(() => { + if (props.amazonChoice === true) return 'Oui' + if (props.amazonChoice === false) return '—' + return '—' +}) + + + + + diff --git a/webui/src/components/PriceHistoryHover.vue b/webui/src/components/PriceHistoryHover.vue index 53ab66b..ffdbb6d 100644 --- a/webui/src/components/PriceHistoryHover.vue +++ b/webui/src/components/PriceHistoryHover.vue @@ -119,7 +119,7 @@ const emit = defineEmits<{ }>(); const popupStyle = computed(() => ({ - position: "fixed", + position: "fixed" as const, top: `${props.position.top}px`, left: `${props.position.left}px`, width: "280px", diff --git a/webui/src/components/ProductCard.vue b/webui/src/components/ProductCard.vue new file mode 100644 index 0000000..5f97a7f --- /dev/null +++ b/webui/src/components/ProductCard.vue @@ -0,0 +1,600 @@ + + + + + diff --git a/webui/src/components/ProductSummary.vue b/webui/src/components/ProductSummary.vue new file mode 100644 index 0000000..6108b92 --- /dev/null +++ b/webui/src/components/ProductSummary.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/webui/src/index.css b/webui/src/index.css index 961ca9a..f5e664f 100644 --- a/webui/src/index.css +++ b/webui/src/index.css @@ -7,7 +7,8 @@ --pw-store-icon: 40px; --pw-card-height-factor: 1; --pw-card-mobile-height-factor: 1; - --pw-card-media-height: 160px; + --pw-card-media-height: 140px; + --pw-card-columns: 3; } .app-root { @@ -138,14 +139,12 @@ background: var(--surface); border-radius: var(--radius); border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 16px 32px var(--shadow); + box-shadow: 0 12px 24px var(--shadow); display: flex; flex-direction: column; gap: 12px; - min-height: calc(470px * var(--pw-card-height-factor, 1)); - padding: 24px; + padding: 16px; position: relative; - padding-bottom: 90px; } .card-thumbnail { @@ -403,6 +402,25 @@ box-shadow: 0 10px 30px rgba(254, 128, 25, 0.2); } +/* Stock status colors */ +.status-in_stock, +.status-in-stock { + color: var(--success); +} + +.status-out_of_stock, +.status-out-of-stock { + color: var(--danger); +} + +.status-unknown { + color: var(--muted); +} + +.status-error { + color: var(--danger); +} + .density-dense .card { padding: 12px; } @@ -616,6 +634,36 @@ min-width: 280px; } +.add-product-modal { + display: flex; + flex-direction: column; + max-height: 80vh; + overflow: hidden; +} + +.add-product-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.add-product-modal__body { + padding: 16px 24px; + overflow-y: auto; +} + +.add-product-modal__footer { + display: flex; + gap: 12px; + padding: 12px 24px 18px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: var(--surface); + position: sticky; + bottom: 0; +} + .image-toggle { border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; @@ -636,6 +684,44 @@ box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); } +.add-product-carousel { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(88px, 1fr); + gap: 8px; + overflow-x: auto; + padding-bottom: 6px; + scroll-snap-type: x mandatory; +} + +.add-product-thumb { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + background: transparent; + padding: 4px; + scroll-snap-align: start; + cursor: pointer; + transition: border 0.15s ease, transform 0.15s ease; +} + +.add-product-thumb:hover { + border-color: rgba(254, 128, 25, 0.6); + transform: translateY(-1px); +} + +.add-product-thumb.selected { + border-color: rgba(254, 128, 25, 0.9); + box-shadow: 0 6px 16px rgba(254, 128, 25, 0.2); +} + +.add-product-thumb__image { + width: 100%; + height: 72px; + object-fit: cover; + border-radius: 8px; + display: block; +} + .log-status-panel { border-color: rgba(255, 255, 255, 0.1); } @@ -777,8 +863,8 @@ .product-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 32px; + grid-template-columns: repeat(var(--pw-card-columns, 3), minmax(0, 1fr)); + gap: 24px; } @media (max-width: 1024px) { @@ -790,6 +876,18 @@ } } +@media (max-width: 1200px) { + .product-grid { + --pw-card-columns: min(var(--pw-card-columns, 3), 3); + } +} + +@media (max-width: 900px) { + .product-grid { + --pw-card-columns: min(var(--pw-card-columns, 3), 2); + } +} + @media (max-width: 640px) { .app-header .toolbar-text { display: none; @@ -800,6 +898,7 @@ } .product-grid { grid-template-columns: 1fr; + --pw-card-columns: 1; } .card { min-height: calc(470px * var(--pw-card-mobile-height-factor, 1)); diff --git a/webui/src/types/api.ts b/webui/src/types/api.ts index 59e80ec..1e8dfc4 100644 --- a/webui/src/types/api.ts +++ b/webui/src/types/api.ts @@ -18,12 +18,23 @@ export interface Product { id: number source: string reference: string + asin: string | null url: string title: string | null category: string | null + type: string | null description: string | null currency: string | null msrp: number | null + rating_value: number | null + rating_count: number | null + amazon_choice: boolean | null + amazon_choice_label: string | null + discount_text: string | null + stock_text: string | null + in_stock: boolean | null + model_number: string | null + model_name: string | null first_seen_at: string last_updated_at: string latest_price: number | null @@ -31,6 +42,8 @@ export interface Product { latest_stock_status: StockStatus | null latest_fetched_at: string | null images: string[] + main_image: string | null + gallery_images: string[] specs: Record discount_amount: number | null discount_percent: number | null @@ -43,6 +56,7 @@ export interface ProductCreate { url: string title?: string | null category?: string | null + type?: string | null description?: string | null currency?: string | null msrp?: number | null @@ -52,6 +66,7 @@ export interface ProductUpdate { url?: string | null title?: string | null category?: string | null + type?: string | null description?: string | null currency?: string | null msrp?: number | null @@ -188,10 +203,23 @@ export interface ProductSnapshot { currency: string | null shipping_cost: number | null stock_status: StockStatus | null + stock_text: string | null + in_stock: boolean | null reference: string | null + asin: string | null category: string | null + type: string | null description: string | null + rating_value: number | null + rating_count: number | null + amazon_choice: boolean | null + amazon_choice_label: string | null + discount_text: string | null + model_number: string | null + model_name: string | null images: string[] + main_image: string | null + gallery_images: string[] specs: Record msrp: number | null debug: DebugInfo diff --git a/webui/src/types/app.ts b/webui/src/types/app.ts index 3870b6c..159f227 100644 --- a/webui/src/types/app.ts +++ b/webui/src/types/app.ts @@ -46,6 +46,7 @@ export interface FilterChip { // === Settings === export interface AppSettings { cardRatio: number + cardColumns: number imageHeight: number imageMode: ImageMode fontSize: number @@ -87,12 +88,12 @@ export interface ProductMeta { export type ProductMetaMap = Record // === Scrape Log === -export type LogLevel = 'debug' | 'info' | 'success' | 'warning' | 'error' +export type ScrapeLogLevel = 'debug' | 'info' | 'success' | 'warning' | 'error' export interface ScrapeLogEntry { id: number time: string - level: LogLevel + level: ScrapeLogLevel text: string } @@ -152,7 +153,7 @@ export type LogTab = 'frontend' | 'backend' | 'uvicorn' export interface FrontendLog { id: number time: string - level: LogLevel + level: ScrapeLogLevel message: string } @@ -164,6 +165,9 @@ export interface CardRatioPreset { // === Constants (exportés pour réutilisation) === export const DEFAULT_CARD_RATIO = 1 +export const DEFAULT_CARD_COLUMNS = 3 +export const MIN_CARD_COLUMNS = 1 +export const MAX_CARD_COLUMNS = 6 export const DEFAULT_IMAGE_HEIGHT = 160 export const CARD_HISTORY_LIMIT = 12 export const DEFAULT_LOG_DURATION = 2500 @@ -178,7 +182,7 @@ export const CARD_RATIO_PRESETS: CardRatioPreset[] = [ export const IMAGE_MODES: ImageMode[] = ['contain', 'cover'] -export const LOG_ICONS: Record = { +export const LOG_ICONS: Record = { debug: '🔍', info: 'ℹ️', success: '✅', diff --git a/webui/tsconfig.json b/webui/tsconfig.json index b6865e6..826470b 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2021", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2021", "DOM", "DOM.Iterable"], "skipLibCheck": true, "strict": true, "noEmit": true,