Refactor worker management and enhance configuration options
- Updated docker-compose.yml to allow read-write access for workers.json. - Introduced WorkerManager class in wallamonitor.py to manage workers dynamically based on workers.json configuration. - Enhanced Worker class to support controlled start/stop operations and updated general arguments. - Improved Workers.vue to include a general configuration modal and refined UI for active and disabled workers. - Added functionality for global filters and improved worker editing capabilities. - Implemented methods for saving general configuration and deleting workers.
This commit is contained in:
@@ -81,7 +81,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
# Montar archivos de configuración
|
# Montar archivos de configuración
|
||||||
- ./config.yaml:/app/config.yaml:ro
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
- ./workers.json:/app/workers.json:ro
|
- ./workers.json:/app/workers.json:rw
|
||||||
# Montar directorio de logs en lugar del archivo para evitar problemas
|
# Montar directorio de logs en lugar del archivo para evitar problemas
|
||||||
- ./logs:/app/logs:rw
|
- ./logs:/app/logs:rw
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
|
import threading
|
||||||
from platforms.platform_factory import PlatformFactory
|
from platforms.platform_factory import PlatformFactory
|
||||||
from managers.worker_conditions import WorkerConditions
|
from managers.worker_conditions import WorkerConditions
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ class Worker:
|
|||||||
self._general_args = general_args
|
self._general_args = general_args
|
||||||
self._queue_manager = queue_manager
|
self._queue_manager = queue_manager
|
||||||
self._worker_conditions = WorkerConditions(item_to_monitor, general_args)
|
self._worker_conditions = WorkerConditions(item_to_monitor, general_args)
|
||||||
|
self._running = True
|
||||||
|
self._stop_event = threading.Event()
|
||||||
# Initialize the platform based on item_to_monitor configuration
|
# Initialize the platform based on item_to_monitor configuration
|
||||||
platform_name = self._item_monitoring.get_platform()
|
platform_name = self._item_monitoring.get_platform()
|
||||||
try:
|
try:
|
||||||
@@ -25,36 +28,76 @@ class Worker:
|
|||||||
# Initialize the queue with existing articles
|
# Initialize the queue with existing articles
|
||||||
self._queue_manager.add_to_notified_articles(self._request_articles())
|
self._queue_manager.add_to_notified_articles(self._request_articles())
|
||||||
|
|
||||||
|
def update_general_args(self, general_args):
|
||||||
|
"""Actualiza los argumentos generales del worker"""
|
||||||
|
self._general_args = general_args
|
||||||
|
self._worker_conditions = WorkerConditions(self._item_monitoring, general_args)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Detiene el worker de forma controlada"""
|
||||||
|
self.logger.info(f"Deteniendo worker: {self._item_monitoring.get_name()}")
|
||||||
|
self._running = False
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
def is_running(self):
|
||||||
|
"""Verifica si el worker está corriendo"""
|
||||||
|
return self._running
|
||||||
|
|
||||||
def _request_articles(self):
|
def _request_articles(self):
|
||||||
return self._platform.fetch_articles()
|
return self._platform.fetch_articles()
|
||||||
|
|
||||||
def work(self):
|
def work(self):
|
||||||
exec_times = []
|
exec_times = []
|
||||||
|
|
||||||
while True:
|
while self._running and not self._stop_event.is_set():
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
try:
|
||||||
articles = self._request_articles()
|
articles = self._request_articles()
|
||||||
for article in articles:
|
for article in articles:
|
||||||
|
if not self._running or self._stop_event.is_set():
|
||||||
|
break
|
||||||
if self._worker_conditions.meets_item_conditions(article):
|
if self._worker_conditions.meets_item_conditions(article):
|
||||||
try:
|
try:
|
||||||
self._queue_manager.add_to_queue(article, self._item_monitoring.get_name(), self._item_monitoring.get_thread_id())
|
self._queue_manager.add_to_queue(article, self._item_monitoring.get_name(), self._item_monitoring.get_thread_id())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"{self._item_monitoring.get_name()} worker crashed: {e}")
|
self.logger.error(f"{self._item_monitoring.get_name()} worker crashed: {e}")
|
||||||
time.sleep(self._item_monitoring.get_check_every())
|
|
||||||
exec_times.append(time.time() - start_time - self._item_monitoring.get_check_every())
|
if not self._running or self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
|
||||||
|
# Sleep con posibilidad de cancelación
|
||||||
|
check_every = self._item_monitoring.get_check_every()
|
||||||
|
sleep_time = 0
|
||||||
|
while sleep_time < check_every and self._running and not self._stop_event.is_set():
|
||||||
|
time.sleep(min(1.0, check_every - sleep_time))
|
||||||
|
sleep_time += 1.0
|
||||||
|
|
||||||
|
if exec_times:
|
||||||
|
exec_times.append(time.time() - start_time - check_every)
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Worker '{self._item_monitoring.get_name()}', "
|
f"Worker '{self._item_monitoring.get_name()}', "
|
||||||
f"Execution time stats - Last: {exec_times[-1]:.2f}s, Max: {max(exec_times):.2f}s, "
|
f"Execution time stats - Last: {exec_times[-1]:.2f}s, Max: {max(exec_times):.2f}s, "
|
||||||
f"Average: {sum(exec_times) / len(exec_times):.2f}s."
|
f"Average: {sum(exec_times) / len(exec_times):.2f}s."
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if self._running and not self._stop_event.is_set():
|
||||||
|
self.logger.error(f"Error en worker {self._item_monitoring.get_name()}: {e}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while self._running and not self._stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
platform_name = self._platform.get_platform_name()
|
platform_name = self._platform.get_platform_name()
|
||||||
self.logger.info(f"{platform_name.capitalize()} monitor worker started - {self._item_monitoring.get_name()}")
|
self.logger.info(f"{platform_name.capitalize()} monitor worker started - {self._item_monitoring.get_name()}")
|
||||||
self.work()
|
self.work()
|
||||||
|
|
||||||
|
# Si el worker se detuvo normalmente, salir
|
||||||
|
if not self._running or self._stop_event.is_set():
|
||||||
|
self.logger.info(f"Worker '{self._item_monitoring.get_name()}' detenido")
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if not self._running or self._stop_event.is_set():
|
||||||
|
break
|
||||||
self.logger.error(f"{''.join(traceback.format_exception(None, e, e.__traceback__))}")
|
self.logger.error(f"{''.join(traceback.format_exception(None, e, e.__traceback__))}")
|
||||||
self.logger.error(f"{self._item_monitoring.get_name()} worker crashed. Restarting worker...")
|
self.logger.error(f"{self._item_monitoring.get_name()} worker crashed. Restarting worker...")
|
||||||
time.sleep(ERROR_SLEEP_TIME)
|
time.sleep(ERROR_SLEEP_TIME)
|
||||||
|
|||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "wallamonitor",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
240
wallamonitor.py
240
wallamonitor.py
@@ -1,10 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor, Future
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import yaml
|
import yaml
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
from datalayer.item_monitor import ItemMonitor
|
from datalayer.item_monitor import ItemMonitor
|
||||||
from datalayer.general_monitor import GeneralMonitor
|
from datalayer.general_monitor import GeneralMonitor
|
||||||
@@ -119,23 +121,247 @@ def load_cache_config():
|
|||||||
'limit': 300
|
'limit': 300
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WorkerManager:
|
||||||
|
"""Gestiona workers dinámicamente, iniciando y deteniendo según workers.json"""
|
||||||
|
|
||||||
|
def __init__(self, general_args, queue_manager):
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self._general_args = general_args
|
||||||
|
self._queue_manager = queue_manager
|
||||||
|
self._executor = ThreadPoolExecutor(max_workers=1000)
|
||||||
|
self._workers = {} # Dict: worker_name -> {'worker': Worker, 'future': Future, 'item': ItemMonitor}
|
||||||
|
self._running = True
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
self._workers_path = os.path.join(base_dir, "workers.json")
|
||||||
|
|
||||||
|
def load_workers_config(self):
|
||||||
|
"""Carga la configuración de workers desde workers.json"""
|
||||||
|
try:
|
||||||
|
with open(self._workers_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
disabled = set(config.get('disabled', []))
|
||||||
|
items = []
|
||||||
|
for item_data in config.get('items', []):
|
||||||
|
item = ItemMonitor.load_from_json(item_data)
|
||||||
|
items.append((item, item.get_name() not in disabled))
|
||||||
|
general = GeneralMonitor.load_from_json(config.get('general', {}))
|
||||||
|
return items, general, disabled
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error cargando workers.json: {e}")
|
||||||
|
return [], GeneralMonitor([], [], [], [], []), set()
|
||||||
|
|
||||||
|
def start_worker(self, item, general_args):
|
||||||
|
"""Inicia un worker"""
|
||||||
|
try:
|
||||||
|
worker = Worker(item, general_args, self._queue_manager)
|
||||||
|
future = self._executor.submit(worker.run)
|
||||||
|
self._workers[item.get_name()] = {
|
||||||
|
'worker': worker,
|
||||||
|
'future': future,
|
||||||
|
'item': item
|
||||||
|
}
|
||||||
|
self.logger.info(f"Worker '{item.get_name()}' iniciado")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error iniciando worker '{item.get_name()}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_worker(self, worker_name):
|
||||||
|
"""Detiene un worker"""
|
||||||
|
if worker_name not in self._workers:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
worker_data = self._workers[worker_name]
|
||||||
|
worker = worker_data['worker']
|
||||||
|
future = worker_data['future']
|
||||||
|
|
||||||
|
# Detener el worker
|
||||||
|
worker.stop()
|
||||||
|
|
||||||
|
# Intentar cancelar el future si aún no está ejecutándose
|
||||||
|
if not future.done():
|
||||||
|
future.cancel()
|
||||||
|
|
||||||
|
# No esperamos a que termine, se detendrá automáticamente
|
||||||
|
del self._workers[worker_name]
|
||||||
|
self.logger.info(f"Worker '{worker_name}' detenido")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error deteniendo worker '{worker_name}': {e}")
|
||||||
|
# Asegurarse de eliminar la entrada aunque haya error
|
||||||
|
if worker_name in self._workers:
|
||||||
|
del self._workers[worker_name]
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sync_workers(self):
|
||||||
|
"""Sincroniza los workers con workers.json"""
|
||||||
|
items, general_args, disabled = self.load_workers_config()
|
||||||
|
|
||||||
|
# Actualizar general_args en todos los workers activos
|
||||||
|
old_general_args = self._general_args
|
||||||
|
self._general_args = general_args
|
||||||
|
|
||||||
|
current_worker_names = set(self._workers.keys())
|
||||||
|
enabled_worker_names = {item.get_name() for item, enabled in items if enabled}
|
||||||
|
|
||||||
|
# Actualizar general_args en workers existentes
|
||||||
|
with self._lock:
|
||||||
|
for worker_data in self._workers.values():
|
||||||
|
worker_data['worker'].update_general_args(general_args)
|
||||||
|
|
||||||
|
# Detener workers que ya no existen o están deshabilitados
|
||||||
|
to_stop = current_worker_names - enabled_worker_names
|
||||||
|
for worker_name in list(to_stop):
|
||||||
|
with self._lock:
|
||||||
|
if worker_name in self._workers:
|
||||||
|
worker = self._workers[worker_name]['worker']
|
||||||
|
worker.stop()
|
||||||
|
future = self._workers[worker_name]['future']
|
||||||
|
if not future.done():
|
||||||
|
future.cancel()
|
||||||
|
del self._workers[worker_name]
|
||||||
|
self.logger.info(f"Worker '{worker_name}' detenido")
|
||||||
|
|
||||||
|
# Iniciar workers nuevos o que se hayan habilitado
|
||||||
|
for item, enabled in items:
|
||||||
|
worker_name = item.get_name()
|
||||||
|
if enabled and worker_name not in current_worker_names:
|
||||||
|
# Nuevo worker o recién activado
|
||||||
|
with self._lock:
|
||||||
|
if worker_name not in self._workers:
|
||||||
|
self.start_worker(item, general_args)
|
||||||
|
elif enabled and worker_name in current_worker_names:
|
||||||
|
# Worker existente, verificar si hay cambios significativos
|
||||||
|
needs_restart = False
|
||||||
|
with self._lock:
|
||||||
|
if worker_name in self._workers:
|
||||||
|
old_item = self._workers[worker_name]['item']
|
||||||
|
# Comparar si hay cambios significativos
|
||||||
|
if self._has_changes(old_item, item):
|
||||||
|
self.logger.info(f"Reiniciando worker '{worker_name}' por cambios en configuración")
|
||||||
|
worker = self._workers[worker_name]['worker']
|
||||||
|
worker.stop()
|
||||||
|
future = self._workers[worker_name]['future']
|
||||||
|
if not future.done():
|
||||||
|
future.cancel()
|
||||||
|
del self._workers[worker_name]
|
||||||
|
needs_restart = True
|
||||||
|
else:
|
||||||
|
# Actualizar la referencia al item sin reiniciar
|
||||||
|
self._workers[worker_name]['item'] = item
|
||||||
|
# Actualizar general_args en el worker
|
||||||
|
self._workers[worker_name]['worker'].update_general_args(general_args)
|
||||||
|
# Reiniciar fuera del lock para evitar deadlocks
|
||||||
|
if needs_restart:
|
||||||
|
time.sleep(0.5) # Dar tiempo para detener
|
||||||
|
with self._lock:
|
||||||
|
if worker_name not in self._workers: # Verificar que no se haya añadido en otro thread
|
||||||
|
self.start_worker(item, general_args)
|
||||||
|
|
||||||
|
def _has_changes(self, old_item, new_item):
|
||||||
|
"""Verifica si hay cambios significativos entre dos items"""
|
||||||
|
# Comparar campos importantes
|
||||||
|
return (
|
||||||
|
old_item.get_search_query() != new_item.get_search_query() or
|
||||||
|
old_item.get_min_price() != new_item.get_min_price() or
|
||||||
|
old_item.get_max_price() != new_item.get_max_price() or
|
||||||
|
old_item.get_thread_id() != new_item.get_thread_id() or
|
||||||
|
old_item.get_platform() != new_item.get_platform() or
|
||||||
|
old_item.get_check_every() != new_item.get_check_every() or
|
||||||
|
old_item.get_latitude() != new_item.get_latitude() or
|
||||||
|
old_item.get_longitude() != new_item.get_longitude() or
|
||||||
|
old_item.get_max_distance() != new_item.get_max_distance()
|
||||||
|
)
|
||||||
|
|
||||||
|
def monitor_workers_file(self):
|
||||||
|
"""Monitorea el archivo workers.json y sincroniza workers usando polling"""
|
||||||
|
self.logger.info("Iniciando monitor de workers.json...")
|
||||||
|
last_mtime = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._workers_path):
|
||||||
|
last_mtime = os.path.getmtime(self._workers_path)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Error obteniendo mtime inicial: {e}")
|
||||||
|
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
time.sleep(2) # Verificar cada 2 segundos
|
||||||
|
|
||||||
|
if not os.path.exists(self._workers_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_mtime = os.path.getmtime(self._workers_path)
|
||||||
|
if current_mtime != last_mtime:
|
||||||
|
self.logger.info("Detectado cambio en workers.json, sincronizando workers...")
|
||||||
|
time.sleep(0.5) # Esperar un poco para que se termine de escribir el archivo
|
||||||
|
self.sync_workers()
|
||||||
|
last_mtime = current_mtime
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error monitoreando workers.json: {e}")
|
||||||
|
time.sleep(5) # Esperar más tiempo si hay error
|
||||||
|
|
||||||
|
def start_monitoring(self):
|
||||||
|
"""Inicia el monitoreo en un thread separado"""
|
||||||
|
monitor_thread = threading.Thread(target=self.monitor_workers_file, daemon=True)
|
||||||
|
monitor_thread.start()
|
||||||
|
return monitor_thread
|
||||||
|
|
||||||
|
def stop_all(self):
|
||||||
|
"""Detiene todos los workers"""
|
||||||
|
self._running = False
|
||||||
|
worker_names = list(self._workers.keys())
|
||||||
|
for worker_name in worker_names:
|
||||||
|
try:
|
||||||
|
with self._lock:
|
||||||
|
if worker_name in self._workers:
|
||||||
|
worker = self._workers[worker_name]['worker']
|
||||||
|
worker.stop()
|
||||||
|
future = self._workers[worker_name]['future']
|
||||||
|
if not future.done():
|
||||||
|
future.cancel()
|
||||||
|
del self._workers[worker_name]
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error deteniendo worker '{worker_name}': {e}")
|
||||||
|
|
||||||
|
self._executor.shutdown(wait=False)
|
||||||
|
self.logger.info("Todos los workers detenidos")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
initialize_config_files()
|
initialize_config_files()
|
||||||
configure_logger()
|
configure_logger()
|
||||||
items, general_args = parse_items_to_monitor()
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Cargar configuración de cache y crear ArticleCache
|
# Cargar configuración de cache y crear ArticleCache
|
||||||
cache_config = load_cache_config()
|
cache_config = load_cache_config()
|
||||||
cache_type = cache_config['cache_type']
|
cache_type = cache_config['cache_type']
|
||||||
# Crear kwargs sin cache_type
|
|
||||||
cache_kwargs = {k: v for k, v in cache_config.items() if k != 'cache_type'}
|
cache_kwargs = {k: v for k, v in cache_config.items() if k != 'cache_type'}
|
||||||
article_cache = create_article_cache(cache_type, **cache_kwargs)
|
article_cache = create_article_cache(cache_type, **cache_kwargs)
|
||||||
|
|
||||||
# Crear QueueManager con ArticleCache
|
# Crear QueueManager con ArticleCache
|
||||||
queue_manager = QueueManager(article_cache)
|
queue_manager = QueueManager(article_cache)
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=1000) as executor:
|
# Crear WorkerManager
|
||||||
for item in items:
|
items, general_args = parse_items_to_monitor()
|
||||||
worker = Worker(item, general_args, queue_manager)
|
worker_manager = WorkerManager(general_args, queue_manager)
|
||||||
executor.submit(worker.run)
|
|
||||||
|
# Sincronizar workers iniciales
|
||||||
|
worker_manager.sync_workers()
|
||||||
|
|
||||||
|
# Iniciar monitoreo del archivo workers.json
|
||||||
|
worker_manager.start_monitoring()
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Sistema de monitoreo iniciado. Esperando cambios en workers.json...")
|
||||||
|
# Mantener el programa corriendo
|
||||||
|
while True:
|
||||||
|
time.sleep(60)
|
||||||
|
# Sincronización periódica por si acaso
|
||||||
|
worker_manager.sync_workers()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Deteniendo sistema...")
|
||||||
|
finally:
|
||||||
|
worker_manager.stop_all()
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h1 class="text-3xl font-bold text-gray-900">Gestión de Workers</h1>
|
<h1 class="text-3xl font-bold text-gray-900">Gestión de Workers</h1>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button @click="showGeneralModal = true" class="btn btn-secondary">
|
||||||
|
⚙️ Configuración General
|
||||||
|
</button>
|
||||||
<button @click="showAddModal = true" class="btn btn-primary">
|
<button @click="showAddModal = true" class="btn btn-primary">
|
||||||
+ Añadir Worker
|
+ Añadir Worker
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="text-center py-12">
|
<div v-if="loading" class="text-center py-12">
|
||||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||||
@@ -15,54 +20,103 @@
|
|||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<!-- Workers activos -->
|
<!-- Workers activos -->
|
||||||
<div v-if="activeWorkers.length > 0">
|
<div v-if="activeWorkers.length > 0">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Workers Activos</h2>
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Workers Activos ({{ activeWorkers.length }})</h2>
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div
|
<div
|
||||||
v-for="(worker, index) in activeWorkers"
|
v-for="(worker, index) in activeWorkers"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="card"
|
class="card hover:shadow-lg transition-shadow"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center space-x-2 mb-2">
|
<div class="flex items-center space-x-2 mb-3">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">{{ worker.name }}</h3>
|
<h3 class="text-lg font-semibold text-gray-900">{{ worker.name }}</h3>
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded bg-green-100 text-green-800">
|
<span class="px-2 py-1 text-xs font-semibold rounded bg-green-100 text-green-800">
|
||||||
Activo
|
Activo
|
||||||
</span>
|
</span>
|
||||||
|
<span class="px-2 py-1 text-xs font-semibold rounded" :class="worker.platform === 'wallapop' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'">
|
||||||
|
{{ (worker.platform || 'wallapop').toUpperCase() }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4 text-sm">
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm mb-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-gray-600">Plataforma:</span>
|
<span class="text-gray-600 block mb-1">Búsqueda:</span>
|
||||||
<p class="font-medium">{{ worker.platform || 'wallapop' }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-600">Búsqueda:</span>
|
|
||||||
<p class="font-medium">{{ worker.search_query }}</p>
|
<p class="font-medium">{{ worker.search_query }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="worker.min_price || worker.max_price">
|
||||||
<span class="text-gray-600">Precio:</span>
|
<span class="text-gray-600 block mb-1">Precio:</span>
|
||||||
<p class="font-medium">
|
<p class="font-medium">
|
||||||
{{ worker.min_price || 'N/A' }} - {{ worker.max_price || 'N/A' }}
|
{{ worker.min_price || '0' }}€ - {{ worker.max_price || '∞' }}€
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="worker.thread_id">
|
||||||
<span class="text-gray-600">Thread ID:</span>
|
<span class="text-gray-600 block mb-1">Thread ID:</span>
|
||||||
<p class="font-medium">{{ worker.thread_id || 'General' }}</p>
|
<p class="font-medium">{{ worker.thread_id }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="worker.latitude && worker.longitude">
|
||||||
|
<span class="text-gray-600 block mb-1">Ubicación:</span>
|
||||||
|
<p class="font-medium">{{ worker.latitude }}, {{ worker.longitude }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="worker.max_distance">
|
||||||
|
<span class="text-gray-600 block mb-1">Distancia Máx:</span>
|
||||||
|
<p class="font-medium">{{ worker.max_distance }} km</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="worker.check_every">
|
||||||
|
<span class="text-gray-600 block mb-1">Check cada:</span>
|
||||||
|
<p class="font-medium">{{ worker.check_every }}s</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros aplicados -->
|
||||||
|
<div v-if="hasFilters(worker)" class="mt-3 pt-3 border-t border-gray-200">
|
||||||
|
<details class="text-xs">
|
||||||
|
<summary class="cursor-pointer text-gray-600 hover:text-gray-900 font-medium">
|
||||||
|
Ver filtros ({{ countFilters(worker) }})
|
||||||
|
</summary>
|
||||||
|
<div class="mt-2 space-y-1 text-gray-600">
|
||||||
|
<div v-if="worker.title_exclude?.length" class="flex items-start">
|
||||||
|
<span class="font-medium mr-2">Excluir título:</span>
|
||||||
|
<span>{{ worker.title_exclude.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="worker.description_exclude?.length" class="flex items-start">
|
||||||
|
<span class="font-medium mr-2">Excluir descripción:</span>
|
||||||
|
<span>{{ worker.description_exclude.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="worker.title_must_include?.length" class="flex items-start">
|
||||||
|
<span class="font-medium mr-2">Requerir título:</span>
|
||||||
|
<span>{{ worker.title_must_include.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="worker.description_must_include?.length" class="flex items-start">
|
||||||
|
<span class="font-medium mr-2">Requerir descripción:</span>
|
||||||
|
<span>{{ worker.description_must_include.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="worker.title_first_word_exclude?.length" class="flex items-start">
|
||||||
|
<span class="font-medium mr-2">Excluir primera palabra:</span>
|
||||||
|
<span>{{ worker.title_first_word_exclude.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2 ml-4">
|
<div class="flex space-x-2 ml-4">
|
||||||
<button
|
<button
|
||||||
@click="editWorker(worker, index)"
|
@click="editWorker(worker, activeWorkersIndex(index))"
|
||||||
class="btn btn-secondary text-sm"
|
class="btn btn-secondary text-sm"
|
||||||
>
|
>
|
||||||
Editar
|
✏️ Editar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteWorker(worker.name)"
|
||||||
|
class="btn btn-danger text-sm"
|
||||||
|
>
|
||||||
|
🗑️ Eliminar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="disableWorker(worker.name)"
|
@click="disableWorker(worker.name)"
|
||||||
class="btn btn-danger text-sm"
|
class="btn btn-secondary text-sm"
|
||||||
>
|
>
|
||||||
Desactivar
|
⏸️ Desactivar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,12 +126,12 @@
|
|||||||
|
|
||||||
<!-- Workers desactivados -->
|
<!-- Workers desactivados -->
|
||||||
<div v-if="disabledWorkers.length > 0" class="mt-8">
|
<div v-if="disabledWorkers.length > 0" class="mt-8">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Workers Desactivados</h2>
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Workers Desactivados ({{ disabledWorkers.length }})</h2>
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
<div
|
<div
|
||||||
v-for="(worker, index) in disabledWorkers"
|
v-for="(worker, index) in disabledWorkers"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="card opacity-60"
|
class="card opacity-60 hover:opacity-80 transition-opacity"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -86,14 +140,32 @@
|
|||||||
<span class="px-2 py-1 text-xs font-semibold rounded bg-red-100 text-red-800">
|
<span class="px-2 py-1 text-xs font-semibold rounded bg-red-100 text-red-800">
|
||||||
Desactivado
|
Desactivado
|
||||||
</span>
|
</span>
|
||||||
|
<span class="px-2 py-1 text-xs font-semibold rounded" :class="worker.platform === 'wallapop' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'">
|
||||||
|
{{ (worker.platform || 'wallapop').toUpperCase() }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-gray-600">{{ worker.search_query }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex space-x-2 ml-4">
|
||||||
|
<button
|
||||||
|
@click="editWorker(worker, disabledWorkersIndex(index))"
|
||||||
|
class="btn btn-secondary text-sm"
|
||||||
|
>
|
||||||
|
✏️ Editar
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="enableWorker(worker.name)"
|
@click="enableWorker(worker.name)"
|
||||||
class="btn btn-primary text-sm ml-4"
|
class="btn btn-primary text-sm"
|
||||||
>
|
>
|
||||||
Activar
|
▶️ Activar
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteWorker(worker.name)"
|
||||||
|
class="btn btn-danger text-sm"
|
||||||
|
>
|
||||||
|
🗑️ Eliminar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,6 +173,9 @@
|
|||||||
|
|
||||||
<div v-if="activeWorkers.length === 0 && disabledWorkers.length === 0" class="card text-center py-12">
|
<div v-if="activeWorkers.length === 0 && disabledWorkers.length === 0" class="card text-center py-12">
|
||||||
<p class="text-gray-600">No hay workers configurados</p>
|
<p class="text-gray-600">No hay workers configurados</p>
|
||||||
|
<button @click="showAddModal = true" class="btn btn-primary mt-4">
|
||||||
|
+ Crear primer worker
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,13 +185,17 @@
|
|||||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
@click.self="closeModal"
|
@click.self="closeModal"
|
||||||
>
|
>
|
||||||
<div class="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
<div class="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">
|
<h2 class="text-2xl font-bold text-gray-900 mb-4">
|
||||||
{{ editingWorker ? 'Editar Worker' : 'Añadir Worker' }}
|
{{ editingWorker ? 'Editar Worker' : 'Añadir Worker' }}
|
||||||
</h2>
|
</h2>
|
||||||
<form @submit.prevent="saveWorker" class="space-y-4">
|
<form @submit.prevent="saveWorker" class="space-y-6">
|
||||||
|
<!-- Información básica -->
|
||||||
|
<div class="border-b border-gray-200 pb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Información Básica</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre *</label>
|
||||||
<input v-model="workerForm.name" type="text" class="input" required />
|
<input v-model="workerForm.name" type="text" class="input" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -126,24 +205,124 @@
|
|||||||
<option value="vinted">Vinted</option>
|
<option value="vinted">Vinted</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="md:col-span-2">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Búsqueda</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Búsqueda *</label>
|
||||||
<input v-model="workerForm.search_query" type="text" class="input" required />
|
<input v-model="workerForm.search_query" type="text" class="input" required placeholder="ej: playstation 1" />
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Precio Mínimo</label>
|
|
||||||
<input v-model.number="workerForm.min_price" type="number" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Precio Máximo</label>
|
|
||||||
<input v-model.number="workerForm.max_price" type="number" class="input" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Precios y Thread -->
|
||||||
|
<div class="border-b border-gray-200 pb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Precios y Notificaciones</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Thread ID (opcional)</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Precio Mínimo (€)</label>
|
||||||
|
<input v-model.number="workerForm.min_price" type="number" class="input" min="0" step="0.01" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Precio Máximo (€)</label>
|
||||||
|
<input v-model.number="workerForm.max_price" type="number" class="input" min="0" step="0.01" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Thread ID (Telegram)</label>
|
||||||
<input v-model.number="workerForm.thread_id" type="number" class="input" />
|
<input v-model.number="workerForm.thread_id" type="number" class="input" />
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Opcional: ID del hilo donde enviar notificaciones</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ubicación -->
|
||||||
|
<div class="border-b border-gray-200 pb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Búsqueda Local (Opcional)</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Latitud</label>
|
||||||
|
<input v-model.number="workerForm.latitude" type="number" class="input" step="any" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Longitud</label>
|
||||||
|
<input v-model.number="workerForm.longitude" type="number" class="input" step="any" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Distancia Máxima (km)</label>
|
||||||
|
<input v-model.number="workerForm.max_distance" type="number" class="input" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros de exclusión -->
|
||||||
|
<div class="border-b border-gray-200 pb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Filtros de Exclusión</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Excluir palabras del título</label>
|
||||||
|
<textarea
|
||||||
|
v-model="workerForm.title_exclude_text"
|
||||||
|
class="input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Si aparece alguna palabra, se excluye el artículo</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Excluir palabras de la descripción</label>
|
||||||
|
<textarea
|
||||||
|
v-model="workerForm.description_exclude_text"
|
||||||
|
class="input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Excluir si primera palabra del título es</label>
|
||||||
|
<textarea
|
||||||
|
v-model="workerForm.title_first_word_exclude_text"
|
||||||
|
class="input"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Ej: "Reacondicionado", "Vendido", etc.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros de inclusión -->
|
||||||
|
<div class="border-b border-gray-200 pb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Filtros de Inclusión (Requeridos)</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Palabras requeridas en el título</label>
|
||||||
|
<textarea
|
||||||
|
v-model="workerForm.title_must_include_text"
|
||||||
|
class="input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">TODAS las palabras deben aparecer</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Palabras requeridas en la descripción</label>
|
||||||
|
<textarea
|
||||||
|
v-model="workerForm.description_must_include_text"
|
||||||
|
class="input"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuración avanzada -->
|
||||||
|
<div class="border-b border-gray-200 pb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Configuración Avanzada</h3>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Intervalo de verificación (segundos)</label>
|
||||||
|
<input v-model.number="workerForm.check_every" type="number" class="input" min="1" />
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Cada cuántos segundos se actualiza la búsqueda (por defecto 30s)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-2 pt-4">
|
<div class="flex justify-end space-x-2 pt-4">
|
||||||
<button type="button" @click="closeModal" class="btn btn-secondary">
|
<button type="button" @click="closeModal" class="btn btn-secondary">
|
||||||
Cancelar
|
Cancelar
|
||||||
@@ -155,6 +334,46 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal para configuración general -->
|
||||||
|
<div
|
||||||
|
v-if="showGeneralModal"
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
|
@click.self="closeGeneralModal"
|
||||||
|
>
|
||||||
|
<div class="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-4">Configuración General</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Estas configuraciones se aplican a todos los workers</p>
|
||||||
|
<form @submit.prevent="saveGeneralConfig" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Excluir palabras del título (global)</label>
|
||||||
|
<textarea
|
||||||
|
v-model="generalForm.title_exclude_text"
|
||||||
|
class="input"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Excluir palabras de la descripción (global)</label>
|
||||||
|
<textarea
|
||||||
|
v-model="generalForm.description_exclude_text"
|
||||||
|
class="input"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Una palabra por línea o separadas por comas"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
|
||||||
|
<button type="button" @click="closeGeneralModal" class="btn btn-secondary">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -165,6 +384,7 @@ import api from '../services/api';
|
|||||||
const workers = ref({ items: [], disabled: [], general: {} });
|
const workers = ref({ items: [], disabled: [], general: {} });
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const showAddModal = ref(false);
|
const showAddModal = ref(false);
|
||||||
|
const showGeneralModal = ref(false);
|
||||||
const editingWorker = ref(null);
|
const editingWorker = ref(null);
|
||||||
|
|
||||||
const activeWorkers = computed(() => {
|
const activeWorkers = computed(() => {
|
||||||
@@ -179,6 +399,42 @@ const disabledWorkers = computed(() => {
|
|||||||
) || [];
|
) || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function activeWorkersIndex(index) {
|
||||||
|
return workers.value.items?.findIndex(w => w.name === activeWorkers.value[index]?.name) ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disabledWorkersIndex(index) {
|
||||||
|
return workers.value.items?.findIndex(w => w.name === disabledWorkers.value[index]?.name) ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFilters(worker) {
|
||||||
|
return !!(worker.title_exclude?.length || worker.description_exclude?.length ||
|
||||||
|
worker.title_must_include?.length || worker.description_must_include?.length ||
|
||||||
|
worker.title_first_word_exclude?.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countFilters(worker) {
|
||||||
|
let count = 0;
|
||||||
|
if (worker.title_exclude?.length) count++;
|
||||||
|
if (worker.description_exclude?.length) count++;
|
||||||
|
if (worker.title_must_include?.length) count++;
|
||||||
|
if (worker.description_must_include?.length) count++;
|
||||||
|
if (worker.title_first_word_exclude?.length) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textToArray(text) {
|
||||||
|
if (!text || !text.trim()) return [];
|
||||||
|
return text.split(/\n|,/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayToText(arr) {
|
||||||
|
if (!arr || !Array.isArray(arr) || arr.length === 0) return '';
|
||||||
|
return arr.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
const workerForm = ref({
|
const workerForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
platform: 'wallapop',
|
platform: 'wallapop',
|
||||||
@@ -186,12 +442,31 @@ const workerForm = ref({
|
|||||||
min_price: null,
|
min_price: null,
|
||||||
max_price: null,
|
max_price: null,
|
||||||
thread_id: null,
|
thread_id: null,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
max_distance: null,
|
||||||
|
title_exclude_text: '',
|
||||||
|
description_exclude_text: '',
|
||||||
|
title_must_include_text: '',
|
||||||
|
description_must_include_text: '',
|
||||||
|
title_first_word_exclude_text: '',
|
||||||
|
check_every: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const generalForm = ref({
|
||||||
|
title_exclude_text: '',
|
||||||
|
description_exclude_text: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadWorkers() {
|
async function loadWorkers() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
workers.value = await api.getWorkers();
|
workers.value = await api.getWorkers();
|
||||||
|
// Actualizar formulario general
|
||||||
|
generalForm.value = {
|
||||||
|
title_exclude_text: arrayToText(workers.value.general?.title_exclude || []),
|
||||||
|
description_exclude_text: arrayToText(workers.value.general?.description_exclude || []),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cargando workers:', error);
|
console.error('Error cargando workers:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -201,7 +476,23 @@ async function loadWorkers() {
|
|||||||
|
|
||||||
function editWorker(worker, index) {
|
function editWorker(worker, index) {
|
||||||
editingWorker.value = { worker, index };
|
editingWorker.value = { worker, index };
|
||||||
workerForm.value = { ...worker };
|
workerForm.value = {
|
||||||
|
name: worker.name || '',
|
||||||
|
platform: worker.platform || 'wallapop',
|
||||||
|
search_query: worker.search_query || '',
|
||||||
|
min_price: worker.min_price || null,
|
||||||
|
max_price: worker.max_price || null,
|
||||||
|
thread_id: worker.thread_id || null,
|
||||||
|
latitude: worker.latitude || null,
|
||||||
|
longitude: worker.longitude || null,
|
||||||
|
max_distance: worker.max_distance || null,
|
||||||
|
title_exclude_text: arrayToText(worker.title_exclude || []),
|
||||||
|
description_exclude_text: arrayToText(worker.description_exclude || []),
|
||||||
|
title_must_include_text: arrayToText(worker.title_must_include || []),
|
||||||
|
description_must_include_text: arrayToText(worker.description_must_include || []),
|
||||||
|
title_first_word_exclude_text: arrayToText(worker.title_first_word_exclude || []),
|
||||||
|
check_every: worker.check_every || null,
|
||||||
|
};
|
||||||
showAddModal.value = true;
|
showAddModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,23 +506,54 @@ function closeModal() {
|
|||||||
min_price: null,
|
min_price: null,
|
||||||
max_price: null,
|
max_price: null,
|
||||||
thread_id: null,
|
thread_id: null,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
max_distance: null,
|
||||||
|
title_exclude_text: '',
|
||||||
|
description_exclude_text: '',
|
||||||
|
title_must_include_text: '',
|
||||||
|
description_must_include_text: '',
|
||||||
|
title_first_word_exclude_text: '',
|
||||||
|
check_every: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeGeneralModal() {
|
||||||
|
showGeneralModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function saveWorker() {
|
async function saveWorker() {
|
||||||
try {
|
try {
|
||||||
const updatedWorkers = { ...workers.value };
|
const updatedWorkers = { ...workers.value };
|
||||||
|
if (!updatedWorkers.items) {
|
||||||
|
updatedWorkers.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerData = {
|
||||||
|
name: workerForm.value.name,
|
||||||
|
platform: workerForm.value.platform,
|
||||||
|
search_query: workerForm.value.search_query,
|
||||||
|
...(workerForm.value.min_price !== null && { min_price: workerForm.value.min_price }),
|
||||||
|
...(workerForm.value.max_price !== null && { max_price: workerForm.value.max_price }),
|
||||||
|
...(workerForm.value.thread_id !== null && { thread_id: workerForm.value.thread_id }),
|
||||||
|
...(workerForm.value.latitude !== null && { latitude: workerForm.value.latitude }),
|
||||||
|
...(workerForm.value.longitude !== null && { longitude: workerForm.value.longitude }),
|
||||||
|
...(workerForm.value.max_distance !== null && { max_distance: String(workerForm.value.max_distance) }),
|
||||||
|
...(workerForm.value.check_every !== null && { check_every: workerForm.value.check_every }),
|
||||||
|
...(textToArray(workerForm.value.title_exclude_text).length > 0 && { title_exclude: textToArray(workerForm.value.title_exclude_text) }),
|
||||||
|
...(textToArray(workerForm.value.description_exclude_text).length > 0 && { description_exclude: textToArray(workerForm.value.description_exclude_text) }),
|
||||||
|
...(textToArray(workerForm.value.title_must_include_text).length > 0 && { title_must_include: textToArray(workerForm.value.title_must_include_text) }),
|
||||||
|
...(textToArray(workerForm.value.description_must_include_text).length > 0 && { description_must_include: textToArray(workerForm.value.description_must_include_text) }),
|
||||||
|
...(textToArray(workerForm.value.title_first_word_exclude_text).length > 0 && { title_first_word_exclude: textToArray(workerForm.value.title_first_word_exclude_text) }),
|
||||||
|
};
|
||||||
|
|
||||||
if (editingWorker.value) {
|
if (editingWorker.value) {
|
||||||
// Editar worker existente
|
// Editar worker existente
|
||||||
const index = editingWorker.value.index;
|
const index = editingWorker.value.index;
|
||||||
updatedWorkers.items[index] = { ...workerForm.value };
|
updatedWorkers.items[index] = workerData;
|
||||||
} else {
|
} else {
|
||||||
// Añadir nuevo worker
|
// Añadir nuevo worker
|
||||||
if (!updatedWorkers.items) {
|
updatedWorkers.items.push(workerData);
|
||||||
updatedWorkers.items = [];
|
|
||||||
}
|
|
||||||
updatedWorkers.items.push({ ...workerForm.value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.updateWorkers(updatedWorkers);
|
await api.updateWorkers(updatedWorkers);
|
||||||
@@ -243,6 +565,23 @@ async function saveWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveGeneralConfig() {
|
||||||
|
try {
|
||||||
|
const updatedWorkers = { ...workers.value };
|
||||||
|
updatedWorkers.general = {
|
||||||
|
...(textToArray(generalForm.value.title_exclude_text).length > 0 && { title_exclude: textToArray(generalForm.value.title_exclude_text) }),
|
||||||
|
...(textToArray(generalForm.value.description_exclude_text).length > 0 && { description_exclude: textToArray(generalForm.value.description_exclude_text) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.updateWorkers(updatedWorkers);
|
||||||
|
await loadWorkers();
|
||||||
|
closeGeneralModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error guardando configuración general:', error);
|
||||||
|
alert('Error al guardar la configuración general');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function disableWorker(name) {
|
async function disableWorker(name) {
|
||||||
if (!confirm(`¿Desactivar el worker "${name}"?`)) {
|
if (!confirm(`¿Desactivar el worker "${name}"?`)) {
|
||||||
return;
|
return;
|
||||||
@@ -256,7 +595,7 @@ async function disableWorker(name) {
|
|||||||
if (!updatedWorkers.disabled.includes(name)) {
|
if (!updatedWorkers.disabled.includes(name)) {
|
||||||
updatedWorkers.disabled.push(name);
|
updatedWorkers.disabled.push(name);
|
||||||
}
|
}
|
||||||
await api.updateWorkers(updatedWorkers);
|
await api.updateWshowGeneralModalorkers(updatedWorkers);
|
||||||
await loadWorkers();
|
await loadWorkers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error desactivando worker:', error);
|
console.error('Error desactivando worker:', error);
|
||||||
@@ -278,10 +617,34 @@ async function enableWorker(name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteWorker(name) {
|
||||||
|
if (!confirm(`¿Eliminar permanentemente el worker "${name}"? Esta acción no se puede deshacer.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedWorkers = { ...workers.value };
|
||||||
|
updatedWorkers.items = updatedWorkers.items.filter(w => w.name !== name);
|
||||||
|
if (updatedWorkers.disabled) {
|
||||||
|
updatedWorkers.disabled = updatedWorkers.disabled.filter(n => n !== name);
|
||||||
|
}
|
||||||
|
await api.updateWorkers(updatedWorkers);
|
||||||
|
await loadWorkers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error eliminando worker:', error);
|
||||||
|
alert('Error al eliminar el worker');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleWSMessage(event) {
|
function handleWSMessage(event) {
|
||||||
const data = event.detail;
|
const data = event.detail;
|
||||||
if (data.type === 'workers_updated') {
|
if (data.type === 'workers_updated') {
|
||||||
workers.value = data.data;
|
workers.value = data.data;
|
||||||
|
// Actualizar formulario general
|
||||||
|
generalForm.value = {
|
||||||
|
title_exclude_text: arrayToText(workers.value.general?.title_exclude || []),
|
||||||
|
description_exclude_text: arrayToText(workers.value.general?.description_exclude || []),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,4 +657,3 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('ws-message', handleWSMessage);
|
window.removeEventListener('ws-message', handleWSMessage);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user