Enhance caching mechanism and logging configuration

- Updated .gitignore to include additional IDE and OS files, as well as log and web build directories.
- Expanded config.sample.yaml to include cache configuration options for memory and Redis.
- Modified wallamonitor.py to load cache configuration and initialize ArticleCache.
- Refactored QueueManager to utilize ArticleCache for tracking notified articles.
- Improved logging setup to dynamically determine log file path based on environment.
This commit is contained in:
Omar Sánchez Pizarro
2026-01-19 19:42:12 +01:00
parent b32b0b2e09
commit 9939c4d9ed
41 changed files with 6742 additions and 28 deletions

View File

@@ -0,0 +1,297 @@
<template>
<div>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900">Gestión de Workers</h1>
<button @click="showAddModal = true" class="btn btn-primary">
+ Añadir Worker
</button>
</div>
<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>
<p class="mt-2 text-gray-600">Cargando workers...</p>
</div>
<div v-else class="space-y-4">
<!-- Workers activos -->
<div v-if="activeWorkers.length > 0">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Workers Activos</h2>
<div class="grid grid-cols-1 gap-4">
<div
v-for="(worker, index) in activeWorkers"
:key="index"
class="card"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center space-x-2 mb-2">
<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">
Activo
</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4 text-sm">
<div>
<span class="text-gray-600">Plataforma:</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>
</div>
<div>
<span class="text-gray-600">Precio:</span>
<p class="font-medium">
{{ worker.min_price || 'N/A' }} - {{ worker.max_price || 'N/A' }}
</p>
</div>
<div>
<span class="text-gray-600">Thread ID:</span>
<p class="font-medium">{{ worker.thread_id || 'General' }}</p>
</div>
</div>
</div>
<div class="flex space-x-2 ml-4">
<button
@click="editWorker(worker, index)"
class="btn btn-secondary text-sm"
>
Editar
</button>
<button
@click="disableWorker(worker.name)"
class="btn btn-danger text-sm"
>
Desactivar
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Workers desactivados -->
<div v-if="disabledWorkers.length > 0" class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Workers Desactivados</h2>
<div class="grid grid-cols-1 gap-4">
<div
v-for="(worker, index) in disabledWorkers"
:key="index"
class="card opacity-60"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center space-x-2 mb-2">
<h3 class="text-lg font-semibold text-gray-900">{{ worker.name }}</h3>
<span class="px-2 py-1 text-xs font-semibold rounded bg-red-100 text-red-800">
Desactivado
</span>
</div>
</div>
<button
@click="enableWorker(worker.name)"
class="btn btn-primary text-sm ml-4"
>
Activar
</button>
</div>
</div>
</div>
</div>
<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>
</div>
</div>
<!-- Modal para añadir/editar worker -->
<div
v-if="showAddModal || editingWorker"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
@click.self="closeModal"
>
<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">
{{ editingWorker ? 'Editar Worker' : 'Añadir Worker' }}
</h2>
<form @submit.prevent="saveWorker" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre</label>
<input v-model="workerForm.name" type="text" class="input" required />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Plataforma</label>
<select v-model="workerForm.platform" class="input">
<option value="wallapop">Wallapop</option>
<option value="vinted">Vinted</option>
</select>
</div>
<div>
<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 />
</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>
<label class="block text-sm font-medium text-gray-700 mb-1">Thread ID (opcional)</label>
<input v-model.number="workerForm.thread_id" type="number" class="input" />
</div>
<div class="flex justify-end space-x-2 pt-4">
<button type="button" @click="closeModal" class="btn btn-secondary">
Cancelar
</button>
<button type="submit" class="btn btn-primary">
Guardar
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import api from '../services/api';
const workers = ref({ items: [], disabled: [], general: {} });
const loading = ref(true);
const showAddModal = ref(false);
const editingWorker = ref(null);
const activeWorkers = computed(() => {
return workers.value.items?.filter(
w => !workers.value.disabled?.includes(w.name)
) || [];
});
const disabledWorkers = computed(() => {
return workers.value.items?.filter(
w => workers.value.disabled?.includes(w.name)
) || [];
});
const workerForm = ref({
name: '',
platform: 'wallapop',
search_query: '',
min_price: null,
max_price: null,
thread_id: null,
});
async function loadWorkers() {
loading.value = true;
try {
workers.value = await api.getWorkers();
} catch (error) {
console.error('Error cargando workers:', error);
} finally {
loading.value = false;
}
}
function editWorker(worker, index) {
editingWorker.value = { worker, index };
workerForm.value = { ...worker };
showAddModal.value = true;
}
function closeModal() {
showAddModal.value = false;
editingWorker.value = null;
workerForm.value = {
name: '',
platform: 'wallapop',
search_query: '',
min_price: null,
max_price: null,
thread_id: null,
};
}
async function saveWorker() {
try {
const updatedWorkers = { ...workers.value };
if (editingWorker.value) {
// Editar worker existente
const index = editingWorker.value.index;
updatedWorkers.items[index] = { ...workerForm.value };
} else {
// Añadir nuevo worker
if (!updatedWorkers.items) {
updatedWorkers.items = [];
}
updatedWorkers.items.push({ ...workerForm.value });
}
await api.updateWorkers(updatedWorkers);
await loadWorkers();
closeModal();
} catch (error) {
console.error('Error guardando worker:', error);
alert('Error al guardar el worker');
}
}
async function disableWorker(name) {
if (!confirm(`¿Desactivar el worker "${name}"?`)) {
return;
}
try {
const updatedWorkers = { ...workers.value };
if (!updatedWorkers.disabled) {
updatedWorkers.disabled = [];
}
if (!updatedWorkers.disabled.includes(name)) {
updatedWorkers.disabled.push(name);
}
await api.updateWorkers(updatedWorkers);
await loadWorkers();
} catch (error) {
console.error('Error desactivando worker:', error);
alert('Error al desactivar el worker');
}
}
async function enableWorker(name) {
try {
const updatedWorkers = { ...workers.value };
if (updatedWorkers.disabled) {
updatedWorkers.disabled = updatedWorkers.disabled.filter(n => n !== name);
}
await api.updateWorkers(updatedWorkers);
await loadWorkers();
} catch (error) {
console.error('Error activando worker:', error);
alert('Error al activar el worker');
}
}
function handleWSMessage(event) {
const data = event.detail;
if (data.type === 'workers_updated') {
workers.value = data.data;
}
}
onMounted(() => {
loadWorkers();
window.addEventListener('ws-message', handleWSMessage);
});
onUnmounted(() => {
window.removeEventListener('ws-message', handleWSMessage);
});
</script>