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(dotnet nuget locals:*)",
"Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)",
"Bash(sed:*)"
"Bash(sed:*)",
"Bash(./clean-build.sh:*)"
]
},
"enableAllProjectMcpServers": false

View File

@ -14,6 +14,7 @@ public class HomeController : Controller
_userPageService = userPageService;
}
[ResponseCache(Duration = 600, Location = ResponseCacheLocation.Any)] // 10 minutos
public async Task<IActionResult> 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;

View File

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

View File

@ -12,21 +12,25 @@ public class UserPageController : Controller
private readonly ISeoService _seoService;
private readonly IThemeService _themeService;
private readonly IModerationService _moderationService;
private readonly ILogger<UserPageController> _logger;
public UserPageController(
IUserPageService userPageService,
ICategoryService categoryService,
ISeoService seoService,
IThemeService themeService,
IModerationService moderationService)
IModerationService moderationService,
ILogger<UserPageController> 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<IActionResult> 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;

View File

@ -8,6 +8,10 @@ public class PageStatusMiddleware
{
private readonly RequestDelegate _next;
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)
{
@ -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)
{

View File

@ -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<BCards.Web.Middleware.PageStatusMiddleware>();
app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>();
app.UseMiddleware<ModerationAuthMiddleware>();
// 🔥 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<ILogger<Program>>();
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}");
}
}
await next();
});
// 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);
// 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();

View File

@ -15,7 +15,11 @@ public class UserPageRepository : IUserPageRepository
var slugIndex = Builders<UserPage>.IndexKeys
.Ascending(x => x.Category)
.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);
_pages.Indexes.CreateOneAsync(new CreateIndexModel<UserPage>(userIndex));
@ -31,7 +35,21 @@ public class UserPageRepository : IUserPageRepository
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)
@ -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(
FilterDefinition<UserPage> filter,
@ -138,7 +156,7 @@ public class UserPageRepository : IUserPageRepository
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)
{
var filter = Builders<UserPage>.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<EFBFBD>todos no UserPageRepository.cs
public async Task<UpdateResult> UpdateAsync(string id, UpdateDefinition<UserPage> 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<EFBFBD>todos espec<65>ficos para modera<72><61>o (mais f<>ceis de usar)
public async Task<bool> ApprovePageAsync(string pageId)
{
var update = Builders<UserPage>.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 <EFBFBD> hist<73>ria de modera<72><61>o
var historyEntry = new ModerationHistory
{
Attempt = page.ModerationAttempts + 1,

View File

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