Feat Workers Prioridades y Nivel Serilog

This commit is contained in:
2025-09-06 21:44:52 -03:00
parent f384a640f3
commit fa92d9638c
29 changed files with 2068 additions and 95 deletions

View File

@@ -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

View File

@@ -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>
);

View 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>
);
};

View File

@@ -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);
};

View File

@@ -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}'." });
}
}

View File

@@ -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.

View File

@@ -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",

View File

@@ -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")]

View File

@@ -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":{}}

View File

@@ -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":{}}

View File

@@ -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, )"

View File

@@ -0,0 +1,7 @@
using System.ComponentModel.DataAnnotations;
public class UpdateLoggingLevelRequest
{
[Required]
public string Level { get; set; } = null!;
}

View File

@@ -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")]

View File

@@ -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")]

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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": ""
}
}
}

View File

@@ -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")]

View File

@@ -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

View File

@@ -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, )"

View File

@@ -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>

View File

@@ -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')" />

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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")]

View File

@@ -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, )"

View File

@@ -14,17 +14,17 @@ services:
- shared-net
# Servicio del Worker (sin cambios)
# elecciones-worker:
# build:
# context: ./Elecciones-Web
# dockerfile: src/Elecciones.Worker/Dockerfile
# container_name: elecciones-worker
# restart: unless-stopped
# env_file: ./.env
# networks:
# - shared-net
# volumes:
# - ./logs-worker:/app/logs
elecciones-worker:
build:
context: ./Elecciones-Web
dockerfile: src/Elecciones.Worker/Dockerfile
container_name: elecciones-worker
restart: unless-stopped
env_file: ./.env
networks:
- shared-net
volumes:
- ./logs-worker:/app/logs
# Servicio del Frontend Público (sin cambios)
elecciones-frontend: