From 004bf284b549a6aac5537f00412f93c16f6b234c Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sun, 7 Sep 2025 16:11:18 -0300 Subject: [PATCH] fix: ajustes de performance --- .claude/settings.local.json | 3 +- src/BCards.Web/Controllers/HomeController.cs | 3 + .../Controllers/LivePageController.cs | 48 +++++----- .../Controllers/UserPageController.cs | 15 ++-- .../Middleware/PageStatusMiddleware.cs | 10 ++- src/BCards.Web/Program.cs | 87 ++++++++++++------- .../Repositories/UserPageRepository.cs | 32 +++++-- src/BCards.Web/appsettings.Development.json | 5 ++ 8 files changed, 134 insertions(+), 69 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a97d884..c611f9a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,8 @@ "Bash(mv:*)", "Bash(dotnet nuget locals:*)", "Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)", - "Bash(sed:*)" + "Bash(sed:*)", + "Bash(./clean-build.sh:*)" ] }, "enableAllProjectMcpServers": false diff --git a/src/BCards.Web/Controllers/HomeController.cs b/src/BCards.Web/Controllers/HomeController.cs index 52ff107..272adfc 100644 --- a/src/BCards.Web/Controllers/HomeController.cs +++ b/src/BCards.Web/Controllers/HomeController.cs @@ -14,6 +14,7 @@ public class HomeController : Controller _userPageService = userPageService; } + [ResponseCache(Duration = 600, Location = ResponseCacheLocation.Any)] // 10 minutos public async Task Index() { ViewBag.IsHomePage = true; // Flag para identificar home @@ -23,6 +24,7 @@ public class HomeController : Controller } [Route("Privacy")] + [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)] // 1 hora public IActionResult Privacy() { ViewBag.IsHomePage = true; @@ -30,6 +32,7 @@ public class HomeController : Controller } [Route("Pricing")] + [ResponseCache(Duration = 1800, Location = ResponseCacheLocation.Any)] // 30 minutos public IActionResult Pricing() { ViewBag.IsHomePage = true; diff --git a/src/BCards.Web/Controllers/LivePageController.cs b/src/BCards.Web/Controllers/LivePageController.cs index 7de4bfd..7727300 100644 --- a/src/BCards.Web/Controllers/LivePageController.cs +++ b/src/BCards.Web/Controllers/LivePageController.cs @@ -39,17 +39,7 @@ public class LivePageController : Controller } // Incrementar view de forma assíncrona (não bloquear response) - _ = Task.Run(async () => - { - try - { - await _livePageService.IncrementViewAsync(livePage.Id); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to increment view for LivePage {LivePageId}", livePage.Id); - } - }); + _ = IncrementViewSafelyAsync(livePage.Id); // Configurar ViewBag para indicar que é uma live page ViewBag.IsLivePage = true; @@ -74,20 +64,34 @@ public class LivePageController : Controller var link = livePage.Links[linkIndex]; // Track click de forma assíncrona - _ = Task.Run(async () => - { - try - { - await _livePageService.IncrementLinkClickAsync(livePage.Id, linkIndex); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to track click for LivePage {LivePageId} link {LinkIndex}", livePage.Id, linkIndex); - } - }); + _ = IncrementLinkClickSafelyAsync(livePage.Id, linkIndex); _logger.LogInformation("Tracking click for LivePage {LivePageId} link {LinkIndex} -> {Url}", livePage.Id, linkIndex, link.Url); return Redirect(link.Url); } + + private async Task IncrementViewSafelyAsync(string livePageId) + { + try + { + await _livePageService.IncrementViewAsync(livePageId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to increment view for LivePage {LivePageId}", livePageId); + } + } + + private async Task IncrementLinkClickSafelyAsync(string livePageId, int linkIndex) + { + try + { + await _livePageService.IncrementLinkClickAsync(livePageId, linkIndex); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to track click for LivePage {LivePageId} link {LinkIndex}", livePageId, linkIndex); + } + } } \ No newline at end of file diff --git a/src/BCards.Web/Controllers/UserPageController.cs b/src/BCards.Web/Controllers/UserPageController.cs index 4c98aa2..daeb0aa 100644 --- a/src/BCards.Web/Controllers/UserPageController.cs +++ b/src/BCards.Web/Controllers/UserPageController.cs @@ -12,21 +12,25 @@ public class UserPageController : Controller private readonly ISeoService _seoService; private readonly IThemeService _themeService; private readonly IModerationService _moderationService; + private readonly ILogger _logger; public UserPageController( IUserPageService userPageService, ICategoryService categoryService, ISeoService seoService, IThemeService themeService, - IModerationService moderationService) + IModerationService moderationService, + ILogger logger) { _userPageService = userPageService; _categoryService = categoryService; _seoService = seoService; _themeService = themeService; _moderationService = moderationService; + _logger = logger; } + [ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "preview" })] public async Task Display(string category, string slug) { var userPage = await _userPageService.GetPageAsync(category, slug); @@ -83,17 +87,18 @@ public class UserPageController : Controller var seoSettings = _seoService.GenerateSeoSettings(userPage, categoryObj); // Record page view (async, don't wait) - only for non-preview requests - Console.WriteLine($"DEBUG VIEW COUNT - Page: {userPage.Slug}, Status: {userPage.Status}, IsPreview: {isPreview}, PreviewToken: {previewToken}"); + _logger.LogDebug("View Count - Page: {Slug}, Status: {Status}, IsPreview: {IsPreview}, PreviewToken: {PreviewToken}", + userPage.Slug, userPage.Status, isPreview, !string.IsNullOrEmpty(previewToken)); if (!isPreview) { - Console.WriteLine($"DEBUG: Recording view for page {userPage.Slug}"); + _logger.LogDebug("Recording view for page {Slug}", userPage.Slug); var referrer = Request.Headers["Referer"].FirstOrDefault(); var userAgent = Request.Headers["User-Agent"].FirstOrDefault(); - _ = Task.Run(() => _userPageService.RecordPageViewAsync(userPage.Id, referrer, userAgent)); + _ = _userPageService.RecordPageViewAsync(userPage.Id, referrer, userAgent); } else { - Console.WriteLine($"DEBUG: NOT recording view - isPreview = true"); + _logger.LogDebug("NOT recording view - isPreview = true for page {Slug}", userPage.Slug); } ViewBag.SeoSettings = seoSettings; diff --git a/src/BCards.Web/Middleware/PageStatusMiddleware.cs b/src/BCards.Web/Middleware/PageStatusMiddleware.cs index 9295dbd..6fed42e 100644 --- a/src/BCards.Web/Middleware/PageStatusMiddleware.cs +++ b/src/BCards.Web/Middleware/PageStatusMiddleware.cs @@ -8,6 +8,10 @@ public class PageStatusMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; + private static readonly Regex UserPageRouteRegex = new(@"^/page/[a-z-]+/[a-z0-9-]+/?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex RouteParametersRegex = new(@"^/page/([a-z-]+)/([a-z0-9-]+)/?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); public PageStatusMiddleware(RequestDelegate next, ILogger logger) { @@ -93,13 +97,13 @@ public class PageStatusMiddleware private static bool IsUserPageRoute(PathString path) { - // Check if path matches pattern: /page/{category}/{slug} - return Regex.IsMatch(path.Value ?? "", @"^/page/[a-z-]+/[a-z0-9-]+/?$", RegexOptions.IgnoreCase); + // Check if path matches pattern: /page/{category}/{slug} using compiled regex + return UserPageRouteRegex.IsMatch(path.Value ?? ""); } private static (string category, string slug) ExtractRouteParameters(PathString path) { - var match = Regex.Match(path.Value ?? "", @"^/page/([a-z-]+)/([a-z0-9-]+)/?$", RegexOptions.IgnoreCase); + var match = RouteParametersRegex.Match(path.Value ?? ""); if (match.Success) { diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index ef41dd2..ebff3b4 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -470,7 +470,43 @@ app.UseHttpsRedirection(); // 🔥 OTIMIZAÇÃO: Ativar compressão de response app.UseResponseCompression(); -app.UseStaticFiles(); +app.UseStaticFiles(new StaticFileOptions +{ + OnPrepareResponse = ctx => + { + var fileName = ctx.File.Name; + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + + TimeSpan maxAge; + + // Cache mais agressivo para assets que mudam raramente + if (extension == ".css" || extension == ".js") + { + // CSS/JS: 30 dias (podem mudar com updates) + maxAge = TimeSpan.FromDays(30); + } + else if (extension == ".woff" || extension == ".woff2" || extension == ".ttf" || + extension == ".eot" || extension == ".svg" || extension == ".otf") + { + // Fontes: 1 ano (raramente mudam) + maxAge = TimeSpan.FromDays(365); + } + else if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || + extension == ".gif" || extension == ".ico" || extension == ".webp") + { + // Imagens: 6 meses + maxAge = TimeSpan.FromDays(180); + } + else + { + // Outros arquivos: 1 dia + maxAge = TimeSpan.FromDays(1); + } + + ctx.Context.Response.Headers.CacheControl = $"public,max-age={maxAge.TotalSeconds}"; + ctx.Context.Response.Headers.Append("Vary", "Accept-Encoding"); + } +}); app.UseRouting(); @@ -485,40 +521,29 @@ app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); -// 🔥 DEBUG MIDDLEWARE MELHORADO -app.Use(async (context, next) => +// 🔥 DEBUG MIDDLEWARE MELHORADO - Apenas para desenvolvimento +if (app.Environment.IsDevelopment()) { - // Debug geral - Console.WriteLine($"=== REQUEST DEBUG ==="); - Console.WriteLine($"Path: {context.Request.Path}"); - Console.WriteLine($"Query: {context.Request.QueryString}"); - Console.WriteLine($"Method: {context.Request.Method}"); - Console.WriteLine($"Scheme: {context.Request.Scheme}"); - Console.WriteLine($"IsHttps: {context.Request.IsHttps}"); - Console.WriteLine($"Host: {context.Request.Host}"); - - // Debug específico para Microsoft signin - if (context.Request.Path.StartsWithSegments("/signin-microsoft")) + app.Use(async (context, next) => { var logger = context.RequestServices.GetRequiredService>(); - logger.LogWarning($"=== SIGNIN-MICROSOFT CALLBACK DEBUG ==="); - logger.LogWarning($"Path: {context.Request.Path}"); - logger.LogWarning($"Query: {context.Request.QueryString}"); - logger.LogWarning($"Method: {context.Request.Method}"); - logger.LogWarning($"Scheme: {context.Request.Scheme}"); - logger.LogWarning($"IsHttps: {context.Request.IsHttps}"); - logger.LogWarning($"Host: {context.Request.Host}"); - logger.LogWarning($"X-Forwarded-Proto: {context.Request.Headers["X-Forwarded-Proto"]}"); - logger.LogWarning($"X-Forwarded-For: {context.Request.Headers["X-Forwarded-For"]}"); - logger.LogWarning($"All Headers:"); - foreach (var header in context.Request.Headers) - { - logger.LogWarning($" {header.Key}: {header.Value}"); - } - } + + // Debug geral apenas em desenvolvimento + logger.LogDebug("Request - Path: {Path}, Query: {Query}, Method: {Method}, Scheme: {Scheme}, IsHttps: {IsHttps}, Host: {Host}", + context.Request.Path, context.Request.QueryString, context.Request.Method, + context.Request.Scheme, context.Request.IsHttps, context.Request.Host); - await next(); -}); + // Debug específico para Microsoft signin + if (context.Request.Path.StartsWithSegments("/signin-microsoft")) + { + logger.LogWarning("SIGNIN-MICROSOFT CALLBACK - Path: {Path}, Query: {Query}, Scheme: {Scheme}, IsHttps: {IsHttps}, Host: {Host}, X-Forwarded-Proto: {ForwardedProto}", + context.Request.Path, context.Request.QueryString, context.Request.Scheme, + context.Request.IsHttps, context.Request.Host, context.Request.Headers["X-Forwarded-Proto"]); + } + + await next(); + }); +} app.UseResponseCaching(); diff --git a/src/BCards.Web/Repositories/UserPageRepository.cs b/src/BCards.Web/Repositories/UserPageRepository.cs index 6beae3c..cfdf40b 100644 --- a/src/BCards.Web/Repositories/UserPageRepository.cs +++ b/src/BCards.Web/Repositories/UserPageRepository.cs @@ -15,7 +15,11 @@ public class UserPageRepository : IUserPageRepository var slugIndex = Builders.IndexKeys .Ascending(x => x.Category) .Ascending(x => x.Slug); - _pages.Indexes.CreateOneAsync(new CreateIndexModel(slugIndex, new CreateIndexOptions { Unique = true })); + var collation = new Collation("en", strength: CollationStrength.Primary); // Case-insensitive + _pages.Indexes.CreateOneAsync(new CreateIndexModel(slugIndex, new CreateIndexOptions { + Unique = true, + Collation = collation + })); var userIndex = Builders.IndexKeys.Ascending(x => x.UserId); _pages.Indexes.CreateOneAsync(new CreateIndexModel(userIndex)); @@ -31,7 +35,21 @@ public class UserPageRepository : IUserPageRepository public async Task GetBySlugAsync(string category, string slug) { - return await _pages.Find(x => x.Category.ToLower() == category.ToLower() && x.Slug == slug && x.IsActive).FirstOrDefaultAsync(); + // Usar filtro com collation case-insensitive para melhor performance + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.Category, category), + Builders.Filter.Eq(x => x.Slug, slug), + Builders.Filter.Eq(x => x.IsActive, true) + ); + + var collation = new Collation("en", strength: CollationStrength.Primary); + var findOptions = new FindOptions + { + Collation = collation + }; + + var cursor = await _pages.FindAsync(filter, findOptions); + return await cursor.FirstOrDefaultAsync(); } public async Task GetByUserIdAsync(string userId) @@ -117,7 +135,7 @@ public class UserPageRepository : IUserPageRepository ); } - // Adicione estes métodos no UserPageRepository.cs + // Adicione estes m�todos no UserPageRepository.cs public async Task> GetManyAsync( FilterDefinition filter, @@ -138,7 +156,7 @@ public class UserPageRepository : IUserPageRepository return await _pages.CountDocumentsAsync(filter); } - // Método específico para moderação (mais simples) + // M�todo espec�fico para modera��o (mais simples) public async Task> GetPendingModerationAsync(int skip = 0, int take = 20) { var filter = Builders.Filter.Eq(x => x.Status, BCards.Web.ViewModels.PageStatus.PendingModeration); @@ -154,7 +172,7 @@ public class UserPageRepository : IUserPageRepository .ToListAsync(); } - // Adicione estes métodos no UserPageRepository.cs + // Adicione estes m�todos no UserPageRepository.cs public async Task UpdateAsync(string id, UpdateDefinition update) { @@ -172,7 +190,7 @@ public class UserPageRepository : IUserPageRepository return await _pages.UpdateManyAsync(filter, combinedUpdate); } - // Métodos específicos para moderação (mais fáceis de usar) + // M�todos espec�ficos para modera��o (mais f�ceis de usar) public async Task ApprovePageAsync(string pageId) { var update = Builders.Update @@ -192,7 +210,7 @@ public class UserPageRepository : IUserPageRepository var page = await GetByIdAsync(pageId); if (page == null) return false; - // Adicionar à história de moderação + // Adicionar � hist�ria de modera��o var historyEntry = new ModerationHistory { Attempt = page.ModerationAttempts + 1, diff --git a/src/BCards.Web/appsettings.Development.json b/src/BCards.Web/appsettings.Development.json index 41702e7..11ae000 100644 --- a/src/BCards.Web/appsettings.Development.json +++ b/src/BCards.Web/appsettings.Development.json @@ -7,6 +7,11 @@ "Microsoft.AspNetCore": "Warning" } }, + "Stripe": { + "PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS", + "SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO", + "WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543" + }, "Serilog": { "SeqUrl": "http://192.168.0.100:5341", "ApiKey": ""