Feat: UI/UX y Procesos
This commit is contained in:
@@ -17,7 +17,7 @@ namespace WhatsappPromo.Core.Services
|
|||||||
{
|
{
|
||||||
private readonly string _configPath;
|
private readonly string _configPath;
|
||||||
private readonly ILogger<ConfigService> _logger;
|
private readonly ILogger<ConfigService> _logger;
|
||||||
private SystemConfig _cachedConfig;
|
private SystemConfig? _cachedConfig;
|
||||||
|
|
||||||
public ConfigService(ILogger<ConfigService> logger)
|
public ConfigService(ILogger<ConfigService> logger)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace WhatsappPromo.Engine.Services
|
|||||||
Task InitializeAsync();
|
Task InitializeAsync();
|
||||||
Task<Page> GetPageAsync();
|
Task<Page> GetPageAsync();
|
||||||
Task CloseAsync();
|
Task CloseAsync();
|
||||||
Task<string> GetQrCodeAsync();
|
Task<string?> GetQrCodeAsync();
|
||||||
Task PerformHumanIdleActionsAsync();
|
Task PerformHumanIdleActionsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,185 +32,103 @@ namespace WhatsappPromo.Engine.Services
|
|||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
|
if (_browser != null && !_browser.IsClosed) return;
|
||||||
|
|
||||||
_logger.LogInformation("Descargando navegador si es necesario...");
|
_logger.LogInformation("Descargando navegador si es necesario...");
|
||||||
var browserFetcher = new BrowserFetcher();
|
var browserFetcher = new BrowserFetcher();
|
||||||
await browserFetcher.DownloadAsync();
|
await browserFetcher.DownloadAsync();
|
||||||
|
|
||||||
var options = new LaunchOptions
|
var options = new LaunchOptions
|
||||||
{
|
{
|
||||||
Headless = false, // Headless mode aumenta riesgo de detección
|
Headless = false,
|
||||||
UserDataDir = _userDataDir,
|
UserDataDir = _userDataDir,
|
||||||
Args = new[]
|
Args = new[]
|
||||||
{
|
{
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--disable-setuid-sandbox",
|
"--disable-setuid-sandbox",
|
||||||
"--disable-infobars",
|
"--disable-infobars",
|
||||||
|
"--start-minimized",
|
||||||
"--window-position=0,0",
|
"--window-position=0,0",
|
||||||
"--ignore-certificate-errors",
|
"--ignore-certificate-errors",
|
||||||
"--ignore-certificate-errors-spki-list",
|
"--ignore-certificate-errors-spki-list",
|
||||||
|
|
||||||
// ANTI-DETECCIÓN CRÍTICA
|
|
||||||
"--disable-blink-features=AutomationControlled",
|
"--disable-blink-features=AutomationControlled",
|
||||||
|
|
||||||
// Evitar detección de DevTools
|
|
||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
|
|
||||||
// Hacer que el navegador parezca más "normal"
|
|
||||||
"--disable-extensions-except",
|
"--disable-extensions-except",
|
||||||
"--disable-extensions",
|
"--disable-extensions",
|
||||||
|
|
||||||
// GPU y WebGL realistas (importante para huellas digitales)
|
|
||||||
"--enable-webgl",
|
"--enable-webgl",
|
||||||
"--use-gl=swiftshader",
|
"--use-gl=swiftshader",
|
||||||
|
"--disable-features=IsolateOrigins,site-per-process,CalculateNativeWinOcclusion",
|
||||||
|
"--disable-default-apps",
|
||||||
|
|
||||||
// Deshabilitar características que revelan automatización
|
// Flags para evitar que el navegador se "duerma" al estar minimizado
|
||||||
"--disable-features=IsolateOrigins,site-per-process",
|
"--disable-backgrounding-occluded-windows",
|
||||||
|
"--disable-background-timer-throttling",
|
||||||
// Evitar leaks de automatización
|
"--disable-renderer-backgrounding"
|
||||||
"--disable-default-apps"
|
|
||||||
},
|
},
|
||||||
IgnoredDefaultArgs = new[] { "--enable-automation" }, // Quita "Chrome is being controlled"
|
IgnoredDefaultArgs = new[] { "--enable-automation" },
|
||||||
DefaultViewport = null // Permite que el viewport sea controlado por window size
|
DefaultViewport = null
|
||||||
};
|
};
|
||||||
|
|
||||||
_logger.LogInformation("Iniciando navegador Chrome...");
|
_logger.LogInformation("Iniciando navegador Chrome...");
|
||||||
_browser = await Puppeteer.LaunchAsync(options);
|
_browser = await Puppeteer.LaunchAsync(options);
|
||||||
|
|
||||||
|
// Forzar minimizado mediante el proceso si es posible
|
||||||
|
try
|
||||||
|
{
|
||||||
var pages = await _browser.PagesAsync();
|
var pages = await _browser.PagesAsync();
|
||||||
_page = pages.Length > 0 ? pages[0] : await _browser.NewPageAsync();
|
_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");
|
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
|
await _page.SetViewportAsync(new ViewPortOptions
|
||||||
{
|
{
|
||||||
Width = 1366 + random.Next(-50, 150), // 1316-1516
|
Width = 1366 + Random.Shared.Next(-50, 150),
|
||||||
Height = 768 + random.Next(-50, 150), // 718-918
|
Height = 768 + Random.Shared.Next(-50, 150),
|
||||||
DeviceScaleFactor = 1
|
DeviceScaleFactor = 1
|
||||||
});
|
});
|
||||||
|
|
||||||
// INYECCIÓN MASIVA DE ANTI-DETECCIÓN
|
// Anti-detección
|
||||||
await _page.EvaluateExpressionOnNewDocumentAsync(@"
|
await _page.EvaluateExpressionOnNewDocumentAsync(@"
|
||||||
// 1. Ocultar navigator.webdriver
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||||
Object.defineProperty(navigator, 'webdriver', {
|
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };
|
||||||
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
|
|
||||||
const originalQuery = window.navigator.permissions.query;
|
const originalQuery = window.navigator.permissions.query;
|
||||||
window.navigator.permissions.query = (parameters) => (
|
window.navigator.permissions.query = (parameters) => (
|
||||||
parameters.name === 'notifications' ?
|
parameters.name === 'notifications' ?
|
||||||
Promise.resolve({ state: Notification.permission }) :
|
Promise.resolve({ state: Notification.permission }) :
|
||||||
originalQuery(parameters)
|
originalQuery(parameters)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Sobrescribir plugins y mimeTypes (navegadores automatizados tienen 0)
|
|
||||||
Object.defineProperty(navigator, 'plugins', {
|
Object.defineProperty(navigator, 'plugins', {
|
||||||
get: () => [
|
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/x-google-chrome-pdf', suffixes: 'pdf'},
|
{0: {type: 'application/pdf', suffixes: 'pdf'}, description: 'Portable Document Format', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', length: 1, name: 'Chrome PDF Viewer'}
|
||||||
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, 'languages', { get: () => ['es-ES', 'es', 'en-US', 'en'] });
|
||||||
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)
|
|
||||||
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
||||||
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
||||||
if (parameter === 37445) {
|
if (parameter === 37445) return 'Intel Inc.';
|
||||||
return 'Intel Inc.'; // UNMASKED_VENDOR_WEBGL
|
if (parameter === 37446) return 'Intel(R) UHD Graphics';
|
||||||
}
|
|
||||||
if (parameter === 37446) {
|
|
||||||
return 'Intel(R) UHD Graphics'; // UNMASKED_RENDERER_WEBGL
|
|
||||||
}
|
|
||||||
return getParameter.apply(this, [parameter]);
|
return getParameter.apply(this, [parameter]);
|
||||||
};
|
};
|
||||||
|
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 + Math.floor(Math.random() * 5) });
|
||||||
// 7. Ocultar características de headless Chrome
|
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
|
||||||
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, 'getBattery', {
|
Object.defineProperty(navigator, 'getBattery', {
|
||||||
get: () => () => Promise.resolve({
|
get: () => () => Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 0.97 })
|
||||||
charging: true,
|
|
||||||
chargingTime: 0,
|
|
||||||
dischargingTime: Infinity,
|
|
||||||
level: 0.97
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 10. Connection API
|
|
||||||
Object.defineProperty(navigator, 'connection', {
|
Object.defineProperty(navigator, 'connection', {
|
||||||
get: () => ({
|
get: () => ({ effectiveType: '4g', downlink: 10, rtt: 50, saveData: false })
|
||||||
effectiveType: '4g',
|
|
||||||
downlink: 10,
|
|
||||||
rtt: 50,
|
|
||||||
saveData: false
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
console.log('✅ Anti-detección ejecutada');
|
||||||
// 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');
|
|
||||||
");
|
");
|
||||||
|
|
||||||
_logger.LogInformation("Navegando a WhatsApp Web...");
|
_logger.LogInformation("Navegando a WhatsApp Web...");
|
||||||
@@ -218,70 +136,75 @@ namespace WhatsappPromo.Engine.Services
|
|||||||
{
|
{
|
||||||
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
|
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation("WhatsApp Web cargado.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Page> GetPageAsync()
|
public async Task<Page> GetPageAsync()
|
||||||
{
|
{
|
||||||
if (_page == null) await InitializeAsync();
|
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()
|
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()
|
public async Task PerformHumanIdleActionsAsync()
|
||||||
{
|
{
|
||||||
if (_page == null) return;
|
if (_page == null || _page.IsClosed) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var rnd = new Random();
|
var action = Random.Shared.Next(1, 4);
|
||||||
var action = rnd.Next(1, 4);
|
|
||||||
|
|
||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case 1: // Random Mouse Move con curva Bezier (más humano)
|
case 1:
|
||||||
var x = rnd.Next(100, 1000);
|
var x = Random.Shared.Next(100, 1000);
|
||||||
var y = rnd.Next(100, 600);
|
var y = Random.Shared.Next(100, 600);
|
||||||
// Steps más alto = movimiento más suave y humano
|
await _page.Mouse.MoveAsync(x, y, new MoveOptions { Steps = Random.Shared.Next(20, 60) });
|
||||||
await _page.Mouse.MoveAsync(x, y, new MoveOptions { Steps = rnd.Next(20, 60) });
|
|
||||||
break;
|
break;
|
||||||
case 2: // Small Scroll con variación
|
case 2:
|
||||||
var delta = rnd.Next(-150, 150);
|
var delta = Random.Shared.Next(-150, 150);
|
||||||
await _page.Mouse.WheelAsync(0, delta);
|
await _page.Mouse.WheelAsync(0, delta);
|
||||||
break;
|
break;
|
||||||
case 3: // Pause con duración variable (simula lectura)
|
case 3:
|
||||||
await Task.Delay(rnd.Next(1500, 4000));
|
await Task.Delay(Random.Shared.Next(1500, 4000));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* Ignore errors during idle actions */ }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetQrCodeAsync()
|
public async Task<string?> GetQrCodeAsync()
|
||||||
{
|
{
|
||||||
if (_page == null) return null;
|
if (_page == null || _page.IsClosed) return null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Selector del canvas del QR
|
|
||||||
var qrSelector = "canvas";
|
var qrSelector = "canvas";
|
||||||
await _page.WaitForSelectorAsync(qrSelector, new WaitForSelectorOptions { Timeout = 5000 });
|
|
||||||
var element = await _page.QuerySelectorAsync(qrSelector);
|
var element = await _page.QuerySelectorAsync(qrSelector);
|
||||||
if (element != null)
|
if (element != null)
|
||||||
{
|
{
|
||||||
// Devolver como base64 para el frontend
|
|
||||||
return await element.ScreenshotBase64Async();
|
return await element.ScreenshotBase64Async();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch {}
|
||||||
{
|
|
||||||
// No se encontró código QR (posiblemente ya logueado)
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
using WhatsappPromo.Core.Models;
|
using WhatsappPromo.Core.Models;
|
||||||
using WhatsappPromo.Core.Services;
|
using WhatsappPromo.Core.Services;
|
||||||
|
|
||||||
@@ -28,7 +30,6 @@ namespace WhatsappPromo.Worker.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> UpdateConfig([FromBody] SystemConfig config)
|
public async Task<IActionResult> UpdateConfig([FromBody] SystemConfig config)
|
||||||
{
|
{
|
||||||
// Validar ruta
|
|
||||||
if (!string.IsNullOrEmpty(config.DownloadPath))
|
if (!string.IsNullOrEmpty(config.DownloadPath))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -62,5 +63,64 @@ namespace WhatsappPromo.Worker.Controllers
|
|||||||
await _configService.UpdateConfigAsync(config);
|
await _configService.UpdateConfigAsync(config);
|
||||||
return Ok(new { status = "Stopping" });
|
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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ var app = builder.Build();
|
|||||||
|
|
||||||
app.UseCors("AllowDashboard");
|
app.UseCors("AllowDashboard");
|
||||||
|
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapHub<WhatsappHub>("/whatsappHub"); // SignalR Endpoint for Frontend
|
app.MapHub<WhatsappHub>("/whatsappHub"); // SignalR Endpoint for Frontend
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace WhatsappPromo.Worker
|
|||||||
{
|
{
|
||||||
private readonly ILogger<WhatsappWorker> _logger;
|
private readonly ILogger<WhatsappWorker> _logger;
|
||||||
private readonly IBrowserService _browserService;
|
private readonly IBrowserService _browserService;
|
||||||
private readonly IConfigService _configService; // Check this dependency
|
private readonly IConfigService _configService;
|
||||||
private readonly Channel<ProcessedMedia> _mediaChannel;
|
private readonly Channel<ProcessedMedia> _mediaChannel;
|
||||||
private readonly IHubContext<WhatsappHub> _hubContext;
|
private readonly IHubContext<WhatsappHub> _hubContext;
|
||||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
@@ -41,6 +41,8 @@ namespace WhatsappPromo.Worker
|
|||||||
_logger.LogInformation("Servicio worker iniciado. Esperando activación manual...");
|
_logger.LogInformation("Servicio worker iniciado. Esperando activación manual...");
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var config = await _configService.GetConfigAsync();
|
var config = await _configService.GetConfigAsync();
|
||||||
|
|
||||||
@@ -51,20 +53,33 @@ namespace WhatsappPromo.Worker
|
|||||||
|
|
||||||
await Task.Delay(2000, stoppingToken);
|
await Task.Delay(2000, stoppingToken);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error en el bucle principal del Worker");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartAutomationAsync(CancellationToken token)
|
_logger.LogInformation("Cerrando servicio worker...");
|
||||||
|
await _browserService.CloseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartAutomationAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
if (_isAutomationRunning) return;
|
if (_isAutomationRunning) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _lock.WaitAsync(token);
|
await _lock.WaitAsync(stoppingToken);
|
||||||
if (_isAutomationRunning) return;
|
if (_isAutomationRunning) return;
|
||||||
|
|
||||||
_isAutomationRunning = true;
|
_isAutomationRunning = true;
|
||||||
_logger.LogInformation("Iniciando AUTOMATIZACIÓN...");
|
_logger.LogInformation("Iniciando AUTOMATIZACIÓN...");
|
||||||
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Iniciando...", token);
|
|
||||||
|
await SafeSendAsync("StatusUpdate", "Iniciando...");
|
||||||
|
|
||||||
await _browserService.InitializeAsync();
|
await _browserService.InitializeAsync();
|
||||||
var page = await _browserService.GetPageAsync();
|
var page = await _browserService.GetPageAsync();
|
||||||
@@ -72,7 +87,7 @@ namespace WhatsappPromo.Worker
|
|||||||
// Configurar bindings
|
// Configurar bindings
|
||||||
await page.ExposeFunctionAsync("onNewMessage", (Func<string, Task>)(async (string jsonMessage) =>
|
await page.ExposeFunctionAsync("onNewMessage", (Func<string, Task>)(async (string jsonMessage) =>
|
||||||
{
|
{
|
||||||
await _hubContext.Clients.All.SendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}");
|
await SafeSendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await page.ExposeFunctionAsync("onMediaDownloaded", (Func<string, string, string, Task>)(async (string phone, string base64, string mimeType) =>
|
await page.ExposeFunctionAsync("onMediaDownloaded", (Func<string, string, string, Task>)(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.";
|
var msg = "No se pudo detectar el número de teléfono. Archivo ignorado. Asegúrese de tener el chat abierto.";
|
||||||
_logger.LogWarning(msg);
|
_logger.LogWarning(msg);
|
||||||
await _hubContext.Clients.All.SendAsync("PersistentLog", "ERROR", msg);
|
await SafeSendAsync("PersistentLog", "ERROR", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var size = base64.Length;
|
var size = base64.Length;
|
||||||
_logger.LogInformation("Media recibido de {Phone}. Tamaño: {Size} bytes", phone, size);
|
_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
|
await _mediaChannel.Writer.WriteAsync(new ProcessedMedia
|
||||||
{
|
{
|
||||||
@@ -95,19 +110,19 @@ namespace WhatsappPromo.Worker
|
|||||||
Base64Content = base64,
|
Base64Content = base64,
|
||||||
MimeType = mimeType,
|
MimeType = mimeType,
|
||||||
Timestamp = DateTime.Now
|
Timestamp = DateTime.Now
|
||||||
});
|
}, stoppingToken);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await page.EvaluateFunctionOnNewDocumentAsync(JsSnippets.MediaExtractor);
|
await page.EvaluateFunctionOnNewDocumentAsync(JsSnippets.MediaExtractor);
|
||||||
await page.ReloadAsync();
|
await page.ReloadAsync();
|
||||||
|
|
||||||
// Procesamiento en segundo plano
|
// Procesamiento en segundo plano
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||||
var config = await _configService.GetConfigAsync();
|
var config = await _configService.GetConfigAsync();
|
||||||
_ = ProcessMediaQueueAsync(config.DownloadPath, cts.Token);
|
var processTask = ProcessMediaQueueAsync(config.DownloadPath, cts.Token);
|
||||||
|
|
||||||
// Bucle de monitoreo
|
// Bucle de monitoreo
|
||||||
while (!token.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var currentConfig = await _configService.GetConfigAsync();
|
var currentConfig = await _configService.GetConfigAsync();
|
||||||
if (!currentConfig.IsActive)
|
if (!currentConfig.IsActive)
|
||||||
@@ -116,7 +131,7 @@ namespace WhatsappPromo.Worker
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new Random().Next(0, 10) > 6)
|
if (Random.Shared.Next(0, 10) > 6)
|
||||||
{
|
{
|
||||||
await _browserService.PerformHumanIdleActionsAsync();
|
await _browserService.PerformHumanIdleActionsAsync();
|
||||||
}
|
}
|
||||||
@@ -126,42 +141,63 @@ namespace WhatsappPromo.Worker
|
|||||||
var qrBase64 = await _browserService.GetQrCodeAsync();
|
var qrBase64 = await _browserService.GetQrCodeAsync();
|
||||||
if (!string.IsNullOrEmpty(qrBase64))
|
if (!string.IsNullOrEmpty(qrBase64))
|
||||||
{
|
{
|
||||||
await _hubContext.Clients.All.SendAsync("QrCodeUpdate", qrBase64, token);
|
await SafeSendAsync("QrCodeUpdate", qrBase64);
|
||||||
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Esperando Login (QR)", token);
|
await SafeSendAsync("StatusUpdate", "Esperando Login (QR)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _hubContext.Clients.All.SendAsync("QrCodeUpdate", null, token);
|
await SafeSendAsync("QrCodeUpdate", null);
|
||||||
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Conectado y Escaneando", token);
|
await SafeSendAsync("StatusUpdate", "Conectado y Escaneando");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {}
|
catch {}
|
||||||
|
|
||||||
if (page.IsClosed) break;
|
if (page.IsClosed) break;
|
||||||
|
|
||||||
await Task.Delay(2000, token);
|
await Task.Delay(2000, stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
|
await processTask;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) {}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error en automatización");
|
_logger.LogError(ex, "Error en automatización");
|
||||||
await _hubContext.Clients.All.SendAsync("LogUpdate", $"ERROR: {ex.Message}");
|
await SafeSendAsync("LogUpdate", $"ERROR: {ex.Message}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var conf = await _configService.GetConfigAsync();
|
var conf = await _configService.GetConfigAsync();
|
||||||
conf.IsActive = false;
|
conf.IsActive = false;
|
||||||
await _configService.UpdateConfigAsync(conf);
|
await _configService.UpdateConfigAsync(conf);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await _browserService.CloseAsync();
|
await _browserService.CloseAsync();
|
||||||
_isAutomationRunning = false;
|
_isAutomationRunning = false;
|
||||||
await _hubContext.Clients.All.SendAsync("StatusUpdate", "Detenido");
|
await SafeSendAsync("QrCodeUpdate", null);
|
||||||
|
await SafeSendAsync("StatusUpdate", "Detenido");
|
||||||
_lock.Release();
|
_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 _processedToday = 0;
|
||||||
private int _processedThisHour = 0;
|
private int _processedThisHour = 0;
|
||||||
private int _currentDay = -1;
|
private int _currentDay = -1;
|
||||||
@@ -173,6 +209,7 @@ namespace WhatsappPromo.Worker
|
|||||||
if (string.IsNullOrEmpty(storagePath))
|
if (string.IsNullOrEmpty(storagePath))
|
||||||
storagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReceivedMedia");
|
storagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReceivedMedia");
|
||||||
|
|
||||||
|
if (!Directory.Exists(storagePath))
|
||||||
Directory.CreateDirectory(storagePath);
|
Directory.CreateDirectory(storagePath);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -183,68 +220,35 @@ namespace WhatsappPromo.Worker
|
|||||||
{
|
{
|
||||||
var config = await _configService.GetConfigAsync();
|
var config = await _configService.GetConfigAsync();
|
||||||
|
|
||||||
// 1. Verificar y resetear contadores de tiempo
|
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
if (_currentDay != now.Day)
|
if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; }
|
||||||
{
|
if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; }
|
||||||
_currentDay = now.Day;
|
|
||||||
_processedToday = 0;
|
|
||||||
}
|
|
||||||
if (_currentHour != now.Hour)
|
|
||||||
{
|
|
||||||
_currentHour = now.Hour;
|
|
||||||
_processedThisHour = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Verificar CUOTAS
|
|
||||||
while (_processedToday >= config.MaxFilesPerDay || _processedThisHour >= config.MaxFilesPerHour)
|
while (_processedToday >= config.MaxFilesPerDay || _processedThisHour >= config.MaxFilesPerHour)
|
||||||
{
|
{
|
||||||
var waitMsg = $"Cuota alcanzada ({_processedThisHour}/{config.MaxFilesPerHour}h, {_processedToday}/{config.MaxFilesPerDay}d). Pausando procesamiento...";
|
await SafeSendAsync("LogUpdate", $"[CUOTA] Límite alcanzado, esperando...");
|
||||||
_logger.LogWarning(waitMsg);
|
|
||||||
await _hubContext.Clients.All.SendAsync("LogUpdate", $"[CUOTA] {waitMsg}", token);
|
|
||||||
|
|
||||||
// Esperar 1 minuto antes de volver a chequear
|
|
||||||
await Task.Delay(60000, token);
|
await Task.Delay(60000, token);
|
||||||
|
|
||||||
now = DateTime.Now;
|
now = DateTime.Now;
|
||||||
if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; }
|
if (_currentDay != now.Day) { _currentDay = now.Day; _processedToday = 0; }
|
||||||
if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; }
|
if (_currentHour != now.Hour) { _currentHour = now.Hour; _processedThisHour = 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Humanizar tiempo de procesamiento (Delay aleatorio)
|
int delay = Random.Shared.Next(5000, 15000);
|
||||||
// Si estamos cerca del límite, aumentamos el delay para "suavizar" el pico
|
if (_processedThisHour > config.MaxFilesPerHour * 0.8) delay = Random.Shared.Next(30000, 60000);
|
||||||
int baseDelay = Random.Shared.Next(5000, 15000); // 5-15s base
|
await Task.Delay(delay, token);
|
||||||
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"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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 fileName = $"{media.PhoneNumber}_{media.Timestamp:yyyyMMdd_HHmmss_fff}{extension}";
|
||||||
var fullPath = Path.Combine(storagePath, fileName);
|
var fullPath = Path.Combine(storagePath, fileName);
|
||||||
|
|
||||||
var bytes = Convert.FromBase64String(media.Base64Content);
|
await File.WriteAllBytesAsync(fullPath, Convert.FromBase64String(media.Base64Content), token);
|
||||||
await File.WriteAllBytesAsync(fullPath, bytes, token);
|
|
||||||
|
|
||||||
_processedToday++;
|
_processedToday++;
|
||||||
_processedThisHour++;
|
_processedThisHour++;
|
||||||
|
await SafeSendAsync("LogUpdate", $"Archivo guardado: {fileName}");
|
||||||
_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");
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "Error en cola"); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) {}
|
catch (OperationCanceledException) {}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>whatsapppromo-dashboard</title>
|
<title>whatsapppromo-dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
<body style="background-color: #0f172a; margin: 0;">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
639
src/Frontend/WhatsappPromo.Dashboard/package-lock.json
generated
639
src/Frontend/WhatsappPromo.Dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/signalr": "^10.0.0",
|
"@microsoft/signalr": "^10.0.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function App() {
|
|||||||
maxFilesPerDay: 50
|
maxFilesPerDay: 50
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isBrowsing, setIsBrowsing] = useState(false);
|
||||||
const [alert, setAlert] = useState<{ type: 'error' | 'info', message: string } | null>(null);
|
const [alert, setAlert] = useState<{ type: 'error' | 'info', message: string } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,6 +84,7 @@ function App() {
|
|||||||
const toggleAutomation = async () => {
|
const toggleAutomation = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const endpoint = config.isActive ? 'stop' : 'start';
|
const endpoint = config.isActive ? 'stop' : 'start';
|
||||||
|
if (config.isActive) setQrCode(null); // Limpieza inmediata local
|
||||||
try {
|
try {
|
||||||
await fetch(`http://localhost:5067/api/config/${endpoint}`, { method: 'POST' });
|
await fetch(`http://localhost:5067/api/config/${endpoint}`, { method: 'POST' });
|
||||||
setConfig((prev: SystemConfig) => ({ ...prev, isActive: !prev.isActive }));
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white p-8 font-sans">
|
<div className="min-h-screen bg-slate-900 text-white p-8 font-sans relative">
|
||||||
|
|
||||||
|
{/* BLOQUEO DE INTERACCIÓN DURANTE SELECCIÓN DE CARPETA */}
|
||||||
|
{isBrowsing && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-[100] flex flex-col items-center justify-center backdrop-blur-sm cursor-wait">
|
||||||
|
<div className="bg-gray-800 p-8 rounded-2xl shadow-2xl border border-gray-700 flex flex-col items-center gap-4">
|
||||||
|
<div className="w-12 h-12 border-4 border-green-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
<p className="text-xl font-bold">Seleccionando Carpeta...</p>
|
||||||
|
<p className="text-gray-400 text-sm">Por favor, usa la ventana de Windows que acaba de aparecer.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{alert && (
|
{alert && (
|
||||||
<div className={`fixed top-4 left-1/2 transform -translate-x-1/2 z-50 px-6 py-4 rounded shadow-2xl flex items-center gap-4 ${alert.type === 'error' ? 'bg-red-800 text-white border border-red-500' : 'bg-blue-800 text-white border border-blue-500'
|
<div className={`fixed top-4 left-1/2 transform -translate-x-1/2 z-50 px-6 py-4 rounded shadow-2xl flex items-center gap-4 ${alert.type === 'error' ? 'bg-red-800 text-white border border-red-500' : 'bg-blue-800 text-white border border-blue-500'
|
||||||
@@ -123,56 +151,68 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row justify-between lg:items-center mb-8 gap-4">
|
<div className="flex flex-col lg:flex-row justify-between lg:items-center mb-10 gap-6">
|
||||||
<h1 className="text-4xl font-bold text-green-400">WhatsApp Promo Monitor</h1>
|
<h1 className="text-4xl font-black text-green-500 tracking-tight drop-shadow-md">
|
||||||
|
WhatsApp Promo Monitor
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 items-center bg-gray-800 p-4 rounded-lg shadow-xl border border-gray-700">
|
<div className="flex flex-col md:flex-row items-stretch md:items-end gap-4 bg-gray-800 p-5 rounded-2xl shadow-2xl border border-gray-700">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col flex-1 min-w-[200px]">
|
||||||
<label className="text-xs text-gray-400 mb-1 uppercase tracking-wider font-bold">Ruta de Descarga</label>
|
<label className="text-[10px] text-gray-400 mb-1.5 uppercase tracking-widest font-extrabold">Ruta de Descarga</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={config.downloadPath}
|
value={config.downloadPath}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfig({ ...config, downloadPath: e.target.value })}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => 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"
|
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="C:\Downloads..."
|
placeholder="Seleccionar carpeta..."
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleBrowse}
|
||||||
|
className="bg-gray-700 hover:bg-gray-600 px-3 rounded-lg border border-gray-600 transition-all flex items-center justify-center"
|
||||||
|
title="Seleccionar Carpeta"
|
||||||
|
>
|
||||||
|
<span className="text-lg">📂</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="text-xs text-gray-400 mb-1 uppercase tracking-wider font-bold">Máx/Hora</label>
|
<label className="text-[10px] text-gray-400 mb-1.5 uppercase tracking-widest font-extrabold">Máx/Hora</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.maxFilesPerHour}
|
value={config.maxFilesPerHour}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfig({ ...config, maxFilesPerHour: parseInt(e.target.value) || 0 })}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="text-xs text-gray-400 mb-1 uppercase tracking-wider font-bold">Máx/Día</label>
|
<label className="text-[10px] text-gray-400 mb-1.5 uppercase tracking-widest font-extrabold">Máx/Día</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={config.maxFilesPerDay}
|
value={config.maxFilesPerDay}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfig({ ...config, maxFilesPerDay: parseInt(e.target.value) || 0 })}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => 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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 self-end">
|
<div className="flex gap-2 items-end">
|
||||||
<button
|
<button
|
||||||
onClick={updateConfig}
|
onClick={updateConfig}
|
||||||
className="bg-gray-700 hover:bg-gray-600 p-2 rounded border border-gray-600 transition-colors"
|
className="bg-slate-700 hover:bg-slate-600 p-3 rounded-lg border border-slate-600 transition-all shadow-md"
|
||||||
title="Guardar Configuración"
|
title="Guardar Configuración"
|
||||||
>
|
>
|
||||||
💾
|
<span className="text-lg leading-none">💾</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleAutomation}
|
onClick={toggleAutomation}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={`px-6 py-2 rounded font-bold transition-all shadow-lg ${config.isActive
|
className={`min-w-[140px] py-2.5 px-4 rounded-lg font-black transition-all shadow-lg text-sm border-b-4 active:border-b-0 active:translate-y-1 ${config.isActive
|
||||||
? 'bg-red-600 hover:bg-red-700 shadow-red-900/40 border border-red-500'
|
? 'bg-red-600 hover:bg-red-500 border-red-800 text-white'
|
||||||
: 'bg-green-600 hover:bg-green-700 shadow-green-900/40 border border-green-500'
|
: 'bg-green-600 hover:bg-green-500 border-green-800 text-white shadow-green-900/20'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{loading ? '...' : (config.isActive ? '🛑 DETENER' : '▶ INICIAR')}
|
{loading ? '...' : (config.isActive ? '🛑 DETENER' : '▶ INICIAR')}
|
||||||
|
|||||||
@@ -1,8 +1,38 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
/* Custom scrollbar if needed */
|
:root {
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
color-scheme: dark;
|
||||||
display: none;
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
/* bg-slate-900 */
|
||||||
|
color: #f1f5f9;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #334155;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #475569;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user