diff --git a/src/Backend/WhatsappPromo.Core/Services/ConfigService.cs b/src/Backend/WhatsappPromo.Core/Services/ConfigService.cs index 9ce8e54..5a90980 100644 --- a/src/Backend/WhatsappPromo.Core/Services/ConfigService.cs +++ b/src/Backend/WhatsappPromo.Core/Services/ConfigService.cs @@ -17,7 +17,7 @@ namespace WhatsappPromo.Core.Services { private readonly string _configPath; private readonly ILogger _logger; - private SystemConfig _cachedConfig; + private SystemConfig? _cachedConfig; public ConfigService(ILogger logger) { diff --git a/src/Backend/WhatsappPromo.Core/WhatsappPromo.Core.csproj b/src/Backend/WhatsappPromo.Core/WhatsappPromo.Core.csproj index c17ee36..8cc85db 100644 --- a/src/Backend/WhatsappPromo.Core/WhatsappPromo.Core.csproj +++ b/src/Backend/WhatsappPromo.Core/WhatsappPromo.Core.csproj @@ -7,8 +7,7 @@ - - + diff --git a/src/Backend/WhatsappPromo.Engine/Services/BrowserService.cs b/src/Backend/WhatsappPromo.Engine/Services/BrowserService.cs index fdcebf1..b41a79b 100644 --- a/src/Backend/WhatsappPromo.Engine/Services/BrowserService.cs +++ b/src/Backend/WhatsappPromo.Engine/Services/BrowserService.cs @@ -13,7 +13,7 @@ namespace WhatsappPromo.Engine.Services Task InitializeAsync(); Task GetPageAsync(); Task CloseAsync(); - Task GetQrCodeAsync(); + Task GetQrCodeAsync(); Task PerformHumanIdleActionsAsync(); } @@ -32,185 +32,103 @@ namespace WhatsappPromo.Engine.Services public async Task InitializeAsync() { + if (_browser != null && !_browser.IsClosed) return; + _logger.LogInformation("Descargando navegador si es necesario..."); var browserFetcher = new BrowserFetcher(); await browserFetcher.DownloadAsync(); var options = new LaunchOptions { - Headless = false, // Headless mode aumenta riesgo de detección + Headless = false, UserDataDir = _userDataDir, Args = new[] { "--no-sandbox", "--disable-setuid-sandbox", "--disable-infobars", + "--start-minimized", "--window-position=0,0", "--ignore-certificate-errors", "--ignore-certificate-errors-spki-list", - - // ANTI-DETECCIÓN CRÍTICA "--disable-blink-features=AutomationControlled", - - // Evitar detección de DevTools "--disable-dev-shm-usage", - - // Hacer que el navegador parezca más "normal" "--disable-extensions-except", "--disable-extensions", - - // GPU y WebGL realistas (importante para huellas digitales) "--enable-webgl", "--use-gl=swiftshader", + "--disable-features=IsolateOrigins,site-per-process,CalculateNativeWinOcclusion", + "--disable-default-apps", - // Deshabilitar características que revelan automatización - "--disable-features=IsolateOrigins,site-per-process", - - // Evitar leaks de automatización - "--disable-default-apps" + // Flags para evitar que el navegador se "duerma" al estar minimizado + "--disable-backgrounding-occluded-windows", + "--disable-background-timer-throttling", + "--disable-renderer-backgrounding" }, - IgnoredDefaultArgs = new[] { "--enable-automation" }, // Quita "Chrome is being controlled" - DefaultViewport = null // Permite que el viewport sea controlado por window size + IgnoredDefaultArgs = new[] { "--enable-automation" }, + DefaultViewport = null }; _logger.LogInformation("Iniciando navegador Chrome..."); _browser = await Puppeteer.LaunchAsync(options); - var pages = await _browser.PagesAsync(); - _page = pages.Length > 0 ? pages[0] : await _browser.NewPageAsync(); + // Forzar minimizado mediante el proceso si es posible + try + { + var pages = await _browser.PagesAsync(); + _page = pages.Length > 0 ? pages[0] : await _browser.NewPageAsync(); + } + catch (Exception ex) + { + _logger.LogWarning("Error inicializando página, reintentando... {Msg}", ex.Message); + await Task.Delay(1000); + var pages = await _browser.PagesAsync(); + _page = pages.Length > 0 ? pages[0] : await _browser.NewPageAsync(); + } + + if (_page == null) throw new Exception("No se pudo crear la página del navegador."); - // User-Agent más actualizado y realista (Chrome 131) await _page.SetUserAgentAsync("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"); - // Viewport aleatorio pero realista - var random = new Random(); await _page.SetViewportAsync(new ViewPortOptions { - Width = 1366 + random.Next(-50, 150), // 1316-1516 - Height = 768 + random.Next(-50, 150), // 718-918 + Width = 1366 + Random.Shared.Next(-50, 150), + Height = 768 + Random.Shared.Next(-50, 150), DeviceScaleFactor = 1 }); - // INYECCIÓN MASIVA DE ANTI-DETECCIÓN + // Anti-detección await _page.EvaluateExpressionOnNewDocumentAsync(@" - // 1. Ocultar navigator.webdriver - Object.defineProperty(navigator, 'webdriver', { - get: () => undefined, - }); - - // 2. Sobrescribir chrome property (PuppeteerSharp deja rastros) - window.chrome = { - runtime: {}, - loadTimes: function() {}, - csi: function() {}, - app: {} - }; - - // 3. Modificar permissions API para evitar detección + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} }; const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); - - // 4. Sobrescribir plugins y mimeTypes (navegadores automatizados tienen 0) Object.defineProperty(navigator, 'plugins', { get: () => [ - { - 0: {type: 'application/x-google-chrome-pdf', suffixes: 'pdf'}, - description: 'Portable Document Format', - filename: 'internal-pdf-viewer', - length: 1, - name: 'Chrome PDF Plugin' - }, - { - 0: {type: 'application/pdf', suffixes: 'pdf'}, - description: 'Portable Document Format', - filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', - length: 1, - name: 'Chrome PDF Viewer' - } + {0: {type: 'application/x-google-chrome-pdf', suffixes: 'pdf'}, description: 'Portable Document Format', filename: 'internal-pdf-viewer', length: 1, name: 'Chrome PDF Plugin'}, + {0: {type: 'application/pdf', suffixes: 'pdf'}, description: 'Portable Document Format', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', length: 1, name: 'Chrome PDF Viewer'} ], }); - - Object.defineProperty(navigator, 'mimeTypes', { - get: () => [ - {type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format'}, - {type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: ''} - ], - }); - - // 5. languages debe ser array (no solo string) - Object.defineProperty(navigator, 'languages', { - get: () => ['es-ES', 'es', 'en-US', 'en'], - }); - - // 6. WebGL vendor info (importante para fingerprinting) + Object.defineProperty(navigator, 'languages', { get: () => ['es-ES', 'es', 'en-US', 'en'] }); const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { - if (parameter === 37445) { - return 'Intel Inc.'; // UNMASKED_VENDOR_WEBGL - } - if (parameter === 37446) { - return 'Intel(R) UHD Graphics'; // UNMASKED_RENDERER_WEBGL - } + if (parameter === 37445) return 'Intel Inc.'; + if (parameter === 37446) return 'Intel(R) UHD Graphics'; return getParameter.apply(this, [parameter]); }; - - // 7. Ocultar características de headless Chrome - Object.defineProperty(navigator, 'hardwareConcurrency', { - get: () => 4 + Math.floor(Math.random() * 5) // 4-8 cores (realista) - }); - - Object.defineProperty(navigator, 'deviceMemory', { - get: () => 8 // 8GB RAM - }); - - // 8. Sobrescribir Notification.permission - Object.defineProperty(Notification, 'permission', { - get: () => 'default' - }); - - // 9. Battery API (navegadores automatizados no tienen) + Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 + Math.floor(Math.random() * 5) }); + Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 }); Object.defineProperty(navigator, 'getBattery', { - get: () => () => Promise.resolve({ - charging: true, - chargingTime: 0, - dischargingTime: Infinity, - level: 0.97 - }) + get: () => () => Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 0.97 }) }); - - // 10. Connection API Object.defineProperty(navigator, 'connection', { - get: () => ({ - effectiveType: '4g', - downlink: 10, - rtt: 50, - saveData: false - }) + get: () => ({ effectiveType: '4g', downlink: 10, rtt: 50, saveData: false }) }); - - // 11. Evitar detección por timing attacks - const originalDate = Date; - Date = class extends originalDate { - constructor(...args) { - if (args.length === 0) { - super(); - // Añadir jitter aleatorio de 0-5ms - const jitter = Math.floor(Math.random() * 5); - return new originalDate(super.getTime() + jitter); - } - return new originalDate(...args); - } - }; - - // 12. Ocultar '__playwright' y '__puppeteer' si existen - delete window.__playwright; - delete window.__puppeteer; - - console.log('✅ Anti-detección ejecutada exitosamente'); + console.log('✅ Anti-detección ejecutada'); "); _logger.LogInformation("Navegando a WhatsApp Web..."); @@ -218,70 +136,75 @@ namespace WhatsappPromo.Engine.Services { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } }); - - _logger.LogInformation("WhatsApp Web cargado."); } public async Task GetPageAsync() { if (_page == null) await InitializeAsync(); - return (Page)_page; + return (_page as Page) ?? throw new Exception("Error al obtener la página de Puppeteer"); } public async Task CloseAsync() { - if (_browser != null) await _browser.CloseAsync(); + try + { + if (_browser != null && !_browser.IsClosed) + { + _logger.LogInformation("Cerrando navegador..."); + await _browser.CloseAsync(); + } + } + catch (Exception ex) + { + _logger.LogWarning("Error al cerrar el navegador: {Message}", ex.Message); + } + finally + { + _browser = null; + _page = null; + } } public async Task PerformHumanIdleActionsAsync() { - if (_page == null) return; + if (_page == null || _page.IsClosed) return; try { - var rnd = new Random(); - var action = rnd.Next(1, 4); - + var action = Random.Shared.Next(1, 4); switch (action) { - case 1: // Random Mouse Move con curva Bezier (más humano) - var x = rnd.Next(100, 1000); - var y = rnd.Next(100, 600); - // Steps más alto = movimiento más suave y humano - await _page.Mouse.MoveAsync(x, y, new MoveOptions { Steps = rnd.Next(20, 60) }); + case 1: + var x = Random.Shared.Next(100, 1000); + var y = Random.Shared.Next(100, 600); + await _page.Mouse.MoveAsync(x, y, new MoveOptions { Steps = Random.Shared.Next(20, 60) }); break; - case 2: // Small Scroll con variación - var delta = rnd.Next(-150, 150); + case 2: + var delta = Random.Shared.Next(-150, 150); await _page.Mouse.WheelAsync(0, delta); break; - case 3: // Pause con duración variable (simula lectura) - await Task.Delay(rnd.Next(1500, 4000)); + case 3: + await Task.Delay(Random.Shared.Next(1500, 4000)); break; } } - catch { /* Ignore errors during idle actions */ } + catch { } } - public async Task GetQrCodeAsync() + public async Task GetQrCodeAsync() { - if (_page == null) return null; + if (_page == null || _page.IsClosed) return null; try { - // Selector del canvas del QR var qrSelector = "canvas"; - await _page.WaitForSelectorAsync(qrSelector, new WaitForSelectorOptions { Timeout = 5000 }); var element = await _page.QuerySelectorAsync(qrSelector); if (element != null) { - // Devolver como base64 para el frontend return await element.ScreenshotBase64Async(); } } - catch - { - // No se encontró código QR (posiblemente ya logueado) - } + catch {} return null; } } diff --git a/src/Backend/WhatsappPromo.Worker/Controllers/ConfigController.cs b/src/Backend/WhatsappPromo.Worker/Controllers/ConfigController.cs index ecf175c..66dfdf3 100644 --- a/src/Backend/WhatsappPromo.Worker/Controllers/ConfigController.cs +++ b/src/Backend/WhatsappPromo.Worker/Controllers/ConfigController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; +using System.Windows.Forms; using WhatsappPromo.Core.Models; using WhatsappPromo.Core.Services; @@ -28,7 +30,6 @@ namespace WhatsappPromo.Worker.Controllers [HttpPost] public async Task UpdateConfig([FromBody] SystemConfig config) { - // Validar ruta if (!string.IsNullOrEmpty(config.DownloadPath)) { try @@ -62,5 +63,64 @@ namespace WhatsappPromo.Worker.Controllers await _configService.UpdateConfigAsync(config); return Ok(new { status = "Stopping" }); } + + [System.Runtime.InteropServices.DllImport("user32.dll")] + [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [System.Runtime.InteropServices.DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + private const uint SWP_NOSIZE = 0x0001; + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_SHOWWINDOW = 0x0040; + + [HttpGet("browse")] + public IActionResult Browse() + { + string? selectedPath = null; + + var thread = new Thread(() => + { + using (var dummyForm = new Form()) + { + dummyForm.TopMost = true; + // El form debe ser "real" para que Windows lo respete + dummyForm.Width = 1; dummyForm.Height = 1; + dummyForm.Opacity = 0.05; + dummyForm.ShowInTaskbar = false; + dummyForm.FormBorderStyle = FormBorderStyle.None; + dummyForm.StartPosition = FormStartPosition.CenterScreen; + + dummyForm.Show(); + + // Forzamos la posición al frente de todo + SetWindowPos(dummyForm.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SetForegroundWindow(dummyForm.Handle); + dummyForm.Activate(); + dummyForm.Focus(); + + using (var fbd = new FolderBrowserDialog()) + { + fbd.Description = "Selecciona la carpeta para descargas de WhatsApp"; + fbd.UseDescriptionForTitle = true; + fbd.ShowNewFolderButton = true; + + if (fbd.ShowDialog(dummyForm) == DialogResult.OK) + { + selectedPath = fbd.SelectedPath; + } + } + dummyForm.Close(); + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + + return Ok(new { path = selectedPath }); + } } } diff --git a/src/Backend/WhatsappPromo.Worker/Program.cs b/src/Backend/WhatsappPromo.Worker/Program.cs index 5509689..fe139f7 100644 --- a/src/Backend/WhatsappPromo.Worker/Program.cs +++ b/src/Backend/WhatsappPromo.Worker/Program.cs @@ -39,7 +39,10 @@ var app = builder.Build(); app.UseCors("AllowDashboard"); -app.UseHttpsRedirection(); +if (!app.Environment.IsDevelopment()) +{ + app.UseHttpsRedirection(); +} app.UseAuthorization(); app.MapControllers(); app.MapHub("/whatsappHub"); // SignalR Endpoint for Frontend diff --git a/src/Backend/WhatsappPromo.Worker/WhatsappPromo.Worker.csproj b/src/Backend/WhatsappPromo.Worker/WhatsappPromo.Worker.csproj index fd26e6c..e34e16f 100644 --- a/src/Backend/WhatsappPromo.Worker/WhatsappPromo.Worker.csproj +++ b/src/Backend/WhatsappPromo.Worker/WhatsappPromo.Worker.csproj @@ -1,9 +1,11 @@ - net10.0 + net10.0-windows enable enable + Exe + true diff --git a/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs b/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs index 3b37f85..964f233 100644 --- a/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs +++ b/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs @@ -18,7 +18,7 @@ namespace WhatsappPromo.Worker { private readonly ILogger _logger; private readonly IBrowserService _browserService; - private readonly IConfigService _configService; // Check this dependency + private readonly IConfigService _configService; private readonly Channel _mediaChannel; private readonly IHubContext _hubContext; private readonly SemaphoreSlim _lock = new(1, 1); @@ -42,29 +42,44 @@ namespace WhatsappPromo.Worker while (!stoppingToken.IsCancellationRequested) { - var config = await _configService.GetConfigAsync(); - - if (config.IsActive && !_isAutomationRunning) + try { - _ = StartAutomationAsync(stoppingToken); + var config = await _configService.GetConfigAsync(); + + if (config.IsActive && !_isAutomationRunning) + { + _ = StartAutomationAsync(stoppingToken); + } + + await Task.Delay(2000, stoppingToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en el bucle principal del Worker"); } - - await Task.Delay(2000, stoppingToken); } + + _logger.LogInformation("Cerrando servicio worker..."); + await _browserService.CloseAsync(); } - private async Task StartAutomationAsync(CancellationToken token) + private async Task StartAutomationAsync(CancellationToken stoppingToken) { if (_isAutomationRunning) return; try { - await _lock.WaitAsync(token); + await _lock.WaitAsync(stoppingToken); if (_isAutomationRunning) return; _isAutomationRunning = true; _logger.LogInformation("Iniciando AUTOMATIZACIÓN..."); - await _hubContext.Clients.All.SendAsync("StatusUpdate", "Iniciando...", token); + + await SafeSendAsync("StatusUpdate", "Iniciando..."); await _browserService.InitializeAsync(); var page = await _browserService.GetPageAsync(); @@ -72,7 +87,7 @@ namespace WhatsappPromo.Worker // Configurar bindings await page.ExposeFunctionAsync("onNewMessage", (Func)(async (string jsonMessage) => { - await _hubContext.Clients.All.SendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}"); + await SafeSendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}"); })); await page.ExposeFunctionAsync("onMediaDownloaded", (Func)(async (string phone, string base64, string mimeType) => @@ -81,13 +96,13 @@ namespace WhatsappPromo.Worker { var msg = "No se pudo detectar el número de teléfono. Archivo ignorado. Asegúrese de tener el chat abierto."; _logger.LogWarning(msg); - await _hubContext.Clients.All.SendAsync("PersistentLog", "ERROR", msg); + await SafeSendAsync("PersistentLog", "ERROR", msg); return; } var size = base64.Length; _logger.LogInformation("Media recibido de {Phone}. Tamaño: {Size} bytes", phone, size); - await _hubContext.Clients.All.SendAsync("NewMediaReceived", new { Phone = phone, Base64 = base64, MimeType = mimeType }); + await SafeSendAsync("NewMediaReceived", new { Phone = phone, Base64 = base64, MimeType = mimeType }); await _mediaChannel.Writer.WriteAsync(new ProcessedMedia { @@ -95,19 +110,19 @@ namespace WhatsappPromo.Worker Base64Content = base64, MimeType = mimeType, Timestamp = DateTime.Now - }); + }, stoppingToken); })); await page.EvaluateFunctionOnNewDocumentAsync(JsSnippets.MediaExtractor); await page.ReloadAsync(); // Procesamiento en segundo plano - using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); var config = await _configService.GetConfigAsync(); - _ = ProcessMediaQueueAsync(config.DownloadPath, cts.Token); + var processTask = ProcessMediaQueueAsync(config.DownloadPath, cts.Token); // Bucle de monitoreo - while (!token.IsCancellationRequested) + while (!stoppingToken.IsCancellationRequested) { var currentConfig = await _configService.GetConfigAsync(); if (!currentConfig.IsActive) @@ -116,7 +131,7 @@ namespace WhatsappPromo.Worker break; } - if (new Random().Next(0, 10) > 6) + if (Random.Shared.Next(0, 10) > 6) { await _browserService.PerformHumanIdleActionsAsync(); } @@ -126,42 +141,63 @@ namespace WhatsappPromo.Worker var qrBase64 = await _browserService.GetQrCodeAsync(); if (!string.IsNullOrEmpty(qrBase64)) { - await _hubContext.Clients.All.SendAsync("QrCodeUpdate", qrBase64, token); - await _hubContext.Clients.All.SendAsync("StatusUpdate", "Esperando Login (QR)", token); + await SafeSendAsync("QrCodeUpdate", qrBase64); + await SafeSendAsync("StatusUpdate", "Esperando Login (QR)"); } else { - await _hubContext.Clients.All.SendAsync("QrCodeUpdate", null, token); - await _hubContext.Clients.All.SendAsync("StatusUpdate", "Conectado y Escaneando", token); + await SafeSendAsync("QrCodeUpdate", null); + await SafeSendAsync("StatusUpdate", "Conectado y Escaneando"); } } catch {} if (page.IsClosed) break; - await Task.Delay(2000, token); + await Task.Delay(2000, stoppingToken); } cts.Cancel(); + await processTask; } + catch (OperationCanceledException) {} catch (Exception ex) { _logger.LogError(ex, "Error en automatización"); - await _hubContext.Clients.All.SendAsync("LogUpdate", $"ERROR: {ex.Message}"); + await SafeSendAsync("LogUpdate", $"ERROR: {ex.Message}"); - var conf = await _configService.GetConfigAsync(); - conf.IsActive = false; - await _configService.UpdateConfigAsync(conf); + try + { + var conf = await _configService.GetConfigAsync(); + conf.IsActive = false; + await _configService.UpdateConfigAsync(conf); + } catch {} } finally { await _browserService.CloseAsync(); _isAutomationRunning = false; - await _hubContext.Clients.All.SendAsync("StatusUpdate", "Detenido"); + await SafeSendAsync("QrCodeUpdate", null); + await SafeSendAsync("StatusUpdate", "Detenido"); _lock.Release(); } } + private async Task SafeSendAsync(string method, object? arg1, object? arg2 = null) + { + try + { + if (arg2 != null) + await _hubContext.Clients.All.SendAsync(method, arg1, arg2); + else + await _hubContext.Clients.All.SendAsync(method, arg1); + } + catch (Exception ex) + { + _logger.LogDebug("SignalR Send skipped: {Msg}", ex.Message); + } + } + private int _processedToday = 0; private int _processedThisHour = 0; private int _currentDay = -1; @@ -173,7 +209,8 @@ namespace WhatsappPromo.Worker if (string.IsNullOrEmpty(storagePath)) storagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReceivedMedia"); - Directory.CreateDirectory(storagePath); + if (!Directory.Exists(storagePath)) + Directory.CreateDirectory(storagePath); try { @@ -183,68 +220,35 @@ namespace WhatsappPromo.Worker { var config = await _configService.GetConfigAsync(); - // 1. Verificar y resetear contadores de tiempo var now = DateTime.Now; - if (_currentDay != now.Day) - { - _currentDay = now.Day; - _processedToday = 0; - } - if (_currentHour != now.Hour) - { - _currentHour = now.Hour; - _processedThisHour = 0; - } + if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; } + if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; } - // 2. Verificar CUOTAS while (_processedToday >= config.MaxFilesPerDay || _processedThisHour >= config.MaxFilesPerHour) { - var waitMsg = $"Cuota alcanzada ({_processedThisHour}/{config.MaxFilesPerHour}h, {_processedToday}/{config.MaxFilesPerDay}d). Pausando procesamiento..."; - _logger.LogWarning(waitMsg); - await _hubContext.Clients.All.SendAsync("LogUpdate", $"[CUOTA] {waitMsg}", token); - - // Esperar 1 minuto antes de volver a chequear + await SafeSendAsync("LogUpdate", $"[CUOTA] Límite alcanzado, esperando..."); await Task.Delay(60000, token); - now = DateTime.Now; if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; } if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; } } - // 3. Humanizar tiempo de procesamiento (Delay aleatorio) - // Si estamos cerca del límite, aumentamos el delay para "suavizar" el pico - int baseDelay = Random.Shared.Next(5000, 15000); // 5-15s base - if (_processedThisHour > config.MaxFilesPerHour * 0.8) - { - baseDelay = Random.Shared.Next(30000, 60000); // Ralentizar al final de la hora - } - - await Task.Delay(baseDelay, token); - - var extension = media.MimeType switch - { - "image/jpeg" => ".jpg", - "image/png" => ".png", - "video/mp4" => ".mp4", - _ => ".dat" - }; + int delay = Random.Shared.Next(5000, 15000); + if (_processedThisHour > config.MaxFilesPerHour * 0.8) delay = Random.Shared.Next(30000, 60000); + await Task.Delay(delay, token); + var extension = media.MimeType switch { "image/jpeg" => ".jpg", "image/png" => ".png", "video/mp4" => ".mp4", _ => ".dat" }; var fileName = $"{media.PhoneNumber}_{media.Timestamp:yyyyMMdd_HHmmss_fff}{extension}"; var fullPath = Path.Combine(storagePath, fileName); - var bytes = Convert.FromBase64String(media.Base64Content); - await File.WriteAllBytesAsync(fullPath, bytes, token); + await File.WriteAllBytesAsync(fullPath, Convert.FromBase64String(media.Base64Content), token); _processedToday++; _processedThisHour++; - - _logger.LogInformation("Archivo guardado: {Path}. Cuota: {H}/{D}", fullPath, _processedThisHour, _processedToday); - await _hubContext.Clients.All.SendAsync("LogUpdate", $"Archivo guardado: {fileName} (Cuota: {_processedThisHour}/{config.MaxFilesPerHour}h)", token); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error procesando media"); + await SafeSendAsync("LogUpdate", $"Archivo guardado: {fileName}"); } + catch (OperationCanceledException) { break; } + catch (Exception ex) { _logger.LogError(ex, "Error en cola"); } } } catch (OperationCanceledException) {} diff --git a/src/Frontend/WhatsappPromo.Dashboard/index.html b/src/Frontend/WhatsappPromo.Dashboard/index.html index 5282bb5..267c5b1 100644 --- a/src/Frontend/WhatsappPromo.Dashboard/index.html +++ b/src/Frontend/WhatsappPromo.Dashboard/index.html @@ -1,13 +1,16 @@ - - - - - whatsapppromo-dashboard - - -
- - - + + + + + + whatsapppromo-dashboard + + + +
+ + + + \ No newline at end of file diff --git a/src/Frontend/WhatsappPromo.Dashboard/package-lock.json b/src/Frontend/WhatsappPromo.Dashboard/package-lock.json index cbce7e0..1684ef7 100644 --- a/src/Frontend/WhatsappPromo.Dashboard/package-lock.json +++ b/src/Frontend/WhatsappPromo.Dashboard/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@microsoft/signalr": "^10.0.0", + "@tailwindcss/vite": "^4.1.18", "react": "^19.2.0", "react-dom": "^19.2.0" }, @@ -319,7 +320,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -336,7 +336,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -353,7 +352,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -370,7 +368,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -387,7 +384,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -404,7 +400,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -421,7 +416,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -438,7 +432,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -455,7 +448,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -472,7 +464,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -489,7 +480,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -506,7 +496,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -523,7 +512,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -540,7 +528,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -557,7 +544,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -574,7 +560,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -591,7 +576,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -608,7 +592,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -625,7 +608,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -642,7 +624,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -659,7 +640,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -676,7 +656,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -693,7 +672,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -710,7 +688,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -727,7 +704,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -744,7 +720,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -967,7 +942,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -978,7 +952,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -989,7 +962,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -999,14 +971,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1040,7 +1010,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1054,7 +1023,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1068,7 +1036,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1082,7 +1049,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1096,7 +1062,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1110,7 +1075,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1124,7 +1088,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1138,7 +1101,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1152,7 +1114,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1166,7 +1127,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1180,7 +1140,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1194,7 +1153,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1208,7 +1166,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1222,7 +1179,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1236,7 +1192,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1250,7 +1205,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1264,7 +1218,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1278,7 +1231,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1292,7 +1244,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1306,7 +1257,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1320,7 +1270,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1334,7 +1283,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1348,7 +1296,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1362,7 +1309,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1376,13 +1322,269 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1432,7 +1634,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1446,7 +1647,7 @@ "version": "24.10.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz", "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2065,6 +2266,15 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -2072,11 +2282,23 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2364,7 +2586,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -2457,7 +2678,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2504,6 +2724,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2598,6 +2824,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2689,6 +2924,255 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2722,6 +3206,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2746,7 +3239,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2882,14 +3374,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2902,7 +3392,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3022,7 +3511,6 @@ "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3112,7 +3600,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3148,14 +3635,25 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3257,7 +3755,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -3324,7 +3822,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", diff --git a/src/Frontend/WhatsappPromo.Dashboard/package.json b/src/Frontend/WhatsappPromo.Dashboard/package.json index 245d848..7801b24 100644 --- a/src/Frontend/WhatsappPromo.Dashboard/package.json +++ b/src/Frontend/WhatsappPromo.Dashboard/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@microsoft/signalr": "^10.0.0", + "@tailwindcss/vite": "^4.1.18", "react": "^19.2.0", "react-dom": "^19.2.0" }, diff --git a/src/Frontend/WhatsappPromo.Dashboard/src/App.tsx b/src/Frontend/WhatsappPromo.Dashboard/src/App.tsx index 631456a..2b09eb3 100644 --- a/src/Frontend/WhatsappPromo.Dashboard/src/App.tsx +++ b/src/Frontend/WhatsappPromo.Dashboard/src/App.tsx @@ -28,6 +28,7 @@ function App() { maxFilesPerDay: 50 }); const [loading, setLoading] = useState(false); + const [isBrowsing, setIsBrowsing] = useState(false); const [alert, setAlert] = useState<{ type: 'error' | 'info', message: string } | null>(null); useEffect(() => { @@ -83,6 +84,7 @@ function App() { const toggleAutomation = async () => { setLoading(true); const endpoint = config.isActive ? 'stop' : 'start'; + if (config.isActive) setQrCode(null); // Limpieza inmediata local try { await fetch(`http://localhost:5067/api/config/${endpoint}`, { method: 'POST' }); setConfig((prev: SystemConfig) => ({ ...prev, isActive: !prev.isActive })); @@ -108,8 +110,34 @@ function App() { } }; + const handleBrowse = async () => { + try { + setIsBrowsing(true); + const res = await fetch('http://localhost:5067/api/config/browse'); + const data = await res.json(); + if (data.path) { + setConfig(prev => ({ ...prev, downloadPath: data.path })); + } + } catch (err) { + console.error("Error browsing directory", err); + } finally { + setIsBrowsing(false); + } + }; + return ( -
+
+ + {/* BLOQUEO DE INTERACCIÓN DURANTE SELECCIÓN DE CARPETA */} + {isBrowsing && ( +
+
+
+

Seleccionando Carpeta...

+

Por favor, usa la ventana de Windows que acaba de aparecer.

+
+
+ )} {alert && (
)} -
-

WhatsApp Promo Monitor

+
+

+ WhatsApp Promo Monitor +

-
-
- - ) => setConfig({ ...config, downloadPath: e.target.value })} - className="bg-gray-700 text-sm px-2 py-1.5 rounded w-48 border border-gray-600 focus:border-green-500 outline-none transition-colors" - placeholder="C:\Downloads..." - /> +
+
+ +
+ ) => setConfig({ ...config, downloadPath: e.target.value })} + className="flex-1 bg-gray-900 text-sm px-3 py-2.5 rounded-lg border border-gray-600 focus:border-green-500 outline-none transition-all" + placeholder="Seleccionar carpeta..." + /> + +
-
- - ) => setConfig({ ...config, maxFilesPerHour: parseInt(e.target.value) || 0 })} - className="bg-gray-700 text-sm px-2 py-1.5 rounded w-20 border border-gray-600 focus:border-green-500 outline-none transition-colors" - /> +
+
+ + ) => setConfig({ ...config, maxFilesPerHour: parseInt(e.target.value) || 0 })} + className="bg-gray-900 text-sm px-2 py-2.5 rounded-lg border border-gray-600 focus:border-green-500 outline-none transition-all w-16 text-center font-bold" + /> +
+
+ + ) => setConfig({ ...config, maxFilesPerDay: parseInt(e.target.value) || 0 })} + className="bg-gray-900 text-sm px-2 py-2.5 rounded-lg border border-gray-600 focus:border-green-500 outline-none transition-all w-16 text-center font-bold" + /> +
-
- - ) => setConfig({ ...config, maxFilesPerDay: parseInt(e.target.value) || 0 })} - className="bg-gray-700 text-sm px-2 py-1.5 rounded w-20 border border-gray-600 focus:border-green-500 outline-none transition-colors" - /> -
- -
+