fix: ajustes de performance
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 3s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m38s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m15s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 1s

This commit is contained in:
Ricardo Carneiro 2025-09-07 16:11:18 -03:00
parent 5abb4b52c5
commit 004bf284b5
8 changed files with 134 additions and 69 deletions

View File

@ -23,7 +23,8 @@
"Bash(mv:*)", "Bash(mv:*)",
"Bash(dotnet nuget locals:*)", "Bash(dotnet nuget locals:*)",
"Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)", "Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)",
"Bash(sed:*)" "Bash(sed:*)",
"Bash(./clean-build.sh:*)"
] ]
}, },
"enableAllProjectMcpServers": false "enableAllProjectMcpServers": false

View File

@ -14,6 +14,7 @@ public class HomeController : Controller
_userPageService = userPageService; _userPageService = userPageService;
} }
[ResponseCache(Duration = 600, Location = ResponseCacheLocation.Any)] // 10 minutos
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
ViewBag.IsHomePage = true; // Flag para identificar home ViewBag.IsHomePage = true; // Flag para identificar home
@ -23,6 +24,7 @@ public class HomeController : Controller
} }
[Route("Privacy")] [Route("Privacy")]
[ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)] // 1 hora
public IActionResult Privacy() public IActionResult Privacy()
{ {
ViewBag.IsHomePage = true; ViewBag.IsHomePage = true;
@ -30,6 +32,7 @@ public class HomeController : Controller
} }
[Route("Pricing")] [Route("Pricing")]
[ResponseCache(Duration = 1800, Location = ResponseCacheLocation.Any)] // 30 minutos
public IActionResult Pricing() public IActionResult Pricing()
{ {
ViewBag.IsHomePage = true; ViewBag.IsHomePage = true;

View File

@ -39,17 +39,7 @@ public class LivePageController : Controller
} }
// Incrementar view de forma assíncrona (não bloquear response) // Incrementar view de forma assíncrona (não bloquear response)
_ = Task.Run(async () => _ = IncrementViewSafelyAsync(livePage.Id);
{
try
{
await _livePageService.IncrementViewAsync(livePage.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to increment view for LivePage {LivePageId}", livePage.Id);
}
});
// Configurar ViewBag para indicar que é uma live page // Configurar ViewBag para indicar que é uma live page
ViewBag.IsLivePage = true; ViewBag.IsLivePage = true;
@ -74,20 +64,34 @@ public class LivePageController : Controller
var link = livePage.Links[linkIndex]; var link = livePage.Links[linkIndex];
// Track click de forma assíncrona // Track click de forma assíncrona
_ = Task.Run(async () => _ = IncrementLinkClickSafelyAsync(livePage.Id, linkIndex);
{
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);
}
});
_logger.LogInformation("Tracking click for LivePage {LivePageId} link {LinkIndex} -> {Url}", livePage.Id, linkIndex, link.Url); _logger.LogInformation("Tracking click for LivePage {LivePageId} link {LinkIndex} -> {Url}", livePage.Id, linkIndex, link.Url);
return Redirect(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);
}
}
} }

View File

@ -12,21 +12,25 @@ public class UserPageController : Controller
private readonly ISeoService _seoService; private readonly ISeoService _seoService;
private readonly IThemeService _themeService; private readonly IThemeService _themeService;
private readonly IModerationService _moderationService; private readonly IModerationService _moderationService;
private readonly ILogger<UserPageController> _logger;
public UserPageController( public UserPageController(
IUserPageService userPageService, IUserPageService userPageService,
ICategoryService categoryService, ICategoryService categoryService,
ISeoService seoService, ISeoService seoService,
IThemeService themeService, IThemeService themeService,
IModerationService moderationService) IModerationService moderationService,
ILogger<UserPageController> logger)
{ {
_userPageService = userPageService; _userPageService = userPageService;
_categoryService = categoryService; _categoryService = categoryService;
_seoService = seoService; _seoService = seoService;
_themeService = themeService; _themeService = themeService;
_moderationService = moderationService; _moderationService = moderationService;
_logger = logger;
} }
[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "preview" })]
public async Task<IActionResult> Display(string category, string slug) public async Task<IActionResult> Display(string category, string slug)
{ {
var userPage = await _userPageService.GetPageAsync(category, slug); var userPage = await _userPageService.GetPageAsync(category, slug);
@ -83,17 +87,18 @@ public class UserPageController : Controller
var seoSettings = _seoService.GenerateSeoSettings(userPage, categoryObj); var seoSettings = _seoService.GenerateSeoSettings(userPage, categoryObj);
// Record page view (async, don't wait) - only for non-preview requests // 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) 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 referrer = Request.Headers["Referer"].FirstOrDefault();
var userAgent = Request.Headers["User-Agent"].FirstOrDefault(); var userAgent = Request.Headers["User-Agent"].FirstOrDefault();
_ = Task.Run(() => _userPageService.RecordPageViewAsync(userPage.Id, referrer, userAgent)); _ = _userPageService.RecordPageViewAsync(userPage.Id, referrer, userAgent);
} }
else else
{ {
Console.WriteLine($"DEBUG: NOT recording view - isPreview = true"); _logger.LogDebug("NOT recording view - isPreview = true for page {Slug}", userPage.Slug);
} }
ViewBag.SeoSettings = seoSettings; ViewBag.SeoSettings = seoSettings;

View File

@ -8,6 +8,10 @@ public class PageStatusMiddleware
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly ILogger<PageStatusMiddleware> _logger; private readonly ILogger<PageStatusMiddleware> _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<PageStatusMiddleware> logger) public PageStatusMiddleware(RequestDelegate next, ILogger<PageStatusMiddleware> logger)
{ {
@ -93,13 +97,13 @@ public class PageStatusMiddleware
private static bool IsUserPageRoute(PathString path) private static bool IsUserPageRoute(PathString path)
{ {
// Check if path matches pattern: /page/{category}/{slug} // Check if path matches pattern: /page/{category}/{slug} using compiled regex
return Regex.IsMatch(path.Value ?? "", @"^/page/[a-z-]+/[a-z0-9-]+/?$", RegexOptions.IgnoreCase); return UserPageRouteRegex.IsMatch(path.Value ?? "");
} }
private static (string category, string slug) ExtractRouteParameters(PathString path) 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) if (match.Success)
{ {

View File

@ -470,7 +470,43 @@ app.UseHttpsRedirection();
// 🔥 OTIMIZAÇÃO: Ativar compressão de response // 🔥 OTIMIZAÇÃO: Ativar compressão de response
app.UseResponseCompression(); 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(); app.UseRouting();
@ -485,40 +521,29 @@ app.UseMiddleware<BCards.Web.Middleware.PageStatusMiddleware>();
app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>(); app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>();
app.UseMiddleware<ModerationAuthMiddleware>(); app.UseMiddleware<ModerationAuthMiddleware>();
// 🔥 DEBUG MIDDLEWARE MELHORADO // 🔥 DEBUG MIDDLEWARE MELHORADO - Apenas para desenvolvimento
if (app.Environment.IsDevelopment())
{
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
// Debug geral var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
Console.WriteLine($"=== REQUEST DEBUG ===");
Console.WriteLine($"Path: {context.Request.Path}"); // Debug geral apenas em desenvolvimento
Console.WriteLine($"Query: {context.Request.QueryString}"); logger.LogDebug("Request - Path: {Path}, Query: {Query}, Method: {Method}, Scheme: {Scheme}, IsHttps: {IsHttps}, Host: {Host}",
Console.WriteLine($"Method: {context.Request.Method}"); context.Request.Path, context.Request.QueryString, context.Request.Method,
Console.WriteLine($"Scheme: {context.Request.Scheme}"); context.Request.Scheme, context.Request.IsHttps, context.Request.Host);
Console.WriteLine($"IsHttps: {context.Request.IsHttps}");
Console.WriteLine($"Host: {context.Request.Host}");
// Debug específico para Microsoft signin // Debug específico para Microsoft signin
if (context.Request.Path.StartsWithSegments("/signin-microsoft")) if (context.Request.Path.StartsWithSegments("/signin-microsoft"))
{ {
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>(); logger.LogWarning("SIGNIN-MICROSOFT CALLBACK - Path: {Path}, Query: {Query}, Scheme: {Scheme}, IsHttps: {IsHttps}, Host: {Host}, X-Forwarded-Proto: {ForwardedProto}",
logger.LogWarning($"=== SIGNIN-MICROSOFT CALLBACK DEBUG ==="); context.Request.Path, context.Request.QueryString, context.Request.Scheme,
logger.LogWarning($"Path: {context.Request.Path}"); context.Request.IsHttps, context.Request.Host, context.Request.Headers["X-Forwarded-Proto"]);
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}");
}
} }
await next(); await next();
}); });
}
app.UseResponseCaching(); app.UseResponseCaching();

View File

@ -15,7 +15,11 @@ public class UserPageRepository : IUserPageRepository
var slugIndex = Builders<UserPage>.IndexKeys var slugIndex = Builders<UserPage>.IndexKeys
.Ascending(x => x.Category) .Ascending(x => x.Category)
.Ascending(x => x.Slug); .Ascending(x => x.Slug);
_pages.Indexes.CreateOneAsync(new CreateIndexModel<UserPage>(slugIndex, new CreateIndexOptions { Unique = true })); var collation = new Collation("en", strength: CollationStrength.Primary); // Case-insensitive
_pages.Indexes.CreateOneAsync(new CreateIndexModel<UserPage>(slugIndex, new CreateIndexOptions {
Unique = true,
Collation = collation
}));
var userIndex = Builders<UserPage>.IndexKeys.Ascending(x => x.UserId); var userIndex = Builders<UserPage>.IndexKeys.Ascending(x => x.UserId);
_pages.Indexes.CreateOneAsync(new CreateIndexModel<UserPage>(userIndex)); _pages.Indexes.CreateOneAsync(new CreateIndexModel<UserPage>(userIndex));
@ -31,7 +35,21 @@ public class UserPageRepository : IUserPageRepository
public async Task<UserPage?> GetBySlugAsync(string category, string slug) public async Task<UserPage?> 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<UserPage>.Filter.And(
Builders<UserPage>.Filter.Eq(x => x.Category, category),
Builders<UserPage>.Filter.Eq(x => x.Slug, slug),
Builders<UserPage>.Filter.Eq(x => x.IsActive, true)
);
var collation = new Collation("en", strength: CollationStrength.Primary);
var findOptions = new FindOptions<UserPage>
{
Collation = collation
};
var cursor = await _pages.FindAsync(filter, findOptions);
return await cursor.FirstOrDefaultAsync();
} }
public async Task<UserPage?> GetByUserIdAsync(string userId) public async Task<UserPage?> GetByUserIdAsync(string userId)
@ -117,7 +135,7 @@ public class UserPageRepository : IUserPageRepository
); );
} }
// Adicione estes métodos no UserPageRepository.cs // Adicione estes m<EFBFBD>todos no UserPageRepository.cs
public async Task<List<UserPage>> GetManyAsync( public async Task<List<UserPage>> GetManyAsync(
FilterDefinition<UserPage> filter, FilterDefinition<UserPage> filter,
@ -138,7 +156,7 @@ public class UserPageRepository : IUserPageRepository
return await _pages.CountDocumentsAsync(filter); return await _pages.CountDocumentsAsync(filter);
} }
// Método específico para moderação (mais simples) // M<EFBFBD>todo espec<65>fico para modera<72><61>o (mais simples)
public async Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20) public async Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20)
{ {
var filter = Builders<UserPage>.Filter.Eq(x => x.Status, BCards.Web.ViewModels.PageStatus.PendingModeration); var filter = Builders<UserPage>.Filter.Eq(x => x.Status, BCards.Web.ViewModels.PageStatus.PendingModeration);
@ -154,7 +172,7 @@ public class UserPageRepository : IUserPageRepository
.ToListAsync(); .ToListAsync();
} }
// Adicione estes métodos no UserPageRepository.cs // Adicione estes m<EFBFBD>todos no UserPageRepository.cs
public async Task<UpdateResult> UpdateAsync(string id, UpdateDefinition<UserPage> update) public async Task<UpdateResult> UpdateAsync(string id, UpdateDefinition<UserPage> update)
{ {
@ -172,7 +190,7 @@ public class UserPageRepository : IUserPageRepository
return await _pages.UpdateManyAsync(filter, combinedUpdate); return await _pages.UpdateManyAsync(filter, combinedUpdate);
} }
// Métodos específicos para moderação (mais fáceis de usar) // M<EFBFBD>todos espec<65>ficos para modera<72><61>o (mais f<>ceis de usar)
public async Task<bool> ApprovePageAsync(string pageId) public async Task<bool> ApprovePageAsync(string pageId)
{ {
var update = Builders<UserPage>.Update var update = Builders<UserPage>.Update
@ -192,7 +210,7 @@ public class UserPageRepository : IUserPageRepository
var page = await GetByIdAsync(pageId); var page = await GetByIdAsync(pageId);
if (page == null) return false; if (page == null) return false;
// Adicionar à história de moderação // Adicionar <EFBFBD> hist<73>ria de modera<72><61>o
var historyEntry = new ModerationHistory var historyEntry = new ModerationHistory
{ {
Attempt = page.ModerationAttempts + 1, Attempt = page.ModerationAttempts + 1,

View File

@ -7,6 +7,11 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"Stripe": {
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543"
},
"Serilog": { "Serilog": {
"SeqUrl": "http://192.168.0.100:5341", "SeqUrl": "http://192.168.0.100:5341",
"ApiKey": "" "ApiKey": ""