From 43a519a4fe2c49e7c59e44ad02190b447e68b470 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 28 Apr 2026 17:38:27 -0300 Subject: [PATCH 1/3] docs: initialize technical debt tracking, roadmap history, and project roadmap documentation --- docs/roadmap-history.md | 16 ++++++++++++++++ docs/roadmap.md | 10 ++++------ docs/technical-debt.md | 17 ++++++++--------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/roadmap-history.md b/docs/roadmap-history.md index b5e4fde8d..48da82ace 100644 --- a/docs/roadmap-history.md +++ b/docs/roadmap-history.md @@ -4,6 +4,22 @@ Este documento contém o registro de todas as sprints concluídas para fins de a --- +## ✅ Sprint 13 - RabbitMQ Excellence & i18n Tests (Concluída em 28 Abr 2026) + +**Objetivo**: Consolidação total da infraestrutura de mensageria, i18n frontend e UI/UX Admin. + +### Entregas: +- ✅ **RabbitMQ Excellence**: Implementação real do `RabbitMqInfrastructureManager` com métodos assíncronos (`CreateQueueAsync`, `CreateExchangeAsync`, `BindQueueToExchangeAsync`), eliminando stubs. +- ✅ **Deadlock Fix**: Remoção de deadlocks em `CreateQueueAsync`, `CreateExchangeAsync`, `BindQueueToExchangeAsync` - agora `GetChannelAsync` gerencia lock internamente. +- ✅ **Safe Dispose**: `DisposeAsync` agora adquire lock antes de dispose e usa flag `_disposed` para prevenir operações pós-descarte. +- ✅ **Fail-Fast DI**: `MessagingExtensions` agora lança `InvalidOperationException` quando `IRabbitMqInfrastructureManager` não está registrado (em vez de retornar silenciosamente). +- ✅ **i18n Frontend**: Implementação de `useTranslation` no dashboard Admin, tradução de labels via i18n (`t()`), uso de `useMemo` para otimização. +- ✅ **i18n Test Mocks**: Implementação de mocks de i18next para testes em Admin, Provider e Customer apps com suporte a `defaultValue` (incluindo strings vazias). +- ✅ **UI/UX Admin Portal**: Aplicação de cores da marca (laranja #D96704, brand #E0702B, cream #FDFBF7) no CSS, variáveis CSS para cores secundárias, `data-testid` únicos em CardTitle. +- ✅ **Testes Unitários**: Cobertura superior a 90%, testes de `MessagingExtensions` e `RabbitMqInfrastructureManager` atualizados e passando. + +--- + ## ✅ Sprint 12 - Bookings & Messaging Excellence (Concluída em 26 Abr 2026) **Objetivo**: Implementar o sistema de agendamentos e consolidar a infraestrutura de mensageria com Rebus. diff --git a/docs/roadmap.md b/docs/roadmap.md index 9453648df..c66bd61b7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -18,10 +18,7 @@ Este é o planejamento estratégico unificado da plataforma MeAjudaAi. ## 🔮 Roadmaps Futuros (MVP Launch & Além) -### Fase 3: Escala e Provedores Reais (Em Execução) -* **[x] RabbitMQ Excellence**: Consolidação total da infraestrutura de mensageria, removendo stubs e dependências diretas de driver. -* **[ ] i18n Apps Provider/Admin**: Implementação de localização frontend para os apps de Prestador e Administrador. -* **[ ] UI/UX Admin Portal**: Padronização visual com cores da marca e melhorias de usabilidade no portal administrativo. +### Fase 3: Escala e Provedores Reais * **Provedores de Comunicação (Próximo)**: Substituir Stubs por SendGrid (E-mail), Twilio (SMS) e Firebase (Push). * **Verificação Automatizada (Próximo)**: OCR via Azure AI Vision e integração com APIs de antecedentes criminais. @@ -38,8 +35,9 @@ Este é o planejamento estratégico unificado da plataforma MeAjudaAi. ## ✅ Concluído Recentemente -* **Sprint 11**: Monetização completa (Checkout, Webhooks, Billing Portal, Renovação Automática), Localização i18n Frontend, Skeleton Loaders e cobertura de testes abrangente. (Abril 2026) -* **Sprint 10**: Módulo de Ratings, Moderação de Conteúdo, Login Social Instagram (#141), Alinhamento de Realms Keycloak, Infra CI/CD (OpenAPI gating) e Documentação (coleções Bruno). (Abril 2026) +* **Sprint 13**: RabbitMQ Excellence (infraestrutura real com RabbitMqInfrastructureManager, deadlocks corrigidos, dispose seguro), i18n mocks para testes (Provider/Admin/Customer), fail-fast em DI de Messaging. +* **Sprint 12**: Bookings Module completo, Command Handlers (Reject/Complete), queries de listagem, automação com Domain Events, integração frontend de agenda. +* **Sprint 11**: Monetização completa (Checkout, Webhooks, Billing Portal, Renovação Automática), Localização i18n Frontend, Skeleton Loaders e cobertura de testes abrangente. --- diff --git a/docs/technical-debt.md b/docs/technical-debt.md index 6cd3d58de..b7352f4b9 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -13,20 +13,19 @@ Este documento rastreia **débitos técnicos e seu histórico de otimização**. - [ ] Perfilagem de memória em produção -### 🎨 Melhorias de UI/UX +--- -**Severidade**: BAIXA -**Sprint**: Backlog +## 📋 Histórico -- [ ] Aplicar cores da marca (azul, creme, laranja) em todo o Portal Admin (React) -- [ ] Atualizar o tema da biblioteca de componentes React -- [ ] Padronizar a estilização dos componentes +### 🎨 UI/UX Admin Portal - Cores da Marca -**Origem**: Sprint 7.19 +**Resolvido em**: Abr 2026 (Sprint 13) | **Severidade original**: BAIXA +Aplicação de cores da marca (laranja #D96704, brand #E0702B, cream #FDFBF7) no CSS do Admin, variáveis CSS para cores secundárias, ring atualizado para cor da marca, e correção de `data-testid` duplicados em CardTitle. ---- +### 🌍 i18n Frontend Apps (Admin/Provider) -## 📋 Histórico +**Resolvido em**: Abr 2026 (Sprint 13) | **Severidade original**: MÉDIA +Implementação de `useTranslation` no dashboard Admin, tradução de labels via i18n, e mocks de i18next para testes em todos os apps (Admin/Provider/Customer) com suporte a `defaultValue` (incluindo strings vazias). ### 🚀 Infraestrutura & Messaging (RabbitMQ Excellence) From 9fffbf2de653bd8f6a60b108eae4353220546889 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 28 Apr 2026 20:49:04 -0300 Subject: [PATCH 2/3] docs: add roadmap and technical debt tracking documentation --- docs/roadmap.md | 4 ++-- docs/technical-debt.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index c66bd61b7..41d2821b1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -6,9 +6,9 @@ Este é o planejamento estratégico unificado da plataforma MeAjudaAi. ## 📊 Status Atual (Abril 2026) -**Sprint Atual**: 13 (Infraestrutura & Excelência UX) +**Sprint Atual**: 14 (Próxima) -**Status**: 🛠️ Em Execução (Início em 27 Abr 2026) +**Status**: ✅ Sprint 13 Concluída (28 Abr 2026) **Meta MVP**: 12 a 16 de maio de 2026 diff --git a/docs/technical-debt.md b/docs/technical-debt.md index b7352f4b9..1f8041718 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -22,7 +22,7 @@ Este documento rastreia **débitos técnicos e seu histórico de otimização**. **Resolvido em**: Abr 2026 (Sprint 13) | **Severidade original**: BAIXA Aplicação de cores da marca (laranja #D96704, brand #E0702B, cream #FDFBF7) no CSS do Admin, variáveis CSS para cores secundárias, ring atualizado para cor da marca, e correção de `data-testid` duplicados em CardTitle. -### 🌍 i18n Frontend Apps (Admin/Provider) +### 🌍 i18n Frontend Apps (Admin/Provider/Customer) **Resolvido em**: Abr 2026 (Sprint 13) | **Severidade original**: MÉDIA Implementação de `useTranslation` no dashboard Admin, tradução de labels via i18n, e mocks de i18next para testes em todos os apps (Admin/Provider/Customer) com suporte a `defaultValue` (incluindo strings vazias). From 80383b1d9ec04af0758588f8c22ef5a8f43d892f Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 01:32:08 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20CodeRabbit=20Chat:=20Impleme?= =?UTF-8?q?nt=20requested=20code=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Middlewares/RequestLoggingMiddleware.cs | 4 ++- .../RegisterCustomerCommandHandler.cs | 30 +++++++++++-------- .../PermissionOptimizationMiddleware.cs | 16 ++++++++-- src/Shared/Caching/CacheMetrics.cs | 16 +++++----- .../Monitoring/BusinessMetricsMiddleware.cs | 24 ++++++++++++--- 5 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RequestLoggingMiddleware.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RequestLoggingMiddleware.cs index 26f19cb43..01867e87a 100644 --- a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RequestLoggingMiddleware.cs +++ b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RequestLoggingMiddleware.cs @@ -132,7 +132,9 @@ private static string GetClientIpAddress(HttpContext context) var xForwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (!string.IsNullOrEmpty(xForwardedFor)) { - return xForwardedFor.Split(',')[0].Trim(); + var span = xForwardedFor.AsSpan(); + var commaIndex = span.IndexOf(','); + return (commaIndex >= 0 ? span[..commaIndex] : span).Trim().ToString(); } var xRealIp = context.Request.Headers["X-Real-IP"].FirstOrDefault(); diff --git a/src/Modules/Users/Application/Handlers/Commands/RegisterCustomerCommandHandler.cs b/src/Modules/Users/Application/Handlers/Commands/RegisterCustomerCommandHandler.cs index bede312ac..9e6ae25bb 100644 --- a/src/Modules/Users/Application/Handlers/Commands/RegisterCustomerCommandHandler.cs +++ b/src/Modules/Users/Application/Handlers/Commands/RegisterCustomerCommandHandler.cs @@ -45,24 +45,27 @@ public async Task> HandleAsync(RegisterCustomerCommand command, try { emailAsValueObject = new Email(command.Email); - - var fullLocalPart = emailAsValueObject.Value.Split('@')[0]; - var noTagLocalPart = fullLocalPart.Split('+')[0]; - var sanitizedLocalPart = SanitizationRegex().Replace(noTagLocalPart, ""); - + + var emailSpan = emailAsValueObject.Value.AsSpan(); + var atIdx = emailSpan.IndexOf('@'); + var localSpan = atIdx >= 0 ? emailSpan[..atIdx] : emailSpan; + var plusIdx = localSpan.IndexOf('+'); + var noTagSpan = plusIdx >= 0 ? localSpan[..plusIdx] : localSpan; + var sanitizedLocalPart = SanitizationRegex().Replace(noTagSpan.ToString(), ""); + if (string.IsNullOrWhiteSpace(sanitizedLocalPart) || sanitizedLocalPart.Length < 3) { - sanitizedLocalPart = $"usr{Guid.NewGuid().ToString("N").Substring(0, 5)}"; + sanitizedLocalPart = $"usr{Guid.NewGuid().ToString("N").AsSpan(0, 5).ToString()}"; } - + // UsernameMaxLength é 30 em ValidationConstants; deduz 1 para '_' e 6 para GUID => localPartMax = UsernameMaxLength - 7 int maxLocalPartLength = ValidationConstants.UserLimits.UsernameMaxLength - 7; if (sanitizedLocalPart.Length > maxLocalPartLength) { - sanitizedLocalPart = sanitizedLocalPart.Substring(0, maxLocalPartLength); + sanitizedLocalPart = sanitizedLocalPart[..maxLocalPartLength]; } - - var slug = $"{sanitizedLocalPart}_{Guid.NewGuid().ToString("N").Substring(0, 6)}"; + + var slug = $"{sanitizedLocalPart}_{Guid.NewGuid().ToString("N").AsSpan(0, 6).ToString()}"; validUsername = new Username(slug); } catch (ArgumentException ex) @@ -70,9 +73,10 @@ public async Task> HandleAsync(RegisterCustomerCommand command, return Result.Failure(Error.BadRequest(ex.Message)); } - var emailParts = command.Email.Split('@'); - var maskedEmail = emailParts.Length == 2 - ? $"{new string('*', Math.Min(3, emailParts[0].Length))}@{emailParts[1]}" + var rawEmailSpan = command.Email.AsSpan(); + var atSeparator = rawEmailSpan.IndexOf('@'); + var maskedEmail = atSeparator >= 0 + ? $"{new string('*', Math.Min(3, atSeparator))}@{rawEmailSpan[(atSeparator + 1)..].ToString()}" : "***@***"; // Valida unicidade primeiro diff --git a/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs b/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs index 1a96543ef..aba1457a8 100644 --- a/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs +++ b/src/Shared/Authorization/Middleware/PermissionOptimizationMiddleware.cs @@ -248,9 +248,19 @@ private static bool IsAdminPath(string path) if (string.IsNullOrEmpty(path)) return false; - var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); - return segments.Any(segment => - string.Equals(segment, "admin", StringComparison.OrdinalIgnoreCase)); + var span = path.AsSpan(); + while (span.Length > 0) + { + var slashIndex = span.IndexOf('/'); + var segment = slashIndex >= 0 ? span[..slashIndex] : span; + if (segment.Equals("admin".AsSpan(), StringComparison.OrdinalIgnoreCase)) + return true; + if (slashIndex < 0) + break; + span = span[(slashIndex + 1)..]; + } + + return false; } /// diff --git a/src/Shared/Caching/CacheMetrics.cs b/src/Shared/Caching/CacheMetrics.cs index eaac0b19a..2aae6587a 100644 --- a/src/Shared/Caching/CacheMetrics.cs +++ b/src/Shared/Caching/CacheMetrics.cs @@ -32,19 +32,19 @@ private static string NormalizeCacheKey(string key) if (string.IsNullOrEmpty(key)) return "empty"; - var parts = key.Split(':'); - if (parts.Length >= 2) + var colonIndex = key.AsSpan().IndexOf(':'); + if (colonIndex >= 0) { - var type = parts[0]; - if (type.Equals("user", StringComparison.OrdinalIgnoreCase)) + var type = key.AsSpan()[..colonIndex]; + if (type.Equals("user".AsSpan(), StringComparison.OrdinalIgnoreCase)) return "user:{id}"; - if (type.Equals("provider", StringComparison.OrdinalIgnoreCase)) + if (type.Equals("provider".AsSpan(), StringComparison.OrdinalIgnoreCase)) return "provider:{id}"; - if (type.Equals("permission", StringComparison.OrdinalIgnoreCase)) + if (type.Equals("permission".AsSpan(), StringComparison.OrdinalIgnoreCase)) return "permission:{id}"; - if (type.Equals("role", StringComparison.OrdinalIgnoreCase)) + if (type.Equals("role".AsSpan(), StringComparison.OrdinalIgnoreCase)) return "role:{id}"; - return $"{type}:{{id}}"; + return $"{type.ToString()}:{{id}}"; } if (key.Length > 20) diff --git a/src/Shared/Monitoring/BusinessMetricsMiddleware.cs b/src/Shared/Monitoring/BusinessMetricsMiddleware.cs index ef1f34d5f..66c0c3528 100644 --- a/src/Shared/Monitoring/BusinessMetricsMiddleware.cs +++ b/src/Shared/Monitoring/BusinessMetricsMiddleware.cs @@ -78,9 +78,7 @@ private void LogBusinessEvents(HttpContext context, TimeSpan elapsed) } // Conclusão de ajuda (aceita rotas versionadas como /api/v1/help-requests/{id}/complete) - if (((path.StartsWith("/api/help-requests/") && path.EndsWith("/complete")) || - (path.StartsWith("/api/v") && path.Contains("/help-requests/") && path.EndsWith("/complete"))) && - method == "POST" && statusCode is >= 200 and < 300) + if (IsHelpRequestCompletePath(path) && method == "POST" && statusCode is >= 200 and < 300) { businessMetrics.RecordHelpRequestCompleted("general", elapsed); logger.LogInformation("Help request completed in {ElapsedMs}ms", elapsed.TotalMilliseconds); @@ -88,10 +86,28 @@ private void LogBusinessEvents(HttpContext context, TimeSpan elapsed) } } + /// + /// Verifica se o path corresponde a uma conclusão de help-request (rotas simples ou versionadas). + /// Usa ReadOnlySpan para evitar alocações em um hot path executado a cada requisição POST. + /// + private static bool IsHelpRequestCompletePath(string path) + { + var span = path.AsSpan(); + ReadOnlySpan completeSuffix = "/complete".AsSpan(); + if (!span.EndsWith(completeSuffix, StringComparison.Ordinal)) + return false; + + ReadOnlySpan directPrefix = "/api/help-requests/".AsSpan(); + ReadOnlySpan segment = "/help-requests/".AsSpan(); + return span.StartsWith(directPrefix, StringComparison.Ordinal) + || (span.StartsWith("/api/v".AsSpan(), StringComparison.Ordinal) + && span.Contains(segment, StringComparison.Ordinal)); + } + private static string GetEndpointName(HttpContext context) { var endpoint = context.GetEndpoint(); - if (endpoint != null) + { return endpoint.DisplayName ?? context.Request.Path.Value ?? "unknown"; }