Feat Workers Prioridades y Nivel Serilog
This commit is contained in:
		| @@ -1 +0,0 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||||
| Before Width: | Height: | Size: 1.5 KiB | 
| @@ -7,6 +7,7 @@ import { ConfiguracionGeneral } from './ConfiguracionGeneral'; | ||||
| import { BancasManager } from './BancasManager'; | ||||
| import { LogoOverridesManager } from './LogoOverridesManager'; | ||||
| import { CandidatoOverridesManager } from './CandidatoOverridesManager'; | ||||
| import { WorkerManager } from './WorkerManager'; | ||||
|  | ||||
| export const DashboardPage = () => { | ||||
|     const { logout } = useAuth(); | ||||
| @@ -35,6 +36,8 @@ export const DashboardPage = () => { | ||||
|                 </div> | ||||
|                 <ConfiguracionGeneral /> | ||||
|                 <BancasManager /> | ||||
|                 <hr style={{ margin: '2rem 0' }}/> | ||||
|                 <WorkerManager /> | ||||
|             </main> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
							
								
								
									
										140
									
								
								Elecciones-Web/frontend-admin/src/components/WorkerManager.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								Elecciones-Web/frontend-admin/src/components/WorkerManager.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { getConfiguracion, updateConfiguracion, updateLoggingLevel } from '../services/apiService'; | ||||
| import type { ConfiguracionResponse } from '../services/apiService'; | ||||
|  | ||||
| // --- Componente de Switch reutilizable para la UI --- | ||||
| const Switch = ({ label, isChecked, onChange }: { label: string, isChecked: boolean, onChange: (checked: boolean) => void }) => ( | ||||
|     <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}> | ||||
|         <input type="checkbox" checked={isChecked} onChange={e => onChange(e.target.checked)} /> | ||||
|         {label} | ||||
|     </label> | ||||
| ); | ||||
|  | ||||
| export const WorkerManager = () => { | ||||
|     const queryClient = useQueryClient(); | ||||
|  | ||||
|     // Estados locales para manejar los valores de la UI | ||||
|     const [resultadosActivado, setResultadosActivado] = useState(true); | ||||
|     const [bajasActivado, setBajasActivado] = useState(true); | ||||
|     const [prioridad, setPrioridad] = useState('Resultados'); | ||||
|     const [loggingLevel, setLoggingLevel] = useState('Information'); | ||||
|      | ||||
|     // Query para obtener la configuración actual desde la API | ||||
|     const { data: configData, isLoading } = useQuery<ConfiguracionResponse>({ | ||||
|         queryKey: ['configuracion'], | ||||
|         queryFn: getConfiguracion, | ||||
|     }); | ||||
|  | ||||
|     // useEffect para sincronizar el estado local con los datos de la API una vez cargados | ||||
|     useEffect(() => { | ||||
|         if (configData) { | ||||
|             setResultadosActivado(configData.Worker_Resultados_Activado === 'true'); | ||||
|             setBajasActivado(configData.Worker_Bajas_Activado === 'true'); | ||||
|             setPrioridad(configData.Worker_Prioridad || 'Resultados'); | ||||
|             setLoggingLevel(configData.Logging_Level || 'Information'); | ||||
|         } | ||||
|     }, [configData]); | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|         try { | ||||
|             // Creamos dos promesas separadas, una para la config general y otra para el logging | ||||
|             const configPromise = updateConfiguracion({ | ||||
|                 ...configData, | ||||
|                 'Worker_Resultados_Activado': resultadosActivado.toString(), | ||||
|                 'Worker_Bajas_Activado': bajasActivado.toString(), | ||||
|                 'Worker_Prioridad': prioridad, | ||||
|                 'Logging_Level': loggingLevel, | ||||
|             }); | ||||
|              | ||||
|             // La llamada al endpoint de logging-level es la que cambia el nivel EN VIVO. | ||||
|             const loggingPromise = updateLoggingLevel({ level: loggingLevel }); | ||||
|  | ||||
|             // Ejecutamos ambas en paralelo | ||||
|             await Promise.all([configPromise, loggingPromise]); | ||||
|              | ||||
|             queryClient.invalidateQueries({ queryKey: ['configuracion'] }); | ||||
|             alert('Configuración de Workers y Logging guardada.'); | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.error("Error al guardar la configuración:", error); | ||||
|             alert('Error al guardar la configuración.'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const isPrioridadDisabled = !resultadosActivado || !bajasActivado; | ||||
|  | ||||
|     if (isLoading) { | ||||
|         return <div className="admin-module"><h3>Gestión de Workers</h3><p>Cargando configuración...</p></div>; | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className="admin-module"> | ||||
|             <h3>Gestión de Workers</h3> | ||||
|             <p>Controla el comportamiento de los procesos de captura de datos.</p> | ||||
|             <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', borderTop: '1px solid #eee', paddingTop: '1rem' }}> | ||||
|                  | ||||
|                 {/* --- Switches On/Off --- */} | ||||
|                 <div style={{ display: 'flex', alignSelf: 'center', gap: '2rem' }}> | ||||
|                     <Switch  | ||||
|                         label="Activar Worker de Resultados" | ||||
|                         isChecked={resultadosActivado} | ||||
|                         onChange={setResultadosActivado} | ||||
|                     /> | ||||
|                     <Switch  | ||||
|                         label="Activar Worker de Bancas/Telegramas" | ||||
|                         isChecked={bajasActivado} | ||||
|                         onChange={setBajasActivado} | ||||
|                     /> | ||||
|                 </div> | ||||
|  | ||||
|                  {/* --- Contenedor para Selectores --- */} | ||||
|                 <div style={{ display: 'flex', gap: '2rem', alignSelf:'center', alignItems: 'flex-start' }}> | ||||
|                     {/* --- Selector de Prioridad --- */} | ||||
|                     <div> | ||||
|                         <label htmlFor="prioridad-select" style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}> | ||||
|                             Prioridad (si ambos están activos) | ||||
|                         </label> | ||||
|                         <select  | ||||
|                             id="prioridad-select" | ||||
|                             value={prioridad}  | ||||
|                             onChange={e => setPrioridad(e.target.value)} | ||||
|                             disabled={isPrioridadDisabled} | ||||
|                             style={{ padding: '0.5rem', minWidth: '200px' }} | ||||
|                         > | ||||
|                             <option value="Resultados">Resultados (Noche Electoral)</option> | ||||
|                             <option value="Telegramas">Telegramas (Post-Escrutinio)</option> | ||||
|                         </select> | ||||
|                         {isPrioridadDisabled && <small style={{ display: 'block', marginTop: '0.5rem', color: '#666' }}>Activar ambos workers para elegir prioridad.</small>} | ||||
|                     </div> | ||||
|  | ||||
|                     {/* --- NUEVO: Selector de Nivel de Logging --- */} | ||||
|                     <div> | ||||
|                         <label htmlFor="logging-select" style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 500 }}> | ||||
|                             Nivel de Logging (En vivo) | ||||
|                         </label> | ||||
|                         <select | ||||
|                             id="logging-select" | ||||
|                             value={loggingLevel} | ||||
|                             onChange={e => setLoggingLevel(e.target.value)} | ||||
|                             style={{ padding: '0.5rem', minWidth: '200px' }} | ||||
|                         > | ||||
|                             <option value="Verbose">Verbose (Máximo detalle)</option> | ||||
|                             <option value="Debug">Debug</option> | ||||
|                             <option value="Information">Information (Normal)</option> | ||||
|                             <option value="Warning">Warning (Advertencias)</option> | ||||
|                             <option value="Error">Error</option> | ||||
|                             <option value="Fatal">Fatal (Críticos)</option> | ||||
|                         </select> | ||||
|                         <small style={{ display: 'block', marginTop: '0.5rem', color: '#666' }}>Cambia el nivel de log en tiempo real.</small> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 {/* --- Botón de Guardar --- */} | ||||
|                 <div style={{ marginTop: '1rem' }}> | ||||
|                     <button onClick={handleSave}>Guardar Toda la Configuración</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -121,10 +121,10 @@ export const updateLogos = async (data: LogoAgrupacionCategoria[]): Promise<void | ||||
| }; | ||||
|  | ||||
| export const getMunicipiosForAdmin = async (): Promise<MunicipioSimple[]> => { | ||||
|     // Ahora usa adminApiClient, que apunta a /api/admin/ | ||||
|     // La URL final será /api/admin/catalogos/municipios | ||||
|     const response = await adminApiClient.get('/catalogos/municipios'); | ||||
|     return response.data; | ||||
|   // Ahora usa adminApiClient, que apunta a /api/admin/ | ||||
|   // La URL final será /api/admin/catalogos/municipios | ||||
|   const response = await adminApiClient.get('/catalogos/municipios'); | ||||
|   return response.data; | ||||
| }; | ||||
|  | ||||
| // 6. Overrides de Candidatos | ||||
| @@ -135,4 +135,14 @@ export const getCandidatos = async (): Promise<CandidatoOverride[]> => { | ||||
|  | ||||
| export const updateCandidatos = async (data: CandidatoOverride[]): Promise<void> => { | ||||
|   await adminApiClient.put('/candidatos', data); | ||||
| }; | ||||
|  | ||||
| // 7. Gestión de Logging | ||||
| export interface UpdateLoggingLevelData { | ||||
|   level: string; | ||||
| } | ||||
|  | ||||
| export const updateLoggingLevel = async (data: UpdateLoggingLevelData): Promise<void> => { | ||||
|   // Este endpoint es específico, no es parte de la configuración general | ||||
|   await adminApiClient.put(`/logging-level`, data); | ||||
| }; | ||||
| @@ -6,6 +6,7 @@ using Elecciones.Core.DTOs.ApiRequests; | ||||
| using Elecciones.Database.Entities; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Elecciones.Core.Enums; | ||||
| using Elecciones.Infrastructure.Services; | ||||
|  | ||||
| namespace Elecciones.Api.Controllers; | ||||
|  | ||||
| @@ -16,11 +17,13 @@ public class AdminController : ControllerBase | ||||
| { | ||||
|   private readonly EleccionesDbContext _dbContext; | ||||
|   private readonly ILogger<AdminController> _logger; | ||||
|   private readonly LoggingSwitchService _loggingSwitchService; | ||||
|  | ||||
|   public AdminController(EleccionesDbContext dbContext, ILogger<AdminController> logger) | ||||
|   public AdminController(EleccionesDbContext dbContext, ILogger<AdminController> logger, LoggingSwitchService loggingSwitchService) | ||||
|   { | ||||
|     _dbContext = dbContext; | ||||
|     _logger = logger; | ||||
|     _loggingSwitchService = loggingSwitchService; | ||||
|   } | ||||
|  | ||||
|   // Endpoint para obtener todas las agrupaciones para el panel de admin | ||||
| @@ -297,4 +300,41 @@ public class AdminController : ControllerBase | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|     return NoContent(); | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Actualiza el nivel mínimo de logging en tiempo real y guarda la configuración en la BD. | ||||
|   /// </summary> | ||||
|   /// <param name="request">Un objeto que contiene el nuevo nivel de logging.</param> | ||||
|   [HttpPut("logging-level")] | ||||
|   public async Task<IActionResult> UpdateLoggingLevel([FromBody] UpdateLoggingLevelRequest request) | ||||
|   { | ||||
|     if (string.IsNullOrWhiteSpace(request.Level)) | ||||
|     { | ||||
|       return BadRequest("El nivel de logging no puede estar vacío."); | ||||
|     } | ||||
|  | ||||
|     // 1. Intentamos actualizar el interruptor de Serilog en memoria. | ||||
|     bool success = _loggingSwitchService.SetLoggingLevel(request.Level); | ||||
|  | ||||
|     if (!success) | ||||
|     { | ||||
|       return BadRequest($"El nivel de logging '{request.Level}' no es válido. Los valores posibles son: Verbose, Debug, Information, Warning, Error, Fatal."); | ||||
|     } | ||||
|  | ||||
|     // 2. Si el cambio fue exitoso, guardamos el nuevo valor en la base de datos. | ||||
|     var config = await _dbContext.Configuraciones.FindAsync("Logging_Level"); | ||||
|     if (config == null) | ||||
|     { | ||||
|       _dbContext.Configuraciones.Add(new Configuracion { Clave = "Logging_Level", Valor = request.Level }); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|       config.Valor = request.Level; | ||||
|     } | ||||
|  | ||||
|     await _dbContext.SaveChangesAsync(); | ||||
|  | ||||
|     _logger.LogWarning("El nivel de logging ha sido cambiado a: {Level}", request.Level); | ||||
|     return Ok(new { message = $"Nivel de logging actualizado a '{request.Level}'." }); | ||||
|   } | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| //Elecciones.Api/Program.cs | ||||
| using Elecciones.Database; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Serilog; | ||||
| @@ -13,13 +14,24 @@ using Microsoft.AspNetCore.HttpOverrides; | ||||
| // Esta es la estructura estándar y recomendada. | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
|  | ||||
| // 1. Configurar Serilog. Esta es la forma correcta de integrarlo. | ||||
| builder.Host.UseSerilog((context, services, configuration) => configuration | ||||
|     .ReadFrom.Configuration(context.Configuration) | ||||
|     .ReadFrom.Services(services) | ||||
|     .Enrich.FromLogContext() | ||||
|     .WriteTo.Console() | ||||
|     .WriteTo.File("logs/api-.log", rollingInterval: RollingInterval.Day)); | ||||
| // 1. Registra el servicio del interruptor como un Singleton. | ||||
| //    Esto asegura que toda la aplicación comparta la MISMA instancia del interruptor. | ||||
| builder.Services.AddSingleton<LoggingSwitchService>(); | ||||
|  | ||||
| builder.Host.UseSerilog((context, services, configuration) => | ||||
| { | ||||
|     // 2. Obtenemos la instancia del interruptor que acabamos de registrar. | ||||
|     var loggingSwitch = services.GetRequiredService<LoggingSwitchService>(); | ||||
|  | ||||
|     configuration | ||||
|         .ReadFrom.Configuration(context.Configuration) | ||||
|         .ReadFrom.Services(services) | ||||
|         .Enrich.FromLogContext() | ||||
|         // 3. Establecemos el nivel mínimo de logging controlado por el interruptor. | ||||
|         .MinimumLevel.ControlledBy(loggingSwitch.LevelSwitch) | ||||
|         .WriteTo.Console() | ||||
|         .WriteTo.File("logs/api-.log", rollingInterval: RollingInterval.Day); // o "logs/worker-.log" | ||||
| }); | ||||
|  | ||||
| // 2. Añadir servicios al contenedor. | ||||
| var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); | ||||
| @@ -83,6 +95,34 @@ builder.Services.Configure<ForwardedHeadersOptions>(options => | ||||
| // 3. Construir la aplicación. | ||||
| var app = builder.Build(); | ||||
|  | ||||
| // --- LÓGICA PARA LEER EL NIVEL DE LOGGING AL INICIO --- | ||||
| // Creamos un scope temporal para leer la configuración de la BD | ||||
| using (var scope = app.Services.CreateScope()) // O 'host.Services.CreateScope()' | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
|     try | ||||
|     { | ||||
|         // El resto de la lógica no cambia | ||||
|         var dbContext = services.GetRequiredService<EleccionesDbContext>(); | ||||
|         var loggingSwitchService = services.GetRequiredService<LoggingSwitchService>(); | ||||
|  | ||||
|         var logLevelConfig = await dbContext.Configuraciones | ||||
|             .AsNoTracking() | ||||
|             .FirstOrDefaultAsync(c => c.Clave == "Logging_Level"); | ||||
|  | ||||
|         if (logLevelConfig != null) | ||||
|         { | ||||
|             loggingSwitchService.SetLoggingLevel(logLevelConfig.Valor); | ||||
|             Console.WriteLine($"--> Nivel de logging inicial establecido desde la BD a: {logLevelConfig.Valor}"); | ||||
|         } | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|         // Si hay un error (ej. la BD no está disponible al arrancar), se usará el nivel por defecto 'Information'. | ||||
|         Console.WriteLine($"--> No se pudo establecer el nivel de logging desde la BD: {ex.Message}"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| app.UseForwardedHeaders(); | ||||
|  | ||||
| // Seeder para el usuario admin | ||||
| @@ -150,19 +190,29 @@ using (var scope = app.Services.CreateScope()) | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
|     var context = services.GetRequiredService<EleccionesDbContext>(); | ||||
|     if (!context.Configuraciones.Any(c => c.Clave == "MostrarOcupantes")) | ||||
|  | ||||
|     // Lista de configuraciones por defecto a asegurar | ||||
|     var defaultConfiguraciones = new Dictionary<string, string> | ||||
|     { | ||||
|         context.Configuraciones.Add(new Configuracion { Clave = "MostrarOcupantes", Valor = "true" }); | ||||
|         context.SaveChanges(); | ||||
|         Console.WriteLine("--> Seeded default configuration 'MostrarOcupantes'."); | ||||
|     } | ||||
|     if (!context.Configuraciones.Any(c => c.Clave == "TickerResultadosCantidad")) | ||||
|         { "MostrarOcupantes", "true" }, | ||||
|         { "TickerResultadosCantidad", "3" }, | ||||
|         { "ConcejalesResultadosCantidad", "5" }, | ||||
|         { "Worker_Resultados_Activado", "false" }, | ||||
|         { "Worker_Bajas_Activado", "false" }, | ||||
|         { "Worker_Prioridad", "Resultados" }, | ||||
|         { "Logging_Level", "Information" } | ||||
|     }; | ||||
|  | ||||
|     foreach (var config in defaultConfiguraciones) | ||||
|     { | ||||
|         context.Configuraciones.Add(new Configuracion { Clave = "TickerResultadosCantidad", Valor = "3" }); | ||||
|         context.Configuraciones.Add(new Configuracion { Clave = "ConcejalesResultadosCantidad", Valor = "5" }); | ||||
|         context.SaveChanges(); | ||||
|         Console.WriteLine("--> Seeded default configuration 'TickerResultadosCantidad'."); | ||||
|         if (!context.Configuraciones.Any(c => c.Clave == config.Key)) | ||||
|         { | ||||
|             context.Configuraciones.Add(new Configuracion { Clave = config.Key, Valor = config.Value }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     context.SaveChanges(); | ||||
|     Console.WriteLine("--> Seeded default configurations."); | ||||
| } | ||||
|  | ||||
| // Configurar el pipeline de peticiones HTTP. | ||||
|   | ||||
| @@ -859,17 +859,17 @@ | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Serilog/4.2.0": { | ||||
|       "Serilog/4.3.0": { | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Serilog.dll": { | ||||
|             "assemblyVersion": "4.2.0.0", | ||||
|             "fileVersion": "4.2.0.0" | ||||
|             "assemblyVersion": "4.3.0.0", | ||||
|             "fileVersion": "4.3.0.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Serilog.AspNetCore/9.0.0": { | ||||
|         "dependencies": { | ||||
|           "Serilog": "4.2.0", | ||||
|           "Serilog": "4.3.0", | ||||
|           "Serilog.Extensions.Hosting": "9.0.0", | ||||
|           "Serilog.Formatting.Compact": "3.0.0", | ||||
|           "Serilog.Settings.Configuration": "9.0.0", | ||||
| @@ -889,7 +889,7 @@ | ||||
|           "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", | ||||
|           "Microsoft.Extensions.Logging.Abstractions": "9.0.8", | ||||
|           "Serilog": "4.2.0", | ||||
|           "Serilog": "4.3.0", | ||||
|           "Serilog.Extensions.Logging": "9.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
| @@ -902,7 +902,7 @@ | ||||
|       "Serilog.Extensions.Logging/9.0.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Extensions.Logging": "9.0.8", | ||||
|           "Serilog": "4.2.0" | ||||
|           "Serilog": "4.3.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Serilog.Extensions.Logging.dll": { | ||||
| @@ -913,7 +913,7 @@ | ||||
|       }, | ||||
|       "Serilog.Formatting.Compact/3.0.0": { | ||||
|         "dependencies": { | ||||
|           "Serilog": "4.2.0" | ||||
|           "Serilog": "4.3.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Serilog.Formatting.Compact.dll": { | ||||
| @@ -926,7 +926,7 @@ | ||||
|         "dependencies": { | ||||
|           "Microsoft.Extensions.Configuration.Binder": "9.0.8", | ||||
|           "Microsoft.Extensions.DependencyModel": "9.0.8", | ||||
|           "Serilog": "4.2.0" | ||||
|           "Serilog": "4.3.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Serilog.Settings.Configuration.dll": { | ||||
| @@ -937,7 +937,7 @@ | ||||
|       }, | ||||
|       "Serilog.Sinks.Console/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "Serilog": "4.2.0" | ||||
|           "Serilog": "4.3.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Serilog.Sinks.Console.dll": { | ||||
| @@ -948,7 +948,7 @@ | ||||
|       }, | ||||
|       "Serilog.Sinks.Debug/3.0.0": { | ||||
|         "dependencies": { | ||||
|           "Serilog": "4.2.0" | ||||
|           "Serilog": "4.3.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Serilog.Sinks.Debug.dll": { | ||||
| @@ -959,7 +959,7 @@ | ||||
|       }, | ||||
|       "Serilog.Sinks.File/7.0.0": { | ||||
|         "dependencies": { | ||||
|           "Serilog": "4.2.0" | ||||
|           "Serilog": "4.3.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Serilog.Sinks.File.dll": { | ||||
| @@ -1294,8 +1294,10 @@ | ||||
|       "Elecciones.Infrastructure/1.0.0": { | ||||
|         "dependencies": { | ||||
|           "Elecciones.Core": "1.0.0", | ||||
|           "Elecciones.Database": "1.0.0", | ||||
|           "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Http": "9.0.8", | ||||
|           "Serilog": "4.3.0", | ||||
|           "System.Threading.RateLimiting": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
| @@ -1691,12 +1693,12 @@ | ||||
|       "path": "mono.texttemplating/3.0.0", | ||||
|       "hashPath": "mono.texttemplating.3.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Serilog/4.2.0": { | ||||
|     "Serilog/4.3.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA==", | ||||
|       "path": "serilog/4.2.0", | ||||
|       "hashPath": "serilog.4.2.0.nupkg.sha512" | ||||
|       "sha512": "sha512-+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==", | ||||
|       "path": "serilog/4.3.0", | ||||
|       "hashPath": "serilog.4.3.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Serilog.AspNetCore/9.0.0": { | ||||
|       "type": "package", | ||||
|   | ||||
| @@ -14,7 +14,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d78a02a0ebc4c70ea01e48821db963110e7ce280")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f384a640f36be1289d652dc85e78ebdcef30968a")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Kt4ImnGs0wklEJp/6NxrhrTvGLQxPfYUAB5LMWAnz10=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","v1SBeIVg8rE3EddYwnvF/EsPYr2F5GAppt/Egvdtr/0="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"b5T/+ta4fUd8qpIzUTm3KyEwAYYUsU5ASo+CSFM3ByE=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","UucupTplk47jbYuQLLfpsVglReDmh1hUE6oD0OEv\u002BsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","nor5YuoHu4p8qlladsJ2COw4pycCja0XN1sckUrKV/w="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -1 +1 @@ | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Kt4ImnGs0wklEJp/6NxrhrTvGLQxPfYUAB5LMWAnz10=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","v1SBeIVg8rE3EddYwnvF/EsPYr2F5GAppt/Egvdtr/0="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| {"GlobalPropertiesHash":"tJTBjV/i0Ihkc6XuOu69wxL8PBac9c9Kak6srMso4pU=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["TyIJk/eQMWjmB5LsDE\u002BZIJC9P9ciVxd7bnzRiTZsGt4=","t631p0kaOa0gMRIcaPzz1ZVPZ1kuq4pq4kYPWQgoPcM=","PA/Beu9jJpOBY5r5Y1CiSyqrARA2j7LHeWYUnEZpQO8=","E2ODTAlJxzsXY1iP1eB/02NIUK\u002BnQveGlWAOHY1cpgA=","6WTvWQ72AaZBYOVSmaxaci9tc1dW5p7IK9Kscjj2cb0=","vAy46VJ9Gp8QqG/Px4J1mj8jL6ws4/A01UKRmMYfYek=","cdgbHR/E4DJsddPc\u002BTpzoUMOVNaFJZm33Pw7AxU9Ees=","4r4JGR4hS5m4rsLfuCSZxzrknTBxKFkLQDXc\u002B2KbqTU=","yVoZ4UnBcSOapsJIi046hnn7ylD3jAcEBUxQ\u002Brkvj/4=","/GfbpJthEWmsuz0uFx1QLHM7gyM1wLLeQgAIl4SzUD4=","i5\u002B5LcfxQD8meRAkQbVf4wMvjxSE4\u002BjCd2/FdPtMpms=","AvSkxVPIg0GjnB1RJ4hDNyo9p9GONrzDs8uVuixH\u002BOE=","IgT9pOgRnK37qfILj2QcjFoBZ180HMt\u002BScgje2iYOo4=","ezUlOMzNZmyKDIe1wwXqvX\u002BvhwfB992xNVts7r2zcTc=","y2BV4WpkQuLfqQhfOQBtmuzh940c3s4LAopGKfztfTE=","lHTUEsMkDu8nqXtfTwl7FRfgocyyc7RI5O/edTHN1\u002B0=","A7nz7qgOtQ1CwZZLvNnr0b5QZB3fTi3y4i6y7rBIcxQ=","znnuRi2tsk7AACuYo4WSgj7NcLriG4PKVaF4L35SvDk=","GelE32odx/vTului267wqi6zL3abBnF9yvwC2Q66LoM=","TEsXImnzxFKTIq2f5fiDu7i6Ar/cbecW5MZ3z8Wb/a4=","5WogJu\u002BUPlF\u002BE5mq/ILtDXpVwqwmhHtsEB13nmT5JJk=","dcHQRkttjMjo2dvhL7hA9t4Pg\u002B7OnjZpkFmakT4QR9U=","Of8nTYw5l\u002BgiAJo7z6XYIntG2tUtCFcILzHbTiiXn\u002Bw=","UucupTplk47jbYuQLLfpsVglReDmh1hUE6oD0OEv\u002BsM=","P8JRhYPpULTLMAydvl3Ky\u002B92/tYDIjui0l66En4aXuQ=","nor5YuoHu4p8qlladsJ2COw4pycCja0XN1sckUrKV/w="],"CachedAssets":{},"CachedCopyCandidates":{}} | ||||
| @@ -287,6 +287,9 @@ | ||||
|             "projectReferences": { | ||||
|               "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Core\\Elecciones.Core.csproj": { | ||||
|                 "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Core\\Elecciones.Core.csproj" | ||||
|               }, | ||||
|               "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj": { | ||||
|                 "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
| @@ -315,6 +318,10 @@ | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|             }, | ||||
|             "Serilog": { | ||||
|               "target": "Package", | ||||
|               "version": "[4.3.0, )" | ||||
|             }, | ||||
|             "System.Threading.RateLimiting": { | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
|  | ||||
| public class UpdateLoggingLevelRequest | ||||
| { | ||||
|     [Required] | ||||
|     public string Level { get; set; } = null!; | ||||
| } | ||||
| @@ -13,7 +13,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d78a02a0ebc4c70ea01e48821db963110e7ce280")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f384a640f36be1289d652dc85e78ebdcef30968a")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Core")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -13,7 +13,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Database")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d78a02a0ebc4c70ea01e48821db963110e7ce280")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f384a640f36be1289d652dc85e78ebdcef30968a")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Database")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Database")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -2,11 +2,13 @@ | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\Elecciones.Core\Elecciones.Core.csproj" /> | ||||
|     <ProjectReference Include="..\Elecciones.Database\Elecciones.Database.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" /> | ||||
|     <PackageReference Include="Serilog" Version="4.3.0" /> | ||||
|     <PackageReference Include="System.Threading.RateLimiting" Version="9.0.8" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,31 @@ | ||||
| using Serilog.Core; | ||||
| using Serilog.Events; | ||||
|  | ||||
| namespace Elecciones.Infrastructure.Services; | ||||
|  | ||||
| public class LoggingSwitchService | ||||
| { | ||||
|     // El interruptor de nivel de logging de Serilog. | ||||
|     // Lo inicializamos en 'Information' por defecto. | ||||
|     public LoggingLevelSwitch LevelSwitch { get; } = new(LogEventLevel.Information); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Cambia el nivel mínimo de logging dinámicamente. | ||||
|     /// </summary> | ||||
|     /// <param name="level">El nuevo nivel de logging como string (ej. "Information", "Warning", "Verbose").</param> | ||||
|     /// <returns>True si el nivel se cambió con éxito, false si el string no es válido.</returns> | ||||
|     public bool SetLoggingLevel(string level) | ||||
|     { | ||||
|         // Usamos Enum.TryParse para convertir el string a un valor del enum LogEventLevel. | ||||
|         // El 'true' ignora mayúsculas/minúsculas. | ||||
|         if (Enum.TryParse<LogEventLevel>(level, true, out var newLevel)) | ||||
|         { | ||||
|             // Si la conversión es exitosa, actualizamos el interruptor. | ||||
|             LevelSwitch.MinimumLevel = newLevel; | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         // Si el string no corresponde a ningún nivel válido, no hacemos nada y devolvemos false. | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,80 @@ | ||||
| using Elecciones.Database; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Elecciones.Infrastructure.Services; | ||||
|  | ||||
| public class WorkerSettings | ||||
| { | ||||
|     public bool ResultadosActivado { get; set; } = true; | ||||
|     public bool BajasActivado { get; set; } = true; | ||||
|     public string Prioridad { get; set; } = "Resultados"; | ||||
| } | ||||
|  | ||||
| public class WorkerConfigService | ||||
| { | ||||
|     private readonly IServiceProvider _serviceProvider; | ||||
|     private readonly ILogger<WorkerConfigService> _logger; | ||||
|     private WorkerSettings _cachedSettings = new(); | ||||
|     private DateTime _lastFetchTime = DateTime.MinValue; | ||||
|     private readonly TimeSpan _cacheDuration = TimeSpan.FromSeconds(25); // Cachear por 25 segundos | ||||
|     private readonly SemaphoreSlim _semaphore = new(1, 1); | ||||
|  | ||||
|     public WorkerConfigService(IServiceProvider serviceProvider, ILogger<WorkerConfigService> logger) | ||||
|     { | ||||
|         _serviceProvider = serviceProvider; | ||||
|         _logger = logger; | ||||
|     } | ||||
|  | ||||
|     public async Task<WorkerSettings> GetSettingsAsync() | ||||
|     { | ||||
|         // Si el caché es válido, lo devolvemos inmediatamente. | ||||
|         if (DateTime.UtcNow < _lastFetchTime + _cacheDuration) | ||||
|         { | ||||
|             return _cachedSettings; | ||||
|         } | ||||
|  | ||||
|         // Si el caché ha expirado, intentamos obtener el control para actualizarlo. | ||||
|         await _semaphore.WaitAsync(); | ||||
|         try | ||||
|         { | ||||
|             // Volvemos a comprobar por si otra tarea ya actualizó el caché mientras esperábamos. | ||||
|             if (DateTime.UtcNow < _lastFetchTime + _cacheDuration) | ||||
|             { | ||||
|                 return _cachedSettings; | ||||
|             } | ||||
|              | ||||
|             _logger.LogInformation("Caché de configuración del worker expirado. Actualizando desde la BD..."); | ||||
|  | ||||
|             using var scope = _serviceProvider.CreateScope(); | ||||
|             var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|              | ||||
|             var configMap = await dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); | ||||
|  | ||||
|             _cachedSettings = new WorkerSettings | ||||
|             { | ||||
|                 ResultadosActivado = configMap.GetValueOrDefault("Worker_Resultados_Activado", "true") == "true", | ||||
|                 BajasActivado = configMap.GetValueOrDefault("Worker_Bajas_Activado", "true") == "true", | ||||
|                 Prioridad = configMap.GetValueOrDefault("Worker_Prioridad", "Resultados") ?? "Resultados" | ||||
|             }; | ||||
|  | ||||
|             _lastFetchTime = DateTime.UtcNow; | ||||
|             _logger.LogInformation("Configuración del worker actualizada: Resultados={res}, Bajas={bajas}, Prioridad={prio}",  | ||||
|                 _cachedSettings.ResultadosActivado, _cachedSettings.BajasActivado, _cachedSettings.Prioridad); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             _logger.LogError(ex, "No se pudo actualizar la configuración del worker desde la BD. Usando la última configuración cacheada."); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             _semaphore.Release(); | ||||
|         } | ||||
|  | ||||
|         return _cachedSettings; | ||||
|     } | ||||
| } | ||||
| @@ -9,14 +9,201 @@ | ||||
|       "Elecciones.Infrastructure/1.0.0": { | ||||
|         "dependencies": { | ||||
|           "Elecciones.Core": "1.0.0", | ||||
|           "Elecciones.Database": "1.0.0", | ||||
|           "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Http": "9.0.8", | ||||
|           "Serilog": "4.3.0", | ||||
|           "System.Threading.RateLimiting": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "Elecciones.Infrastructure.dll": {} | ||||
|         } | ||||
|       }, | ||||
|       "Azure.Core/1.38.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Bcl.AsyncInterfaces": "1.1.1", | ||||
|           "System.ClientModel": "1.0.0", | ||||
|           "System.Diagnostics.DiagnosticSource": "6.0.1", | ||||
|           "System.Memory.Data": "1.0.2", | ||||
|           "System.Numerics.Vectors": "4.5.0", | ||||
|           "System.Text.Encodings.Web": "6.0.0", | ||||
|           "System.Text.Json": "9.0.8", | ||||
|           "System.Threading.Tasks.Extensions": "4.5.4" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Azure.Core.dll": { | ||||
|             "assemblyVersion": "1.38.0.0", | ||||
|             "fileVersion": "1.3800.24.12602" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Azure.Identity/1.11.4": { | ||||
|         "dependencies": { | ||||
|           "Azure.Core": "1.38.0", | ||||
|           "Microsoft.Identity.Client": "4.61.3", | ||||
|           "Microsoft.Identity.Client.Extensions.Msal": "4.61.3", | ||||
|           "System.Memory": "4.5.4", | ||||
|           "System.Security.Cryptography.ProtectedData": "6.0.0", | ||||
|           "System.Text.Json": "9.0.8", | ||||
|           "System.Threading.Tasks.Extensions": "4.5.4" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/netstandard2.0/Azure.Identity.dll": { | ||||
|             "assemblyVersion": "1.11.4.0", | ||||
|             "fileVersion": "1.1100.424.31005" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Bcl.AsyncInterfaces/1.1.1": { | ||||
|         "runtime": { | ||||
|           "lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": { | ||||
|             "assemblyVersion": "1.0.0.0", | ||||
|             "fileVersion": "4.700.20.21406" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.CSharp/4.5.0": {}, | ||||
|       "Microsoft.Data.SqlClient/5.1.6": { | ||||
|         "dependencies": { | ||||
|           "Azure.Identity": "1.11.4", | ||||
|           "Microsoft.Data.SqlClient.SNI.runtime": "5.1.1", | ||||
|           "Microsoft.Identity.Client": "4.61.3", | ||||
|           "Microsoft.IdentityModel.JsonWebTokens": "6.35.0", | ||||
|           "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0", | ||||
|           "Microsoft.SqlServer.Server": "1.0.0", | ||||
|           "System.Configuration.ConfigurationManager": "6.0.1", | ||||
|           "System.Diagnostics.DiagnosticSource": "6.0.1", | ||||
|           "System.Runtime.Caching": "6.0.0", | ||||
|           "System.Security.Cryptography.Cng": "5.0.0", | ||||
|           "System.Security.Principal.Windows": "5.0.0", | ||||
|           "System.Text.Encoding.CodePages": "6.0.0", | ||||
|           "System.Text.Encodings.Web": "6.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.Data.SqlClient.dll": { | ||||
|             "assemblyVersion": "5.0.0.0", | ||||
|             "fileVersion": "5.16.24240.5" | ||||
|           } | ||||
|         }, | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/unix/lib/net6.0/Microsoft.Data.SqlClient.dll": { | ||||
|             "rid": "unix", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "5.0.0.0", | ||||
|             "fileVersion": "5.16.24240.5" | ||||
|           }, | ||||
|           "runtimes/win/lib/net6.0/Microsoft.Data.SqlClient.dll": { | ||||
|             "rid": "win", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "5.0.0.0", | ||||
|             "fileVersion": "5.16.24240.5" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Data.SqlClient.SNI.runtime/5.1.1": { | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/win-arm/native/Microsoft.Data.SqlClient.SNI.dll": { | ||||
|             "rid": "win-arm", | ||||
|             "assetType": "native", | ||||
|             "fileVersion": "5.1.1.0" | ||||
|           }, | ||||
|           "runtimes/win-arm64/native/Microsoft.Data.SqlClient.SNI.dll": { | ||||
|             "rid": "win-arm64", | ||||
|             "assetType": "native", | ||||
|             "fileVersion": "5.1.1.0" | ||||
|           }, | ||||
|           "runtimes/win-x64/native/Microsoft.Data.SqlClient.SNI.dll": { | ||||
|             "rid": "win-x64", | ||||
|             "assetType": "native", | ||||
|             "fileVersion": "5.1.1.0" | ||||
|           }, | ||||
|           "runtimes/win-x86/native/Microsoft.Data.SqlClient.SNI.dll": { | ||||
|             "rid": "win-x86", | ||||
|             "assetType": "native", | ||||
|             "fileVersion": "5.1.1.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.EntityFrameworkCore/9.0.8": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.EntityFrameworkCore.Abstractions": "9.0.8", | ||||
|           "Microsoft.EntityFrameworkCore.Analyzers": "9.0.8", | ||||
|           "Microsoft.Extensions.Caching.Memory": "9.0.8", | ||||
|           "Microsoft.Extensions.Logging": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Microsoft.EntityFrameworkCore.dll": { | ||||
|             "assemblyVersion": "9.0.8.0", | ||||
|             "fileVersion": "9.0.825.36802" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.EntityFrameworkCore.Abstractions/9.0.8": { | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": { | ||||
|             "assemblyVersion": "9.0.8.0", | ||||
|             "fileVersion": "9.0.825.36802" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.EntityFrameworkCore.Analyzers/9.0.8": {}, | ||||
|       "Microsoft.EntityFrameworkCore.Relational/9.0.8": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.EntityFrameworkCore": "9.0.8", | ||||
|           "Microsoft.Extensions.Caching.Memory": "9.0.8", | ||||
|           "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Logging": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": { | ||||
|             "assemblyVersion": "9.0.8.0", | ||||
|             "fileVersion": "9.0.825.36802" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.EntityFrameworkCore.SqlServer/9.0.8": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Data.SqlClient": "5.1.6", | ||||
|           "Microsoft.EntityFrameworkCore.Relational": "9.0.8", | ||||
|           "Microsoft.Extensions.Caching.Memory": "9.0.8", | ||||
|           "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Logging": "9.0.8", | ||||
|           "System.Formats.Asn1": "9.0.8", | ||||
|           "System.Text.Json": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net8.0/Microsoft.EntityFrameworkCore.SqlServer.dll": { | ||||
|             "assemblyVersion": "9.0.8.0", | ||||
|             "fileVersion": "9.0.825.36802" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Extensions.Caching.Abstractions/9.0.8": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Extensions.Primitives": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll": { | ||||
|             "assemblyVersion": "9.0.0.0", | ||||
|             "fileVersion": "9.0.825.36511" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Extensions.Caching.Memory/9.0.8": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Extensions.Caching.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Logging.Abstractions": "9.0.8", | ||||
|           "Microsoft.Extensions.Options": "9.0.8", | ||||
|           "Microsoft.Extensions.Primitives": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Microsoft.Extensions.Caching.Memory.dll": { | ||||
|             "assemblyVersion": "9.0.0.0", | ||||
|             "fileVersion": "9.0.825.36511" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Extensions.Configuration/9.0.8": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", | ||||
| @@ -170,6 +357,308 @@ | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Identity.Client/4.61.3": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.IdentityModel.Abstractions": "6.35.0", | ||||
|           "System.Diagnostics.DiagnosticSource": "6.0.1" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.Identity.Client.dll": { | ||||
|             "assemblyVersion": "4.61.3.0", | ||||
|             "fileVersion": "4.61.3.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Identity.Client.Extensions.Msal/4.61.3": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Identity.Client": "4.61.3", | ||||
|           "System.Security.Cryptography.ProtectedData": "6.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.Identity.Client.Extensions.Msal.dll": { | ||||
|             "assemblyVersion": "4.61.3.0", | ||||
|             "fileVersion": "4.61.3.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.IdentityModel.Abstractions/6.35.0": { | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.IdentityModel.Abstractions.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.IdentityModel.JsonWebTokens/6.35.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.IdentityModel.Tokens": "6.35.0", | ||||
|           "System.Text.Encoding": "4.3.0", | ||||
|           "System.Text.Encodings.Web": "6.0.0", | ||||
|           "System.Text.Json": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.IdentityModel.JsonWebTokens.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.IdentityModel.Logging/6.35.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.IdentityModel.Abstractions": "6.35.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.IdentityModel.Logging.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.IdentityModel.Protocols/6.35.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.IdentityModel.Logging": "6.35.0", | ||||
|           "Microsoft.IdentityModel.Tokens": "6.35.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.IdentityModel.Protocols.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.IdentityModel.Protocols.OpenIdConnect/6.35.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.IdentityModel.Protocols": "6.35.0", | ||||
|           "System.IdentityModel.Tokens.Jwt": "6.35.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.IdentityModel.Tokens/6.35.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.CSharp": "4.5.0", | ||||
|           "Microsoft.IdentityModel.Logging": "6.35.0", | ||||
|           "System.Security.Cryptography.Cng": "5.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.IdentityModel.Tokens.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.NETCore.Platforms/1.1.0": {}, | ||||
|       "Microsoft.NETCore.Targets/1.1.0": {}, | ||||
|       "Microsoft.SqlServer.Server/1.0.0": { | ||||
|         "runtime": { | ||||
|           "lib/netstandard2.0/Microsoft.SqlServer.Server.dll": { | ||||
|             "assemblyVersion": "1.0.0.0", | ||||
|             "fileVersion": "1.0.0.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Microsoft.Win32.SystemEvents/6.0.0": { | ||||
|         "runtime": { | ||||
|           "lib/net6.0/Microsoft.Win32.SystemEvents.dll": { | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         }, | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/win/lib/net6.0/Microsoft.Win32.SystemEvents.dll": { | ||||
|             "rid": "win", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Serilog/4.3.0": { | ||||
|         "runtime": { | ||||
|           "lib/net9.0/Serilog.dll": { | ||||
|             "assemblyVersion": "4.3.0.0", | ||||
|             "fileVersion": "4.3.0.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.ClientModel/1.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Memory.Data": "1.0.2", | ||||
|           "System.Text.Json": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.ClientModel.dll": { | ||||
|             "assemblyVersion": "1.0.0.0", | ||||
|             "fileVersion": "1.0.24.5302" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Configuration.ConfigurationManager/6.0.1": { | ||||
|         "dependencies": { | ||||
|           "System.Security.Cryptography.ProtectedData": "6.0.0", | ||||
|           "System.Security.Permissions": "6.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.Configuration.ConfigurationManager.dll": { | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.922.41905" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Diagnostics.DiagnosticSource/6.0.1": { | ||||
|         "dependencies": { | ||||
|           "System.Runtime.CompilerServices.Unsafe": "6.0.0" | ||||
|         } | ||||
|       }, | ||||
|       "System.Drawing.Common/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.Win32.SystemEvents": "6.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.Drawing.Common.dll": { | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         }, | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/unix/lib/net6.0/System.Drawing.Common.dll": { | ||||
|             "rid": "unix", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           }, | ||||
|           "runtimes/win/lib/net6.0/System.Drawing.Common.dll": { | ||||
|             "rid": "win", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Formats.Asn1/9.0.8": { | ||||
|         "runtime": { | ||||
|           "lib/net9.0/System.Formats.Asn1.dll": { | ||||
|             "assemblyVersion": "9.0.0.0", | ||||
|             "fileVersion": "9.0.825.36511" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.IdentityModel.Tokens.Jwt/6.35.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.IdentityModel.JsonWebTokens": "6.35.0", | ||||
|           "Microsoft.IdentityModel.Tokens": "6.35.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.IdentityModel.Tokens.Jwt.dll": { | ||||
|             "assemblyVersion": "6.35.0.0", | ||||
|             "fileVersion": "6.35.0.41201" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Memory/4.5.4": {}, | ||||
|       "System.Memory.Data/1.0.2": { | ||||
|         "dependencies": { | ||||
|           "System.Text.Encodings.Web": "6.0.0", | ||||
|           "System.Text.Json": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/netstandard2.0/System.Memory.Data.dll": { | ||||
|             "assemblyVersion": "1.0.2.0", | ||||
|             "fileVersion": "1.0.221.20802" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Numerics.Vectors/4.5.0": {}, | ||||
|       "System.Runtime/4.3.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.NETCore.Platforms": "1.1.0", | ||||
|           "Microsoft.NETCore.Targets": "1.1.0" | ||||
|         } | ||||
|       }, | ||||
|       "System.Runtime.Caching/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Configuration.ConfigurationManager": "6.0.1" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.Runtime.Caching.dll": { | ||||
|             "assemblyVersion": "4.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         }, | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/win/lib/net6.0/System.Runtime.Caching.dll": { | ||||
|             "rid": "win", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "4.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Runtime.CompilerServices.Unsafe/6.0.0": {}, | ||||
|       "System.Security.AccessControl/6.0.0": {}, | ||||
|       "System.Security.Cryptography.Cng/5.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Formats.Asn1": "9.0.8" | ||||
|         } | ||||
|       }, | ||||
|       "System.Security.Cryptography.ProtectedData/6.0.0": { | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.Security.Cryptography.ProtectedData.dll": { | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         }, | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/win/lib/net6.0/System.Security.Cryptography.ProtectedData.dll": { | ||||
|             "rid": "win", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Security.Permissions/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Security.AccessControl": "6.0.0", | ||||
|           "System.Windows.Extensions": "6.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.Security.Permissions.dll": { | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Security.Principal.Windows/5.0.0": {}, | ||||
|       "System.Text.Encoding/4.3.0": { | ||||
|         "dependencies": { | ||||
|           "Microsoft.NETCore.Platforms": "1.1.0", | ||||
|           "Microsoft.NETCore.Targets": "1.1.0", | ||||
|           "System.Runtime": "4.3.0" | ||||
|         } | ||||
|       }, | ||||
|       "System.Text.Encoding.CodePages/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Runtime.CompilerServices.Unsafe": "6.0.0" | ||||
|         } | ||||
|       }, | ||||
|       "System.Text.Encodings.Web/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Runtime.CompilerServices.Unsafe": "6.0.0" | ||||
|         } | ||||
|       }, | ||||
|       "System.Text.Json/9.0.8": { | ||||
|         "runtime": { | ||||
|           "lib/net9.0/System.Text.Json.dll": { | ||||
|             "assemblyVersion": "9.0.0.0", | ||||
|             "fileVersion": "9.0.825.36511" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Threading.RateLimiting/9.0.8": { | ||||
|         "runtime": { | ||||
|           "lib/net9.0/System.Threading.RateLimiting.dll": { | ||||
| @@ -178,6 +667,26 @@ | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "System.Threading.Tasks.Extensions/4.5.4": {}, | ||||
|       "System.Windows.Extensions/6.0.0": { | ||||
|         "dependencies": { | ||||
|           "System.Drawing.Common": "6.0.0" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "lib/net6.0/System.Windows.Extensions.dll": { | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         }, | ||||
|         "runtimeTargets": { | ||||
|           "runtimes/win/lib/net6.0/System.Windows.Extensions.dll": { | ||||
|             "rid": "win", | ||||
|             "assetType": "runtime", | ||||
|             "assemblyVersion": "6.0.0.0", | ||||
|             "fileVersion": "6.0.21.52210" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Elecciones.Core/1.0.0": { | ||||
|         "runtime": { | ||||
|           "Elecciones.Core.dll": { | ||||
| @@ -185,6 +694,18 @@ | ||||
|             "fileVersion": "1.0.0.0" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "Elecciones.Database/1.0.0": { | ||||
|         "dependencies": { | ||||
|           "Elecciones.Core": "1.0.0", | ||||
|           "Microsoft.EntityFrameworkCore.SqlServer": "9.0.8" | ||||
|         }, | ||||
|         "runtime": { | ||||
|           "Elecciones.Database.dll": { | ||||
|             "assemblyVersion": "1.0.0.0", | ||||
|             "fileVersion": "1.0.0.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -194,6 +715,97 @@ | ||||
|       "serviceable": false, | ||||
|       "sha512": "" | ||||
|     }, | ||||
|     "Azure.Core/1.38.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-IuEgCoVA0ef7E4pQtpC3+TkPbzaoQfa77HlfJDmfuaJUCVJmn7fT0izamZiryW5sYUFKizsftIxMkXKbgIcPMQ==", | ||||
|       "path": "azure.core/1.38.0", | ||||
|       "hashPath": "azure.core.1.38.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Azure.Identity/1.11.4": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-Sf4BoE6Q3jTgFkgBkx7qztYOFELBCo+wQgpYDwal/qJ1unBH73ywPztIJKXBXORRzAeNijsuxhk94h0TIMvfYg==", | ||||
|       "path": "azure.identity/1.11.4", | ||||
|       "hashPath": "azure.identity.1.11.4.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Bcl.AsyncInterfaces/1.1.1": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==", | ||||
|       "path": "microsoft.bcl.asyncinterfaces/1.1.1", | ||||
|       "hashPath": "microsoft.bcl.asyncinterfaces.1.1.1.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.CSharp/4.5.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-kaj6Wb4qoMuH3HySFJhxwQfe8R/sJsNJnANrvv8WdFPMoNbKY5htfNscv+LHCu5ipz+49m2e+WQXpLXr9XYemQ==", | ||||
|       "path": "microsoft.csharp/4.5.0", | ||||
|       "hashPath": "microsoft.csharp.4.5.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Data.SqlClient/5.1.6": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-+pz7gIPh5ydsBcQvivt4R98PwJXer86fyQBBToIBLxZ5kuhW4N13Ijz87s9WpuPtF1vh4JesYCgpDPAOgkMhdg==", | ||||
|       "path": "microsoft.data.sqlclient/5.1.6", | ||||
|       "hashPath": "microsoft.data.sqlclient.5.1.6.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Data.SqlClient.SNI.runtime/5.1.1": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-wNGM5ZTQCa2blc9ikXQouybGiyMd6IHPVJvAlBEPtr6JepZEOYeDxGyprYvFVeOxlCXs7avridZQ0nYkHzQWCQ==", | ||||
|       "path": "microsoft.data.sqlclient.sni.runtime/5.1.1", | ||||
|       "hashPath": "microsoft.data.sqlclient.sni.runtime.5.1.1.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.EntityFrameworkCore/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-bNGdPhN762+BIIO5MFYLjafRqkSS1MqLOc/erd55InvLnFxt9H3N5JNsuag1ZHyBor1VtD42U0CHpgqkWeAYgQ==", | ||||
|       "path": "microsoft.entityframeworkcore/9.0.8", | ||||
|       "hashPath": "microsoft.entityframeworkcore.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.EntityFrameworkCore.Abstractions/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-B2yfAIQRRAQ4zvvWqh+HudD+juV3YoLlpXnrog3tU0PM9AFpuq6xo0+mEglN1P43WgdcUiF+65CWBcZe35s15Q==", | ||||
|       "path": "microsoft.entityframeworkcore.abstractions/9.0.8", | ||||
|       "hashPath": "microsoft.entityframeworkcore.abstractions.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.EntityFrameworkCore.Analyzers/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-2EYStCXt4Hi9p3J3EYMQbItJDtASJd064Kcs8C8hj8Jt5srILrR9qlaL0Ryvk8NrWQoCQvIELsmiuqLEZMLvGA==", | ||||
|       "path": "microsoft.entityframeworkcore.analyzers/9.0.8", | ||||
|       "hashPath": "microsoft.entityframeworkcore.analyzers.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.EntityFrameworkCore.Relational/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-OVhfyxiHxMvYpwQ8Jy3YZi4koy6TK5/Q7C1oq3z6db+HEGuu6x9L1BX5zDIdJxxlRePMyO4D8ORiXj/D7+MUqw==", | ||||
|       "path": "microsoft.entityframeworkcore.relational/9.0.8", | ||||
|       "hashPath": "microsoft.entityframeworkcore.relational.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.EntityFrameworkCore.SqlServer/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-yNZJIdLQTTHj6FTv9+IUQwmQvOwvUanTBOG1ibeTaaB1zfTtOqrSFQnjMOkcKOgxu+ofsBEDcuctb/f5xj/Oog==", | ||||
|       "path": "microsoft.entityframeworkcore.sqlserver/9.0.8", | ||||
|       "hashPath": "microsoft.entityframeworkcore.sqlserver.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Extensions.Caching.Abstractions/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-4h7bsVoKoiK+SlPM+euX/ayGnKZhl47pPCidLTiio9xyG+vgVVfcYxcYQgjm0SCrdSxjG0EGIAKF8EFr3G8Ifw==", | ||||
|       "path": "microsoft.extensions.caching.abstractions/9.0.8", | ||||
|       "hashPath": "microsoft.extensions.caching.abstractions.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Extensions.Caching.Memory/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-grR+oPyj8HVn4DT8CFUUdSw2pZZKS13KjytFe4txpHQliGM1GEDotohmjgvyl3hm7RFB3FRqvbouEX3/1ewp5A==", | ||||
|       "path": "microsoft.extensions.caching.memory/9.0.8", | ||||
|       "hashPath": "microsoft.extensions.caching.memory.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Extensions.Configuration/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
| @@ -285,6 +897,244 @@ | ||||
|       "path": "microsoft.extensions.primitives/9.0.8", | ||||
|       "hashPath": "microsoft.extensions.primitives.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Identity.Client/4.61.3": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-naJo/Qm35Caaoxp5utcw+R8eU8ZtLz2ALh8S+gkekOYQ1oazfCQMWVT4NJ/FnHzdIJlm8dMz0oMpMGCabx5odA==", | ||||
|       "path": "microsoft.identity.client/4.61.3", | ||||
|       "hashPath": "microsoft.identity.client.4.61.3.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Identity.Client.Extensions.Msal/4.61.3": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-PWnJcznrSGr25MN8ajlc2XIDW4zCFu0U6FkpaNLEWLgd1NgFCp5uDY3mqLDgM8zCN8hqj8yo5wHYfLB2HjcdGw==", | ||||
|       "path": "microsoft.identity.client.extensions.msal/4.61.3", | ||||
|       "hashPath": "microsoft.identity.client.extensions.msal.4.61.3.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.IdentityModel.Abstractions/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-xuR8E4Rd96M41CnUSCiOJ2DBh+z+zQSmyrYHdYhD6K4fXBcQGVnRCFQ0efROUYpP+p0zC1BLKr0JRpVuujTZSg==", | ||||
|       "path": "microsoft.identitymodel.abstractions/6.35.0", | ||||
|       "hashPath": "microsoft.identitymodel.abstractions.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.IdentityModel.JsonWebTokens/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-9wxai3hKgZUb4/NjdRKfQd0QJvtXKDlvmGMYACbEC8DFaicMFCFhQFZq9ZET1kJLwZahf2lfY5Gtcpsx8zYzbg==", | ||||
|       "path": "microsoft.identitymodel.jsonwebtokens/6.35.0", | ||||
|       "hashPath": "microsoft.identitymodel.jsonwebtokens.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.IdentityModel.Logging/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-jePrSfGAmqT81JDCNSY+fxVWoGuJKt9e6eJ+vT7+quVS55nWl//jGjUQn4eFtVKt4rt5dXaleZdHRB9J9AJZ7Q==", | ||||
|       "path": "microsoft.identitymodel.logging/6.35.0", | ||||
|       "hashPath": "microsoft.identitymodel.logging.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.IdentityModel.Protocols/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-BPQhlDzdFvv1PzaUxNSk+VEPwezlDEVADIKmyxubw7IiELK18uJ06RQ9QKKkds30XI+gDu9n8j24XQ8w7fjWcg==", | ||||
|       "path": "microsoft.identitymodel.protocols/6.35.0", | ||||
|       "hashPath": "microsoft.identitymodel.protocols.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.IdentityModel.Protocols.OpenIdConnect/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-LMtVqnECCCdSmyFoCOxIE5tXQqkOLrvGrL7OxHg41DIm1bpWtaCdGyVcTAfOQpJXvzND9zUKIN/lhngPkYR8vg==", | ||||
|       "path": "microsoft.identitymodel.protocols.openidconnect/6.35.0", | ||||
|       "hashPath": "microsoft.identitymodel.protocols.openidconnect.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.IdentityModel.Tokens/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-RN7lvp7s3Boucg1NaNAbqDbxtlLj5Qeb+4uSS1TeK5FSBVM40P4DKaTKChT43sHyKfh7V0zkrMph6DdHvyA4bg==", | ||||
|       "path": "microsoft.identitymodel.tokens/6.35.0", | ||||
|       "hashPath": "microsoft.identitymodel.tokens.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.NETCore.Platforms/1.1.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", | ||||
|       "path": "microsoft.netcore.platforms/1.1.0", | ||||
|       "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.NETCore.Targets/1.1.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==", | ||||
|       "path": "microsoft.netcore.targets/1.1.0", | ||||
|       "hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.SqlServer.Server/1.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-N4KeF3cpcm1PUHym1RmakkzfkEv3GRMyofVv40uXsQhCQeglr2OHNcUk2WOG51AKpGO8ynGpo9M/kFXSzghwug==", | ||||
|       "path": "microsoft.sqlserver.server/1.0.0", | ||||
|       "hashPath": "microsoft.sqlserver.server.1.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Microsoft.Win32.SystemEvents/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==", | ||||
|       "path": "microsoft.win32.systemevents/6.0.0", | ||||
|       "hashPath": "microsoft.win32.systemevents.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Serilog/4.3.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==", | ||||
|       "path": "serilog/4.3.0", | ||||
|       "hashPath": "serilog.4.3.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.ClientModel/1.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-I3CVkvxeqFYjIVEP59DnjbeoGNfo/+SZrCLpRz2v/g0gpCHaEMPtWSY0s9k/7jR1rAsLNg2z2u1JRB76tPjnIw==", | ||||
|       "path": "system.clientmodel/1.0.0", | ||||
|       "hashPath": "system.clientmodel.1.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Configuration.ConfigurationManager/6.0.1": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-jXw9MlUu/kRfEU0WyTptAVueupqIeE3/rl0EZDMlf8pcvJnitQ8HeVEp69rZdaStXwTV72boi/Bhw8lOeO+U2w==", | ||||
|       "path": "system.configuration.configurationmanager/6.0.1", | ||||
|       "hashPath": "system.configuration.configurationmanager.6.0.1.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Diagnostics.DiagnosticSource/6.0.1": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", | ||||
|       "path": "system.diagnostics.diagnosticsource/6.0.1", | ||||
|       "hashPath": "system.diagnostics.diagnosticsource.6.0.1.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Drawing.Common/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", | ||||
|       "path": "system.drawing.common/6.0.0", | ||||
|       "hashPath": "system.drawing.common.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Formats.Asn1/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-gGL0gt2nAArsF2oOMFzClll6QN2FhtooTxEQ+K26uer4lrhahnYIo/qOn5HUSfjHlM91646L5/7dYIMJ86fHkQ==", | ||||
|       "path": "system.formats.asn1/9.0.8", | ||||
|       "hashPath": "system.formats.asn1.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "System.IdentityModel.Tokens.Jwt/6.35.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-yxGIQd3BFK7F6S62/7RdZk3C/mfwyVxvh6ngd1VYMBmbJ1YZZA9+Ku6suylVtso0FjI0wbElpJ0d27CdsyLpBQ==", | ||||
|       "path": "system.identitymodel.tokens.jwt/6.35.0", | ||||
|       "hashPath": "system.identitymodel.tokens.jwt.6.35.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Memory/4.5.4": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", | ||||
|       "path": "system.memory/4.5.4", | ||||
|       "hashPath": "system.memory.4.5.4.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Memory.Data/1.0.2": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-JGkzeqgBsiZwKJZ1IxPNsDFZDhUvuEdX8L8BDC8N3KOj+6zMcNU28CNN59TpZE/VJYy9cP+5M+sbxtWJx3/xtw==", | ||||
|       "path": "system.memory.data/1.0.2", | ||||
|       "hashPath": "system.memory.data.1.0.2.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Numerics.Vectors/4.5.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==", | ||||
|       "path": "system.numerics.vectors/4.5.0", | ||||
|       "hashPath": "system.numerics.vectors.4.5.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Runtime/4.3.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", | ||||
|       "path": "system.runtime/4.3.0", | ||||
|       "hashPath": "system.runtime.4.3.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Runtime.Caching/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-E0e03kUp5X2k+UAoVl6efmI7uU7JRBWi5EIdlQ7cr0NpBGjHG4fWII35PgsBY9T4fJQ8E4QPsL0rKksU9gcL5A==", | ||||
|       "path": "system.runtime.caching/6.0.0", | ||||
|       "hashPath": "system.runtime.caching.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Runtime.CompilerServices.Unsafe/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", | ||||
|       "path": "system.runtime.compilerservices.unsafe/6.0.0", | ||||
|       "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Security.AccessControl/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==", | ||||
|       "path": "system.security.accesscontrol/6.0.0", | ||||
|       "hashPath": "system.security.accesscontrol.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Security.Cryptography.Cng/5.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-jIMXsKn94T9JY7PvPq/tMfqa6GAaHpElRDpmG+SuL+D3+sTw2M8VhnibKnN8Tq+4JqbPJ/f+BwtLeDMEnzAvRg==", | ||||
|       "path": "system.security.cryptography.cng/5.0.0", | ||||
|       "hashPath": "system.security.cryptography.cng.5.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Security.Cryptography.ProtectedData/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-rp1gMNEZpvx9vP0JW0oHLxlf8oSiQgtno77Y4PLUBjSiDYoD77Y8uXHr1Ea5XG4/pIKhqAdxZ8v8OTUtqo9PeQ==", | ||||
|       "path": "system.security.cryptography.protecteddata/6.0.0", | ||||
|       "hashPath": "system.security.cryptography.protecteddata.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Security.Permissions/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-T/uuc7AklkDoxmcJ7LGkyX1CcSviZuLCa4jg3PekfJ7SU0niF0IVTXwUiNVP9DSpzou2PpxJ+eNY2IfDM90ZCg==", | ||||
|       "path": "system.security.permissions/6.0.0", | ||||
|       "hashPath": "system.security.permissions.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Security.Principal.Windows/5.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==", | ||||
|       "path": "system.security.principal.windows/5.0.0", | ||||
|       "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Text.Encoding/4.3.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", | ||||
|       "path": "system.text.encoding/4.3.0", | ||||
|       "hashPath": "system.text.encoding.4.3.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Text.Encoding.CodePages/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", | ||||
|       "path": "system.text.encoding.codepages/6.0.0", | ||||
|       "hashPath": "system.text.encoding.codepages.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Text.Encodings.Web/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", | ||||
|       "path": "system.text.encodings.web/6.0.0", | ||||
|       "hashPath": "system.text.encodings.web.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Text.Json/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-mIQir9jBqk0V7X0Nw5hzPJZC8DuGdf+2DS3jAVsr6rq5+/VyH5rza0XGcONJUWBrZ+G6BCwNyjWYd9lncBu48A==", | ||||
|       "path": "system.text.json/9.0.8", | ||||
|       "hashPath": "system.text.json.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Threading.RateLimiting/9.0.8": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
| @@ -292,10 +1142,29 @@ | ||||
|       "path": "system.threading.ratelimiting/9.0.8", | ||||
|       "hashPath": "system.threading.ratelimiting.9.0.8.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Threading.Tasks.Extensions/4.5.4": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", | ||||
|       "path": "system.threading.tasks.extensions/4.5.4", | ||||
|       "hashPath": "system.threading.tasks.extensions.4.5.4.nupkg.sha512" | ||||
|     }, | ||||
|     "System.Windows.Extensions/6.0.0": { | ||||
|       "type": "package", | ||||
|       "serviceable": true, | ||||
|       "sha512": "sha512-IXoJOXIqc39AIe+CIR7koBtRGMiCt/LPM3lI+PELtDIy9XdyeSrwXFdWV9dzJ2Awl0paLWUaknLxFQ5HpHZUog==", | ||||
|       "path": "system.windows.extensions/6.0.0", | ||||
|       "hashPath": "system.windows.extensions.6.0.0.nupkg.sha512" | ||||
|     }, | ||||
|     "Elecciones.Core/1.0.0": { | ||||
|       "type": "project", | ||||
|       "serviceable": false, | ||||
|       "sha512": "" | ||||
|     }, | ||||
|     "Elecciones.Database/1.0.0": { | ||||
|       "type": "project", | ||||
|       "serviceable": false, | ||||
|       "sha512": "" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -13,7 +13,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d78a02a0ebc4c70ea01e48821db963110e7ce280")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f384a640f36be1289d652dc85e78ebdcef30968a")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Infrastructure")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -13,3 +13,5 @@ E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0 | ||||
| E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0\refint\Elecciones.Infrastructure.dll | ||||
| E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0\Elecciones.Infrastructure.pdb | ||||
| E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\obj\Debug\net9.0\ref\Elecciones.Infrastructure.dll | ||||
| E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\bin\Debug\net9.0\Elecciones.Database.dll | ||||
| E:\Elecciones-2025\Elecciones-Web\src\Elecciones.Infrastructure\bin\Debug\net9.0\Elecciones.Database.pdb | ||||
|   | ||||
| @@ -69,14 +69,14 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\Elecciones.Infrastructure.csproj": { | ||||
|     "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj": { | ||||
|       "version": "1.0.0", | ||||
|       "restore": { | ||||
|         "projectUniqueName": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\Elecciones.Infrastructure.csproj", | ||||
|         "projectName": "Elecciones.Infrastructure", | ||||
|         "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\Elecciones.Infrastructure.csproj", | ||||
|         "projectUniqueName": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj", | ||||
|         "projectName": "Elecciones.Database", | ||||
|         "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj", | ||||
|         "packagesPath": "C:\\Users\\dmolinari\\.nuget\\packages\\", | ||||
|         "outputPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\obj\\", | ||||
|         "outputPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\obj\\", | ||||
|         "projectStyle": "PackageReference", | ||||
|         "fallbackFolders": [ | ||||
|           "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages" | ||||
| @@ -115,6 +115,90 @@ | ||||
|         }, | ||||
|         "SdkAnalysisLevel": "9.0.300" | ||||
|       }, | ||||
|       "frameworks": { | ||||
|         "net9.0": { | ||||
|           "targetAlias": "net9.0", | ||||
|           "dependencies": { | ||||
|             "Microsoft.EntityFrameworkCore.Design": { | ||||
|               "include": "Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive", | ||||
|               "suppressParent": "All", | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|             }, | ||||
|             "Microsoft.EntityFrameworkCore.SqlServer": { | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|             } | ||||
|           }, | ||||
|           "imports": [ | ||||
|             "net461", | ||||
|             "net462", | ||||
|             "net47", | ||||
|             "net471", | ||||
|             "net472", | ||||
|             "net48", | ||||
|             "net481" | ||||
|           ], | ||||
|           "assetTargetFallback": true, | ||||
|           "warn": true, | ||||
|           "frameworkReferences": { | ||||
|             "Microsoft.NETCore.App": { | ||||
|               "privateAssets": "all" | ||||
|             } | ||||
|           }, | ||||
|           "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.300/PortableRuntimeIdentifierGraph.json" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\Elecciones.Infrastructure.csproj": { | ||||
|       "version": "1.0.0", | ||||
|       "restore": { | ||||
|         "projectUniqueName": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\Elecciones.Infrastructure.csproj", | ||||
|         "projectName": "Elecciones.Infrastructure", | ||||
|         "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\Elecciones.Infrastructure.csproj", | ||||
|         "packagesPath": "C:\\Users\\dmolinari\\.nuget\\packages\\", | ||||
|         "outputPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Infrastructure\\obj\\", | ||||
|         "projectStyle": "PackageReference", | ||||
|         "fallbackFolders": [ | ||||
|           "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages" | ||||
|         ], | ||||
|         "configFilePaths": [ | ||||
|           "C:\\Users\\dmolinari\\AppData\\Roaming\\NuGet\\NuGet.Config", | ||||
|           "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", | ||||
|           "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" | ||||
|         ], | ||||
|         "originalTargetFrameworks": [ | ||||
|           "net9.0" | ||||
|         ], | ||||
|         "sources": { | ||||
|           "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, | ||||
|           "https://api.nuget.org/v3/index.json": {} | ||||
|         }, | ||||
|         "frameworks": { | ||||
|           "net9.0": { | ||||
|             "targetAlias": "net9.0", | ||||
|             "projectReferences": { | ||||
|               "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Core\\Elecciones.Core.csproj": { | ||||
|                 "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Core\\Elecciones.Core.csproj" | ||||
|               }, | ||||
|               "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj": { | ||||
|                 "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "warningProperties": { | ||||
|           "warnAsError": [ | ||||
|             "NU1605" | ||||
|           ] | ||||
|         }, | ||||
|         "restoreAuditProperties": { | ||||
|           "enableAudit": "true", | ||||
|           "auditLevel": "low", | ||||
|           "auditMode": "direct" | ||||
|         }, | ||||
|         "SdkAnalysisLevel": "9.0.300" | ||||
|       }, | ||||
|       "frameworks": { | ||||
|         "net9.0": { | ||||
|           "targetAlias": "net9.0", | ||||
| @@ -127,6 +211,10 @@ | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|             }, | ||||
|             "Serilog": { | ||||
|               "target": "Package", | ||||
|               "version": "[4.3.0, )" | ||||
|             }, | ||||
|             "System.Threading.RateLimiting": { | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|   | ||||
| @@ -13,4 +13,7 @@ | ||||
|     <SourceRoot Include="C:\Users\dmolinari\.nuget\packages\" /> | ||||
|     <SourceRoot Include="D:\Microsoft\VisualStudio\Microsoft Visual Studio\Shared\NuGetPackages\" /> | ||||
|   </ItemGroup> | ||||
|   <ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> | ||||
|     <Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore\9.0.8\buildTransitive\net8.0\Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore\9.0.8\buildTransitive\net8.0\Microsoft.EntityFrameworkCore.props')" /> | ||||
|   </ImportGroup> | ||||
| </Project> | ||||
| @@ -1,6 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8" standalone="no"?> | ||||
| <Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||||
|   <ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> | ||||
|     <Import Project="$(NuGetPackageRoot)system.text.json\9.0.8\buildTransitive\net8.0\System.Text.Json.targets" Condition="Exists('$(NuGetPackageRoot)system.text.json\9.0.8\buildTransitive\net8.0\System.Text.Json.targets')" /> | ||||
|     <Import Project="$(NuGetPackageRoot)serilog\4.3.0\build\Serilog.targets" Condition="Exists('$(NuGetPackageRoot)serilog\4.3.0\build\Serilog.targets')" /> | ||||
|     <Import Project="$(NuGetPackageRoot)microsoft.extensions.options\9.0.8\buildTransitive\net8.0\Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options\9.0.8\buildTransitive\net8.0\Microsoft.Extensions.Options.targets')" /> | ||||
|     <Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder\9.0.8\buildTransitive\netstandard2.0\Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder\9.0.8\buildTransitive\netstandard2.0\Microsoft.Extensions.Configuration.Binder.targets')" /> | ||||
|     <Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\9.0.8\buildTransitive\net8.0\Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\9.0.8\buildTransitive\net8.0\Microsoft.Extensions.Logging.Abstractions.targets')" /> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| //Elecciones.Worker/CriticalDataWorker.cs | ||||
| using Elecciones.Database; | ||||
| using Elecciones.Database.Entities; | ||||
| using Elecciones.Infrastructure.Services; | ||||
| @@ -12,17 +13,20 @@ public class CriticalDataWorker : BackgroundService | ||||
|   private readonly SharedTokenService _tokenService; | ||||
|   private readonly IServiceProvider _serviceProvider; | ||||
|   private readonly IElectoralApiService _apiService; | ||||
|   private readonly WorkerConfigService _configService; | ||||
|  | ||||
|   public CriticalDataWorker( | ||||
|       ILogger<CriticalDataWorker> logger, | ||||
|       SharedTokenService tokenService, | ||||
|       IServiceProvider serviceProvider, | ||||
|       IElectoralApiService apiService) | ||||
|       IElectoralApiService apiService, | ||||
|       WorkerConfigService configService) | ||||
|   { | ||||
|     _logger = logger; | ||||
|     _tokenService = tokenService; | ||||
|     _serviceProvider = serviceProvider; | ||||
|     _apiService = apiService; | ||||
|     _configService = configService; | ||||
|   } | ||||
|  | ||||
|   protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
| @@ -50,9 +54,25 @@ public class CriticalDataWorker : BackgroundService | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | ||||
|       await SondearResumenProvincialAsync(authToken, stoppingToken); | ||||
|       await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | ||||
|       var settings = await _configService.GetSettingsAsync(); | ||||
|  | ||||
|       if (settings.Prioridad == "Resultados" && settings.ResultadosActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Ejecutando tareas de Resultados en alta prioridad."); | ||||
|         await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | ||||
|         await SondearResumenProvincialAsync(authToken, stoppingToken); | ||||
|         await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else if (settings.Prioridad == "Telegramas" && settings.BajasActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Ejecutando tareas de Baja Prioridad en alta prioridad."); | ||||
|         await SondearProyeccionBancasAsync(authToken, stoppingToken); | ||||
|         await SondearNuevosTelegramasAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         _logger.LogInformation("Worker de alta prioridad inactivo según la configuración."); | ||||
|       } | ||||
|  | ||||
|       var cicloFin = DateTime.UtcNow; | ||||
|       var duracionCiclo = cicloFin - cicloInicio; | ||||
| @@ -69,6 +89,252 @@ public class CriticalDataWorker : BackgroundService | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral. | ||||
|   /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas, | ||||
|   /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos. | ||||
|   /// </summary> | ||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||
|   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||
|   private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       var categoriasDeBancas = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS")) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       var provincia = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | ||||
|  | ||||
|       var seccionesElectorales = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!categoriasDeBancas.Any() || provincia == null) | ||||
|       { | ||||
|         _logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales...", seccionesElectorales.Count); | ||||
|  | ||||
|       var todasLasProyecciones = new List<ProyeccionBanca>(); | ||||
|       bool hasReceivedAnyNewData = false; | ||||
|  | ||||
|       // Bucle para el nivel Provincial | ||||
|       foreach (var categoria in categoriasDeBancas) | ||||
|       { | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|         var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id); | ||||
|  | ||||
|         if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) | ||||
|         { | ||||
|           hasReceivedAnyNewData = true; | ||||
|  | ||||
|           // --- SEGURIDAD: Usar TryParse para la fecha --- | ||||
|           DateTime fechaTotalizacion; | ||||
|           if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|           { | ||||
|             // Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo. | ||||
|             _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); | ||||
|             fechaTotalizacion = DateTime.UtcNow; | ||||
|           } | ||||
|           else | ||||
|           { | ||||
|             fechaTotalizacion = parsedDate.ToUniversalTime(); | ||||
|           } | ||||
|  | ||||
|           foreach (var banca in bancas) | ||||
|           { | ||||
|             todasLasProyecciones.Add(new ProyeccionBanca | ||||
|             { | ||||
|               AmbitoGeograficoId = provincia.Id, | ||||
|               AgrupacionPoliticaId = banca.IdAgrupacion, | ||||
|               NroBancas = banca.NroBancas, | ||||
|               CategoriaId = categoria.Id, | ||||
|               FechaTotalizacion = fechaTotalizacion | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Bucle para el nivel de Sección Electoral | ||||
|       foreach (var seccion in seccionesElectorales) | ||||
|       { | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|         foreach (var categoria in categoriasDeBancas) | ||||
|         { | ||||
|           if (stoppingToken.IsCancellationRequested) break; | ||||
|           var repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id); | ||||
|  | ||||
|           if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas) | ||||
|           { | ||||
|             hasReceivedAnyNewData = true; | ||||
|  | ||||
|             // --- APLICAMOS LA MISMA SEGURIDAD AQUÍ --- | ||||
|             DateTime fechaTotalizacion; | ||||
|             if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|             { | ||||
|               _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion); | ||||
|               fechaTotalizacion = DateTime.UtcNow; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|               fechaTotalizacion = parsedDate.ToUniversalTime(); | ||||
|             } | ||||
|  | ||||
|             foreach (var banca in bancas) | ||||
|             { | ||||
|               todasLasProyecciones.Add(new ProyeccionBanca | ||||
|               { | ||||
|                 AmbitoGeograficoId = seccion.Id, | ||||
|                 AgrupacionPoliticaId = banca.IdAgrupacion, | ||||
|                 NroBancas = banca.NroBancas, | ||||
|                 CategoriaId = categoria.Id, | ||||
|                 FechaTotalizacion = fechaTotalizacion | ||||
|               }); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (hasReceivedAnyNewData) | ||||
|       { | ||||
|         _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos..."); | ||||
|         await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||
|  | ||||
|         await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken); | ||||
|         await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken); | ||||
|         await dbContext.SaveChangesAsync(stoppingToken); | ||||
|         await transaction.CommitAsync(stoppingToken); | ||||
|  | ||||
|         _logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada."); | ||||
|       } | ||||
|     } | ||||
|     catch (OperationCanceledException) | ||||
|     { | ||||
|       _logger.LogInformation("Sondeo de bancas cancelado."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Busca y descarga nuevos telegramas de forma masiva y concurrente. | ||||
|   /// Este método crea una lista de todas las combinaciones de Partido/Categoría, | ||||
|   /// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente | ||||
|   /// maneja su propia lógica de descarga y guardado en la base de datos. | ||||
|   /// </summary> | ||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||
|   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||
|   private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---"); | ||||
|  | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       var partidos = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       var categorias = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!partidos.Any() || !categorias.Any()) return; | ||||
|  | ||||
|       foreach (var partido in partidos) | ||||
|       { | ||||
|         foreach (var categoria in categorias) | ||||
|         { | ||||
|           if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|           var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id); | ||||
|  | ||||
|           if (listaTelegramasApi is { Count: > 0 }) | ||||
|           { | ||||
|             using var innerScope = _serviceProvider.CreateScope(); | ||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|             var idsYaEnDb = await innerDbContext.Telegramas | ||||
|                 .Where(t => listaTelegramasApi.Contains(t.Id)) | ||||
|                 .Select(t => t.Id) | ||||
|                 .ToListAsync(stoppingToken); | ||||
|  | ||||
|             var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList(); | ||||
|  | ||||
|             if (nuevosTelegramasIds.Any()) | ||||
|             { | ||||
|               _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre); | ||||
|  | ||||
|               foreach (var mesaId in nuevosTelegramasIds) | ||||
|               { | ||||
|                 if (stoppingToken.IsCancellationRequested) return; | ||||
|  | ||||
|                 var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||
|                 if (telegramaFile != null) | ||||
|                 { | ||||
|                   // 1. Buscamos el AmbitoGeografico específico de la MESA que estamos procesando. | ||||
|                   var ambitoMesa = await innerDbContext.AmbitosGeograficos | ||||
|                       .AsNoTracking() | ||||
|                       .FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken); | ||||
|  | ||||
|                   // 2. Solo guardamos el telegrama si encontramos su ámbito de mesa correspondiente. | ||||
|                   if (ambitoMesa != null) | ||||
|                   { | ||||
|                     var nuevoTelegrama = new Telegrama | ||||
|                     { | ||||
|                       Id = telegramaFile.NombreArchivo, | ||||
|                       // 3. Usamos el ID del ÁMBITO DE LA MESA, no el del municipio. | ||||
|                       AmbitoGeograficoId = ambitoMesa.Id, | ||||
|                       ContenidoBase64 = telegramaFile.Imagen, | ||||
|                       FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(), | ||||
|                       FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime() | ||||
|                     }; | ||||
|                     await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken); | ||||
|                   } | ||||
|                   else | ||||
|                   { | ||||
|                     _logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId); | ||||
|                   } | ||||
|                 } | ||||
|                 await Task.Delay(250, stoppingToken); | ||||
|               } | ||||
|               await innerDbContext.SaveChangesAsync(stoppingToken); | ||||
|             } | ||||
|           } | ||||
|           await Task.Delay(100, stoppingToken); | ||||
|         } | ||||
|       } | ||||
|       _logger.LogInformation("Sondeo de Telegramas completado."); | ||||
|     } | ||||
|     catch (OperationCanceledException) | ||||
|     { | ||||
|       _logger.LogInformation("Sondeo de telegramas cancelado."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| //Elecciones.Worker/LowPriorityDataWorker.cs | ||||
| using Elecciones.Database; | ||||
| using Elecciones.Database.Entities; | ||||
| using Elecciones.Infrastructure.Services; | ||||
| @@ -11,6 +12,7 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|   private readonly SharedTokenService _tokenService; | ||||
|   private readonly IServiceProvider _serviceProvider; | ||||
|   private readonly IElectoralApiService _apiService; | ||||
|   private readonly WorkerConfigService _configService; | ||||
|  | ||||
|   // Una variable para rastrear la tarea de telegramas, si está en ejecución. | ||||
|   private Task? _telegramasTask; | ||||
| @@ -19,12 +21,14 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|   ILogger<LowPriorityDataWorker> logger, | ||||
|   SharedTokenService tokenService, | ||||
|   IServiceProvider serviceProvider, | ||||
|   IElectoralApiService apiService) | ||||
|   IElectoralApiService apiService, | ||||
|   WorkerConfigService configService) | ||||
|   { | ||||
|     _logger = logger; | ||||
|     _tokenService = tokenService; | ||||
|     _serviceProvider = serviceProvider; | ||||
|     _apiService = apiService; | ||||
|     _configService = configService; | ||||
|   } | ||||
|  | ||||
|   protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
| @@ -46,30 +50,25 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       // --- LÓGICA DE EJECUCIÓN INDEPENDIENTE --- | ||||
|       var settings = await _configService.GetSettingsAsync(); | ||||
|  | ||||
|       // 1. TAREA DE BANCAS: Siempre se ejecuta y se espera. Es rápida. | ||||
|       _logger.LogInformation("Iniciando sondeo de Bancas..."); | ||||
|       await SondearProyeccionBancasAsync(authToken, stoppingToken); | ||||
|       _logger.LogInformation("Sondeo de Bancas completado."); | ||||
|  | ||||
|       // 2. TAREA DE TELEGRAMAS: "Dispara y Olvida" de forma segura. | ||||
|       // Comprobamos si la tarea anterior de telegramas ya ha terminado. | ||||
|       if (_telegramasTask == null || _telegramasTask.IsCompleted) | ||||
|       if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Iniciando sondeo de Telegramas en segundo plano..."); | ||||
|         // Lanzamos la tarea de telegramas pero NO la esperamos con 'await'. | ||||
|         // Guardamos una referencia a la tarea en nuestra variable de estado. | ||||
|         _telegramasTask = SondearNuevosTelegramasAsync(authToken, stoppingToken); | ||||
|         _logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad."); | ||||
|         await SondearResultadosMunicipalesAsync(authToken, stoppingToken); | ||||
|         await SondearResumenProvincialAsync(authToken, stoppingToken); | ||||
|         await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else if (settings.Prioridad == "Resultados" && settings.BajasActivado) | ||||
|       { | ||||
|         _logger.LogInformation("Ejecutando tareas de Baja Prioridad en baja prioridad."); | ||||
|         await SondearProyeccionBancasAsync(authToken, stoppingToken); | ||||
|         await SondearNuevosTelegramasAsync(authToken, stoppingToken); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         // Si la descarga anterior todavía está en curso, nos saltamos este sondeo | ||||
|         // para no acumular tareas y sobrecargar el sistema. | ||||
|         _logger.LogInformation("El sondeo de telegramas anterior sigue en ejecución. Se omitirá en este ciclo."); | ||||
|         _logger.LogInformation("Worker de baja prioridad inactivo según la configuración."); | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("--- Ciclo de Datos de Baja Prioridad completado. Esperando 5 minutos. ---"); | ||||
|       try | ||||
|       { | ||||
|         await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); | ||||
| @@ -81,6 +80,337 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       var municipiosASondear = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null) | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       var todasLasCategorias = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!municipiosASondear.Any() || !todasLasCategorias.Any()) | ||||
|       { | ||||
|         _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count); | ||||
|  | ||||
|       foreach (var municipio in municipiosASondear) | ||||
|       { | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|  | ||||
|         var tareasCategoria = todasLasCategorias.Select(async categoria => | ||||
|         { | ||||
|           var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id); | ||||
|  | ||||
|           if (resultados != null) | ||||
|           { | ||||
|             using var innerScope = _serviceProvider.CreateScope(); | ||||
|             var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|             // --- LLAMADA CORRECTA --- | ||||
|             await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken); | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         await Task.WhenAll(tareasCategoria); | ||||
|       } | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async Task GuardarResultadosDeAmbitoAsync( | ||||
|       EleccionesDbContext dbContext, int ambitoId, int categoriaId, | ||||
|       Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken) | ||||
|   { | ||||
|     var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken); | ||||
|  | ||||
|     if (estadoRecuento == null) | ||||
|     { | ||||
|       estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId }; | ||||
|       dbContext.EstadosRecuentos.Add(estadoRecuento); | ||||
|     } | ||||
|  | ||||
|     estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime(); | ||||
|     estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas; | ||||
|     estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas; | ||||
|     estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje; | ||||
|     estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores; | ||||
|     estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes; | ||||
|     estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje; | ||||
|  | ||||
|     if (resultadosDto.ValoresTotalizadosOtros != null) | ||||
|     { | ||||
|       estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco; | ||||
|       estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje; | ||||
|       estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos; | ||||
|       estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje; | ||||
|       estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos; | ||||
|       estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje; | ||||
|     } | ||||
|  | ||||
|     foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos) | ||||
|     { | ||||
|       var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync( | ||||
|           rv => rv.AmbitoGeograficoId == ambitoId && | ||||
|                 rv.CategoriaId == categoriaId && | ||||
|                 rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion, | ||||
|           stoppingToken | ||||
|       ); | ||||
|  | ||||
|       if (resultadoVoto == null) | ||||
|       { | ||||
|         resultadoVoto = new ResultadoVoto | ||||
|         { | ||||
|           AmbitoGeograficoId = ambitoId, | ||||
|           CategoriaId = categoriaId, | ||||
|           AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion | ||||
|         }; | ||||
|         dbContext.ResultadosVotos.Add(resultadoVoto); | ||||
|       } | ||||
|       resultadoVoto.CantidadVotos = votoPositivoDto.Votos; | ||||
|       resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje; | ||||
|     } | ||||
|  | ||||
|     try | ||||
|     { | ||||
|       await dbContext.SaveChangesAsync(stoppingToken); | ||||
|     } | ||||
|     catch (DbUpdateException ex) | ||||
|     { | ||||
|       _logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial. | ||||
|   /// Esta versión actualizada guarda tanto los votos por agrupación (en ResumenesVotos) | ||||
|   /// como el estado general del recuento, incluyendo la fecha de totalización (en EstadosRecuentosGenerales), | ||||
|   /// asegurando que toda la operación sea atómica mediante una transacción de base de datos. | ||||
|   /// </summary> | ||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||
|   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||
|   private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       // Creamos un scope de DbContext para esta operación. | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       // Obtenemos el registro de la Provincia (NivelId 10). | ||||
|       var provincia = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() | ||||
|           .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | ||||
|  | ||||
|       // Si no encontramos el ámbito de la provincia, no podemos continuar. | ||||
|       if (provincia == null) | ||||
|       { | ||||
|         _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) para el sondeo de resumen."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Llamamos a la API para obtener el resumen de datos provincial. | ||||
|       var resumenDto = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!); | ||||
|  | ||||
|       // Solo procedemos si la API devolvió una respuesta válida y no nula. | ||||
|       if (resumenDto != null) | ||||
|       { | ||||
|         // Iniciamos una transacción explícita. Esto garantiza que todas las operaciones de base de datos | ||||
|         // dentro de este bloque (el DELETE, los INSERTs y los UPDATEs) se completen con éxito, | ||||
|         // o si algo falla, se reviertan todas, manteniendo la consistencia de los datos. | ||||
|         await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken); | ||||
|  | ||||
|         // --- 1. ACTUALIZAR LA TABLA 'ResumenesVotos' --- | ||||
|  | ||||
|         // Verificamos si la respuesta contiene una lista de votos positivos. | ||||
|         if (resumenDto.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos) | ||||
|         { | ||||
|           // Estrategia "Borrar y Reemplazar": vaciamos la tabla antes de insertar los nuevos datos. | ||||
|           await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken); | ||||
|  | ||||
|           // Añadimos cada nuevo registro de voto al DbContext. | ||||
|           foreach (var voto in nuevosVotos) | ||||
|           { | ||||
|             dbContext.ResumenesVotos.Add(new ResumenVoto | ||||
|             { | ||||
|               AmbitoGeograficoId = provincia.Id, | ||||
|               AgrupacionPoliticaId = voto.IdAgrupacion, | ||||
|               Votos = voto.Votos, | ||||
|               VotosPorcentaje = voto.VotosPorcentaje | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // --- 2. ACTUALIZAR LA TABLA 'EstadosRecuentosGenerales' --- | ||||
|  | ||||
|         // El endpoint de Resumen no especifica una categoría, por lo que aplicamos sus datos de estado de recuento | ||||
|         // a todas las categorías que tenemos en nuestra base de datos. | ||||
|         var todasLasCategorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken); | ||||
|         foreach (var categoria in todasLasCategorias) | ||||
|         { | ||||
|           // Buscamos el registro existente usando la clave primaria compuesta. | ||||
|           var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken); | ||||
|  | ||||
|           // Si no existe, lo creamos. | ||||
|           if (registroDb == null) | ||||
|           { | ||||
|             registroDb = new EstadoRecuentoGeneral { AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id }; | ||||
|             dbContext.EstadosRecuentosGenerales.Add(registroDb); | ||||
|           } | ||||
|  | ||||
|           // Parseamos la fecha de forma segura para evitar errores con cadenas vacías o nulas. | ||||
|           if (DateTime.TryParse(resumenDto.FechaTotalizacion, out var parsedDate)) | ||||
|           { | ||||
|             registroDb.FechaTotalizacion = parsedDate.ToUniversalTime(); | ||||
|           } | ||||
|  | ||||
|           // Mapeamos el resto de los datos del estado del recuento. | ||||
|           registroDb.MesasEsperadas = resumenDto.EstadoRecuento.MesasEsperadas; | ||||
|           registroDb.MesasTotalizadas = resumenDto.EstadoRecuento.MesasTotalizadas; | ||||
|           registroDb.MesasTotalizadasPorcentaje = resumenDto.EstadoRecuento.MesasTotalizadasPorcentaje; | ||||
|           registroDb.CantidadElectores = resumenDto.EstadoRecuento.CantidadElectores; | ||||
|           registroDb.CantidadVotantes = resumenDto.EstadoRecuento.CantidadVotantes; | ||||
|           registroDb.ParticipacionPorcentaje = resumenDto.EstadoRecuento.ParticipacionPorcentaje; | ||||
|         } | ||||
|  | ||||
|         // 3. CONFIRMAR Y GUARDAR | ||||
|         // Guardamos todos los cambios preparados (DELETEs, INSERTs, UPDATEs) en la base de datos. | ||||
|         await dbContext.SaveChangesAsync(stoppingToken); | ||||
|         // Confirmamos la transacción para hacer los cambios permanentes. | ||||
|         await transaction.CommitAsync(stoppingToken); | ||||
|  | ||||
|         _logger.LogInformation("Sondeo de Resumen Provincial completado. Las tablas han sido actualizadas."); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         // Si la API no devolvió datos (ej. devuelve null), no hacemos nada en la BD. | ||||
|         _logger.LogInformation("Sondeo de Resumen Provincial completado. No se recibieron datos nuevos."); | ||||
|       } | ||||
|     } | ||||
|     catch (OperationCanceledException) | ||||
|     { | ||||
|       _logger.LogInformation("Sondeo de resumen provincial cancelado."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       // Capturamos cualquier otro error inesperado para que el worker no se detenga. | ||||
|       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Resumen Provincial."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral. | ||||
|   /// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta | ||||
|   /// de la base de datos y guarda todos los cambios en una única transacción al final. | ||||
|   /// </summary> | ||||
|   /// <param name="authToken">El token de autenticación válido para la sesión.</param> | ||||
|   /// <param name="stoppingToken">El token de cancelación para detener la operación.</param> | ||||
|   private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken) | ||||
|   { | ||||
|     try | ||||
|     { | ||||
|       // PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext. | ||||
|       // Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia. | ||||
|       using var scope = _serviceProvider.CreateScope(); | ||||
|       var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>(); | ||||
|  | ||||
|       // PASO 2: Obtener el ámbito geográfico de la Provincia. | ||||
|       // Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API. | ||||
|       var provincia = await dbContext.AmbitosGeograficos | ||||
|           .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos. | ||||
|           .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken); | ||||
|  | ||||
|       // Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia, | ||||
|       // no podemos continuar. Registramos una advertencia y salimos del método. | ||||
|       if (provincia == null) | ||||
|       { | ||||
|         _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos. | ||||
|       // Esto hace que el método sea dinámico y no dependa de IDs fijos en el código. | ||||
|       var categoriasParaSondear = await dbContext.CategoriasElectorales | ||||
|           .AsNoTracking() | ||||
|           .ToListAsync(stoppingToken); | ||||
|  | ||||
|       if (!categoriasParaSondear.Any()) | ||||
|       { | ||||
|         _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento."); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count); | ||||
|  | ||||
|       // PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual. | ||||
|       foreach (var categoria in categoriasParaSondear) | ||||
|       { | ||||
|         // Salimos limpiamente del bucle si la aplicación se está deteniendo. | ||||
|         if (stoppingToken.IsCancellationRequested) break; | ||||
|  | ||||
|         // Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle. | ||||
|         var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id); | ||||
|  | ||||
|         // Solo procedemos si la API devolvió datos válidos. | ||||
|         if (estadoDto != null) | ||||
|         { | ||||
|           // Lógica "Upsert" (Update or Insert): | ||||
|           // Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA. | ||||
|           var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync( | ||||
|               new object[] { provincia.Id, categoria.Id }, | ||||
|               cancellationToken: stoppingToken | ||||
|           ); | ||||
|  | ||||
|           // Si no se encuentra (FindAsync devuelve null), es un registro nuevo. | ||||
|           if (registroDb == null) | ||||
|           { | ||||
|             // Creamos una nueva instancia de la entidad. | ||||
|             registroDb = new EstadoRecuentoGeneral | ||||
|             { | ||||
|               AmbitoGeograficoId = provincia.Id, | ||||
|               CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria. | ||||
|             }; | ||||
|             // Y la añadimos al ChangeTracker de EF para que la inserte en la BD. | ||||
|             dbContext.EstadosRecuentosGenerales.Add(registroDb); | ||||
|           } | ||||
|  | ||||
|           // Mapeamos los datos del DTO de la API a nuestra entidad de base de datos. | ||||
|           // Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar. | ||||
|           registroDb.MesasEsperadas = estadoDto.MesasEsperadas; | ||||
|           registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas; | ||||
|           registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje; | ||||
|           registroDb.CantidadElectores = estadoDto.CantidadElectores; | ||||
|           registroDb.CantidadVotantes = estadoDto.CantidadVotantes; | ||||
|           registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // PASO 5: Guardar todos los cambios en la base de datos. | ||||
|       // Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa | ||||
|       // todas las inserciones y actualizaciones en una única transacción eficiente. | ||||
|       await dbContext.SaveChangesAsync(stoppingToken); | ||||
|       _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías."); | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|       // Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos. | ||||
|       _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General."); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// <summary> | ||||
|   /// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones) | ||||
|   /// desde la API a la base de datos local. Se ejecuta una sola vez al iniciar el worker. | ||||
| @@ -286,7 +616,7 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|         { | ||||
|           hasReceivedAnyNewData = true; | ||||
|  | ||||
|           // --- CORRECCIÓN DE SEGURIDAD: Usar TryParse para la fecha --- | ||||
|           // --- SEGURIDAD: Usar TryParse para la fecha --- | ||||
|           DateTime fechaTotalizacion; | ||||
|           if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|           { | ||||
| @@ -326,7 +656,7 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|           { | ||||
|             hasReceivedAnyNewData = true; | ||||
|  | ||||
|             // --- APLICAMOS LA MISMA CORRECCIÓN DE SEGURIDAD AQUÍ --- | ||||
|             // --- APLICAMOS SEGURIDAD AQUÍ --- | ||||
|             DateTime fechaTotalizacion; | ||||
|             if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate)) | ||||
|             { | ||||
| @@ -439,7 +769,6 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|                 var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId); | ||||
|                 if (telegramaFile != null) | ||||
|                 { | ||||
|                   // --- INICIO DE LA CORRECCIÓN --- | ||||
|                   // 1. Buscamos el AmbitoGeografico específico de la MESA que estamos procesando. | ||||
|                   var ambitoMesa = await innerDbContext.AmbitosGeograficos | ||||
|                       .AsNoTracking() | ||||
| @@ -463,7 +792,6 @@ public class LowPriorityDataWorker : BackgroundService | ||||
|                   { | ||||
|                     _logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId); | ||||
|                   } | ||||
|                   // --- FIN DE LA CORRECCIÓN --- | ||||
|                 } | ||||
|                 await Task.Delay(250, stoppingToken); | ||||
|               } | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| //Elecciones.Worker/Program.cs | ||||
| using Elecciones.Database; | ||||
| using Elecciones.Infrastructure.Services; | ||||
| using Elecciones.Worker; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using System.Net; | ||||
| using Serilog; | ||||
| using System.Net.Http; | ||||
| using System.Net.Security; | ||||
| using System.Security.Authentication; | ||||
| using Polly; | ||||
| @@ -21,12 +19,20 @@ Log.Information("Iniciando Elecciones.Worker Host..."); | ||||
|  | ||||
| var builder = Host.CreateApplicationBuilder(args); | ||||
|  | ||||
| builder.Services.AddSerilog(config =>  | ||||
|     config | ||||
| // 1. Registra el servicio del interruptor como siempre. | ||||
| builder.Services.AddSingleton<LoggingSwitchService>(); | ||||
|  | ||||
| // 2. Configura Serilog usando AddSerilog. | ||||
| builder.Services.AddSerilog((services, configuration) => { | ||||
|     var loggingSwitch = services.GetRequiredService<LoggingSwitchService>(); | ||||
|     configuration | ||||
|         .ReadFrom.Configuration(builder.Configuration) | ||||
|         .ReadFrom.Services(services) | ||||
|         .Enrich.FromLogContext() | ||||
|         .MinimumLevel.ControlledBy(loggingSwitch.LevelSwitch) | ||||
|         .WriteTo.Console() | ||||
|         .WriteTo.File("logs/worker-.log", rollingInterval: RollingInterval.Day)); | ||||
|         .WriteTo.File("logs/worker-.log", rollingInterval: RollingInterval.Day); | ||||
| }); | ||||
|  | ||||
| // --- Configuración de Servicios --- | ||||
| var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); | ||||
| @@ -93,16 +99,47 @@ builder.Services.AddSingleton<RateLimiter>(sp => | ||||
|         QueueProcessingOrder = QueueProcessingOrder.OldestFirst | ||||
|     })); | ||||
|      | ||||
| builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>();  | ||||
| builder.Services.AddScoped<IElectoralApiService, ElectoralApiService>(); | ||||
|  | ||||
| // Registramos el servicio de token como un Singleton para que sea compartido. | ||||
| builder.Services.AddSingleton<SharedTokenService>(); | ||||
| // Registramos el servicio de configuraciones de workers como un Singleton para que sea compartido. | ||||
| builder.Services.AddSingleton<WorkerConfigService>(); | ||||
|  | ||||
| // Registramos ambos workers. El framework se encargará de iniciarlos y detenerlos. | ||||
| builder.Services.AddHostedService<CriticalDataWorker>(); | ||||
| builder.Services.AddHostedService<LowPriorityDataWorker>(); | ||||
| //builder.Services.AddHostedService<Worker>(); | ||||
|  | ||||
| // --- LÓGICA PARA LEER EL NIVEL DE LOGGING AL INICIO --- | ||||
| // Creamos un scope temporal para leer la configuración de la BD | ||||
| using (var scope = builder.Services.BuildServiceProvider().CreateScope()) | ||||
| { | ||||
|     var services = scope.ServiceProvider; | ||||
|     try | ||||
|     { | ||||
|         var dbContext = services.GetRequiredService<EleccionesDbContext>(); | ||||
|         var loggingSwitchService = services.GetRequiredService<LoggingSwitchService>(); | ||||
|          | ||||
|         // Buscamos el nivel de logging guardado en la BD | ||||
|         var logLevelConfig = await dbContext.Configuraciones | ||||
|             .AsNoTracking() | ||||
|             .FirstOrDefaultAsync(c => c.Clave == "Logging_Level"); | ||||
|  | ||||
|         if (logLevelConfig != null) | ||||
|         { | ||||
|             // Si lo encontramos, lo aplicamos al interruptor | ||||
|             loggingSwitchService.SetLoggingLevel(logLevelConfig.Valor); | ||||
|             Console.WriteLine($"--> Nivel de logging inicial establecido desde la BD a: {logLevelConfig.Valor}"); | ||||
|         } | ||||
|     } | ||||
|     catch (Exception ex) | ||||
|     { | ||||
|         // Si hay un error (ej. la BD no está disponible al arrancar), se usará el nivel por defecto 'Information'. | ||||
|         Console.WriteLine($"--> No se pudo establecer el nivel de logging desde la BD: {ex.Message}"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| var host = builder.Build(); | ||||
|  | ||||
| try | ||||
|   | ||||
| @@ -14,7 +14,7 @@ using System.Reflection; | ||||
| [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Worker")] | ||||
| [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] | ||||
| [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+55954e18a797dce22f76f00b645832f361d97362")] | ||||
| [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f384a640f36be1289d652dc85e78ebdcef30968a")] | ||||
| [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Worker")] | ||||
| [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Worker")] | ||||
| [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] | ||||
|   | ||||
| @@ -180,6 +180,9 @@ | ||||
|             "projectReferences": { | ||||
|               "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Core\\Elecciones.Core.csproj": { | ||||
|                 "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Core\\Elecciones.Core.csproj" | ||||
|               }, | ||||
|               "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj": { | ||||
|                 "projectPath": "E:\\Elecciones-2025\\Elecciones-Web\\src\\Elecciones.Database\\Elecciones.Database.csproj" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
| @@ -208,6 +211,10 @@ | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|             }, | ||||
|             "Serilog": { | ||||
|               "target": "Package", | ||||
|               "version": "[4.3.0, )" | ||||
|             }, | ||||
|             "System.Threading.RateLimiting": { | ||||
|               "target": "Package", | ||||
|               "version": "[9.0.8, )" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user