# WebApp Improvement Plan ## 🔴 CRITICAL - Security Issues (Fix Immediately) ### 1. Fix Login Credential Exposure ⚠️ URGENT **Status**: Not Started **Priority**: P0 **Estimated Time**: 30 minutes **Problem**: Login.razor (lines 129-133) sends credentials via GET request in URL parameters. **Security Risks**: - Credentials appear in server logs - Credentials stored in browser history - Credentials may leak via referrer headers - Violates OWASP security guidelines **Current Code**: ```csharp // Login.razor - INSECURE var uri = $"/Auth/CookieLogin?email={Uri.EscapeDataString(_loginModel.Email)}" + $"&password={Uri.EscapeDataString(_loginModel.Password)}" + $"&rememberMe={_loginModel.RememberMe}"; Navigation.NavigateTo(uri, forceLoad: true); ``` **Fix**: ```csharp // Login.razor - Add HttpClient injection [Inject] private HttpClient Http { get; set; } = default!; private async Task HandleLogin() { _isSubmitting = true; _errorMessage = null; try { // Get client IP address var ipAddress = HttpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "unknown"; // Check rate limiting if (RateLimitService.IsLockedOut(ipAddress)) { var remaining = RateLimitService.GetRemainingLockoutTime(ipAddress); _errorMessage = $"Too many failed attempts. Please try again in {remaining?.Minutes ?? 15} minutes."; return; } // Create form data var formData = new Dictionary { ["email"] = _loginModel.Email, ["password"] = _loginModel.Password, ["rememberMe"] = _loginModel.RememberMe.ToString() }; // POST credentials securely var response = await Http.PostAsync("/Auth/CookieLogin", new FormUrlEncodedContent(formData)); if (response.IsSuccessStatusCode) { Navigation.NavigateTo("/", forceLoad: true); } else { _errorMessage = "Invalid email or password."; } } catch (Exception ex) { _errorMessage = "An error occurred during login. Please try again."; Console.WriteLine($"Login error: {ex}"); } finally { _isSubmitting = false; } } ``` ```csharp // AuthController.cs - Remove [HttpGet] attribute [HttpPost] // Remove [HttpGet] support [AllowAnonymous] public async Task CookieLogin( [FromForm] string email, [FromForm] string password, [FromForm] bool rememberMe = false) { // ... existing implementation } ``` **Files to Modify**: - `WebApp/Components/Login.razor` - `WebApp/Authentication/AuthController.cs` --- ### 2. Fix StateContainer Duplicate Registration ⚠️ URGENT **Status**: Not Started **Priority**: P0 **Estimated Time**: 5 minutes **Problem**: Program.cs registers StateContainer twice with different lifetimes (lines 41-42). **Current Code**: ```csharp builder.Services.AddScoped(); // Server-side builder.Services.AddSingleton();//Client-side ``` **Issue**: This creates two separate service registrations, leading to unpredictable behavior. **Fix**: ```csharp // For Blazor Server (recommended) builder.Services.AddScoped(); // OR if you need singleton (shared across all users - less common) // builder.Services.AddSingleton(); ``` **Recommendation**: Use `Scoped` for Blazor Server to maintain state per user connection. **Files to Modify**: - `WebApp/Program.cs` (line 41-42) --- ### 3. Add CSRF Protection to Forms **Status**: Not Started **Priority**: P0 **Estimated Time**: 15 minutes **Problem**: Forms don't explicitly include antiforgery tokens. **Fix**: ```razor ``` **Apply to all forms in**: - `WebApp/Components/Login.razor` - `WebApp/Components/Pages/StudentPages/*.razor` - `WebApp/Components/Pages/TeamPages/*.razor` - `WebApp/Components/Pages/EventDefinitionPages/*.razor` --- ## 🟠 HIGH Priority - Code Quality ### 4. Fix Misleading Property Names **Status**: Not Started **Priority**: P1 **Estimated Time**: 15 minutes **Problem**: StateContainer has property named `UserId` that actually stores `scheduledTeams`. **Current Code**: ```csharp public class StateContainer { private int[]? _scheduledTeams; public int[] UserId // Misleading name! { get => _scheduledTeams ?? []; set { _scheduledTeams = value; NotifyStateChanged(); } } } ``` **Fix**: ```csharp public class StateContainer { private int[]? _scheduledTeams; public int[] ScheduledTeams // Correct, descriptive name { get => _scheduledTeams ?? []; set { _scheduledTeams = value; NotifyStateChanged(); } } public event Action? OnChange; private void NotifyStateChanged() => OnChange?.Invoke(); } ``` **Files to Modify**: - `WebApp/ChapterSettings.cs` - All files that use `StateContainer.UserId` (search and replace) --- ### 5. Add Structured Logging **Status**: Not Started **Priority**: P1 **Estimated Time**: 1 hour **Add Serilog for better logging**: ```bash dotnet add package Serilog.AspNetCore dotnet add package Serilog.Sinks.Console dotnet add package Serilog.Sinks.File ``` ```csharp // Program.cs - Add at the top using Serilog; // Replace builder creation var builder = WebApplication.CreateBuilder(args); // Add Serilog builder.Host.UseSerilog((context, configuration) => configuration .ReadFrom.Configuration(context.Configuration) .Enrich.FromLogContext() .Enrich.WithMachineName() .Enrich.WithThreadId() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}") .WriteTo.File( path: "logs/webapp-.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")); ``` ```json // appsettings.json - Add Serilog configuration { "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "Microsoft.AspNetCore": "Warning", "System": "Warning" } } } } ``` **Benefits**: - Structured log output for parsing/analysis - File rotation with retention policies - Console and file output - Performance optimizations **Files to Modify**: - `WebApp/Program.cs` - `WebApp/appsettings.json` - `WebApp/appsettings.Development.json` --- ### 6. Add Global Error Handling **Status**: Not Started **Priority**: P1 **Estimated Time**: 30 minutes **Create ErrorBoundary Component**: ```razor @using Microsoft.AspNetCore.Components.Web @ChildContent An Error Occurred @if (ShowDetails) { Error: @ex.Message Stack Trace:
@ex.StackTrace
} else { Something went wrong. Please try refreshing the page or contact support if the problem persists. }
Return to Home
@code { [Parameter] public RenderFragment? ChildContent { get; set; } [Inject] private IWebHostEnvironment Environment { get; set; } = default!; [Inject] private NavigationManager Navigation { get; set; } = default!; [Inject] private ILogger Logger { get; set; } = default!; private bool ShowDetails => Environment.IsDevelopment(); protected override void OnParametersSet() { Logger.LogError("Error boundary triggered"); } } ``` **Wrap App.razor**: ```razor ``` **Files to Create**: - `WebApp/Components/Shared/AppErrorBoundary.razor` **Files to Modify**: - `WebApp/Components/App.razor` --- ### 7. Enhance Input Validation **Status**: Not Started **Priority**: P1 **Estimated Time**: 20 minutes **Strengthen validation attributes**: ```csharp // Login.razor private class LoginModel { [Required(ErrorMessage = "Email is required")] [EmailAddress(ErrorMessage = "Invalid email format")] [MaxLength(100, ErrorMessage = "Email must be less than 100 characters")] [RegularExpression(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", ErrorMessage = "Please enter a valid email address")] public string Email { get; set; } = string.Empty; [Required(ErrorMessage = "Password is required")] [MinLength(8, ErrorMessage = "Password must be at least 8 characters")] [MaxLength(100, ErrorMessage = "Password must be less than 100 characters")] [DataType(DataType.Password)] public string Password { get; set; } = string.Empty; public bool RememberMe { get; set; } } ``` **Apply similar validation to**: - Student creation/edit forms - Team creation/edit forms - Event definition forms **Files to Modify**: - `WebApp/Components/Login.razor` - `WebApp/Components/Pages/StudentPages/Create.razor` - `WebApp/Components/Pages/StudentPages/Edit.razor` - `WebApp/Components/Pages/TeamPages/Create.razor` - `WebApp/Components/Pages/TeamPages/Edit.razor` --- ## 🟡 MEDIUM Priority - Architecture ### 8. Add Health Checks **Status**: Not Started **Priority**: P2 **Estimated Time**: 45 minutes **Add health check endpoints**: ```bash dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore ``` ```csharp // Program.cs builder.Services.AddHealthChecks() .AddDbContextCheck( name: "Database", failureStatus: HealthStatus.Unhealthy, tags: new[] { "db", "sql", "sqlite" }) .AddCheck("Authentication Service", () => { // Check if auth service is configured var authSettings = builder.Configuration.GetSection("Authentication"); return authSettings.Exists() ? HealthCheckResult.Healthy("Authentication configured") : HealthCheckResult.Degraded("Authentication may not be fully configured"); }, tags: new[] { "auth" }) .AddCheck("Rate Limiter", () => { // Check if rate limiter is working return HealthCheckResult.Healthy("Rate limiter active"); }, tags: new[] { "security" }); // After app.Build() app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; var result = JsonSerializer.Serialize(new { status = report.Status.ToString(), checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.ToString() }), totalDuration = report.TotalDuration.ToString() }); await context.Response.WriteAsync(result); } }); // Simpler endpoint for Kubernetes/Docker app.MapHealthChecks("/health/ready"); app.MapHealthChecks("/health/live"); ``` **Endpoints**: - `/health` - Full health check with JSON response - `/health/ready` - Readiness probe (200 if ready) - `/health/live` - Liveness probe (200 if alive) **Files to Modify**: - `WebApp/Program.cs` --- ### 9. Add Response Caching **Status**: Not Started **Priority**: P2 **Estimated Time**: 30 minutes ```csharp // Program.cs builder.Services.AddResponseCaching(); builder.Services.AddOutputCache(options => { options.AddBasePolicy(builder => builder.Cache()); // Cache event list for 5 minutes options.AddPolicy("EventList", builder => builder .Expire(TimeSpan.FromMinutes(5)) .Tag("events")); // Cache student list for 2 minutes options.AddPolicy("StudentList", builder => builder .Expire(TimeSpan.FromMinutes(2)) .Tag("students")); }); // After UseRouting() app.UseResponseCaching(); app.UseOutputCache(); ``` **Apply to pages**: ```razor @* EventDefinitionPages/Index.razor *@ @attribute [OutputCache(PolicyName = "EventList")] ``` **Files to Modify**: - `WebApp/Program.cs` - `WebApp/Components/Pages/EventDefinitionPages/Index.razor` - `WebApp/Components/Pages/StudentPages/Index.razor` - `WebApp/Components/Pages/TeamPages/Index.razor` --- ### 10. Implement Repository Pattern **Status**: Not Started **Priority**: P2 **Estimated Time**: 4 hours **Create repository interfaces and implementations**: ```csharp // Repositories/IRepository.cs public interface IRepository where T : class { Task> GetAllAsync(); Task GetByIdAsync(int id); Task CreateAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(int id); Task ExistsAsync(int id); } // Repositories/IStudentRepository.cs public interface IStudentRepository : IRepository { Task> GetStudentsWithTeamsAsync(); Task> GetStudentsWithRankingsAsync(); Task GetStudentDetailsAsync(int id); Task> SearchByNameAsync(string searchTerm); } // Repositories/StudentRepository.cs public class StudentRepository : IStudentRepository { private readonly AppDbContext _context; private readonly ILogger _logger; public StudentRepository(AppDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task> GetAllAsync() { return await _context.Students .AsNoTracking() .OrderBy(s => s.LastName) .ThenBy(s => s.FirstName) .ToListAsync(); } public async Task GetByIdAsync(int id) { return await _context.Students.FindAsync(id); } public async Task> GetStudentsWithTeamsAsync() { return await _context.Students .Include(s => s.Teams) .ThenInclude(t => t.Event) .AsNoTracking() .ToListAsync(); } public async Task> GetStudentsWithRankingsAsync() { return await _context.Students .Include(s => s.EventRankings) .ThenInclude(er => er.EventDefinition) .AsNoTracking() .ToListAsync(); } public async Task GetStudentDetailsAsync(int id) { return await _context.Students .Include(s => s.Teams) .ThenInclude(t => t.Event) .Include(s => s.EventRankings) .ThenInclude(er => er.EventDefinition) .FirstOrDefaultAsync(s => s.Id == id); } public async Task CreateAsync(Student student) { _context.Students.Add(student); await _context.SaveChangesAsync(); _logger.LogInformation("Created student {StudentId}: {Name}", student.Id, student.Name); return student; } public async Task UpdateAsync(Student student) { _context.Entry(student).State = EntityState.Modified; await _context.SaveChangesAsync(); _logger.LogInformation("Updated student {StudentId}: {Name}", student.Id, student.Name); } public async Task DeleteAsync(int id) { var student = await _context.Students.FindAsync(id); if (student != null) { _context.Students.Remove(student); await _context.SaveChangesAsync(); _logger.LogInformation("Deleted student {StudentId}", id); } } public async Task ExistsAsync(int id) { return await _context.Students.AnyAsync(s => s.Id == id); } public async Task> SearchByNameAsync(string searchTerm) { return await _context.Students .Where(s => s.FirstName.Contains(searchTerm) || s.LastName.Contains(searchTerm)) .AsNoTracking() .ToListAsync(); } } ``` **Register in Program.cs**: ```csharp builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); ``` **Files to Create**: - `WebApp/Repositories/IRepository.cs` - `WebApp/Repositories/IStudentRepository.cs` - `WebApp/Repositories/StudentRepository.cs` - `WebApp/Repositories/IEventDefinitionRepository.cs` - `WebApp/Repositories/EventDefinitionRepository.cs` - `WebApp/Repositories/ITeamRepository.cs` - `WebApp/Repositories/TeamRepository.cs` **Files to Modify**: - `WebApp/Program.cs` - All page files that currently use `AppDbContext` directly --- ### 11. Add API Versioning **Status**: Not Started **Priority**: P2 **Estimated Time**: 1 hour ```bash dotnet add package Asp.Versioning.Mvc ``` ```csharp // Program.cs builder.Services.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified = true; options.ReportApiVersions = true; options.ApiVersionReader = new UrlSegmentApiVersionReader(); }).AddMvc(); ``` **Files to Modify**: - `WebApp/Program.cs` - Any API controllers you create --- ## 🟢 LOW Priority - Nice to Have ### 12. Add Swagger/OpenAPI Documentation **Status**: Not Started **Priority**: P3 **Estimated Time**: 30 minutes ```bash dotnet add package Swashbuckle.AspNetCore ``` ```csharp // Program.cs builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "TSA Chapter Organizer API", Version = "v1", Description = "API for managing TSA chapter events, students, and teams", Contact = new OpenApiContact { Name = "TSA Chapter Organizer", Email = "support@example.com" } }); // Include XML comments if available var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) { c.IncludeXmlComments(xmlPath); } // Add authentication c.AddSecurityDefinition("Cookie", new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Cookie, Name = "TSA.Auth" }); }); // Only in development if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "TSA Chapter Organizer API v1"); c.RoutePrefix = "api-docs"; }); } ``` **Access**: Navigate to `/api-docs` in development **Files to Modify**: - `WebApp/Program.cs` - `WebApp/WebApp.csproj` (enable XML documentation) --- ### 13. Add Request/Response Compression **Status**: Not Started **Priority**: P3 **Estimated Time**: 15 minutes ```csharp // Program.cs builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/json", "text/html", "text/css", "application/javascript" }); }); builder.Services.Configure(options => { options.Level = CompressionLevel.Fastest; }); builder.Services.Configure(options => { options.Level = CompressionLevel.Optimal; }); // Add early in middleware pipeline app.UseResponseCompression(); ``` **Files to Modify**: - `WebApp/Program.cs` --- ### 14. Add Database Migration Check on Startup **Status**: Not Started **Priority**: P3 **Estimated Time**: 20 minutes ```csharp // Program.cs - After app.Build(), before app.Run() using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; var logger = services.GetRequiredService>(); try { var context = services.GetRequiredService(); if (app.Environment.IsDevelopment()) { // Auto-apply migrations in development logger.LogInformation("Applying database migrations (Development)..."); await context.Database.MigrateAsync(); logger.LogInformation("Database migrations applied successfully"); } else { // Check if migrations are needed in production var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); if (pendingMigrations.Any()) { logger.LogError( "Database has pending migrations: {Migrations}", string.Join(", ", pendingMigrations)); throw new InvalidOperationException( "Database migrations are pending. Run migrations before starting the application."); } logger.LogInformation("Database is up to date"); } } catch (Exception ex) { logger.LogError(ex, "An error occurred while checking database migrations"); if (!app.Environment.IsDevelopment()) { throw; // Fail fast in production } } } ``` **Files to Modify**: - `WebApp/Program.cs` --- ### 15. Add Rate Limiting Middleware **Status**: Not Started **Priority**: P3 **Estimated Time**: 30 minutes **.NET 7+ has built-in rate limiting**: ```csharp // Program.cs builder.Services.AddRateLimiter(options => { // General API rate limit options.AddFixedWindowLimiter("api", limiterOptions => { limiterOptions.PermitLimit = 100; limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiterOptions.QueueLimit = 10; }); // Stricter limit for authentication endpoints options.AddSlidingWindowLimiter("auth", limiterOptions => { limiterOptions.PermitLimit = 10; limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.SegmentsPerWindow = 2; limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiterOptions.QueueLimit = 2; }); options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { await context.HttpContext.Response.WriteAsync( $"Too many requests. Please try again after {retryAfter.TotalSeconds} seconds.", token); } else { await context.HttpContext.Response.WriteAsync( "Too many requests. Please try again later.", token); } }; }); // After UseRouting() app.UseRateLimiter(); ``` **Apply to controllers**: ```csharp [EnableRateLimiting("auth")] public class AuthController : Controller { // ... } ``` **Files to Modify**: - `WebApp/Program.cs` - `WebApp/Authentication/AuthController.cs` --- ### 16. Improve Component Organization **Status**: Not Started **Priority**: P3 **Estimated Time**: 2 hours **Reorganize into feature folders**: ``` WebApp/Components/ ├── Features/ │ ├── Students/ │ │ ├── StudentList.razor │ │ ├── StudentForm.razor │ │ ├── StudentDetails.razor │ │ ├── StudentEventRanking.razor │ │ └── StudentSelector.razor │ ├── Teams/ │ │ ├── TeamList.razor │ │ ├── TeamForm.razor │ │ ├── TeamDetails.razor │ │ ├── TeamScheduler.razor │ │ ├── TeamSelector.razor │ │ └── TeamPrintout.razor │ ├── Events/ │ │ ├── EventList.razor │ │ ├── EventForm.razor │ │ ├── EventDetails.razor │ │ ├── EventAttributes.razor │ │ └── EventPrintout.razor │ ├── MeetingSchedule/ │ │ ├── MeetingScheduleIndex.razor │ │ ├── ScheduledTeamsList.razor │ │ └── UnscheduledStudentsList.razor │ └── Authentication/ │ └── Login.razor ├── Shared/ │ ├── Components/ │ │ ├── CrudActions.razor │ │ └── ErrorBoundary.razor │ └── Layout/ │ ├── MainLayout.razor │ ├── EmptyLayout.razor │ └── NavMenu.razor └── Pages/ ├── Home.razor ├── Import.razor ├── Legend.razor └── Error.razor ``` **Benefits**: - Better organization by feature - Easier to find related components - Clearer responsibility boundaries - Easier onboarding for new developers **This is a large refactoring - consider doing incrementally** --- ### 17. Add Client-Side Validation Feedback **Status**: Not Started **Priority**: P3 **Estimated Time**: 30 minutes **Improve MudBlazor validation UX**: ```razor ``` **Add custom validators**: ```csharp public class StrongPasswordAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { var password = value as string; if (string.IsNullOrEmpty(password)) return ValidationResult.Success; var hasUpperCase = password.Any(char.IsUpper); var hasLowerCase = password.Any(char.IsLower); var hasDigit = password.Any(char.IsDigit); if (!hasUpperCase || !hasLowerCase || !hasDigit) { return new ValidationResult( "Password must contain at least one uppercase letter, one lowercase letter, and one digit"); } return ValidationResult.Success; } } ``` **Files to Modify**: - All form components - Add `Validators/StrongPasswordAttribute.cs` --- ## 📊 Implementation Roadmap ### Week 1: Critical Security (P0) - [ ] Day 1: Fix login credential exposure (#1) - [ ] Day 1: Fix StateContainer duplicate registration (#2) - [ ] Day 2: Add CSRF protection (#3) - [ ] Day 2-3: Fix misleading property names (#4) - [ ] Day 4-5: Add structured logging (#5) ### Week 2: High Priority Quality (P1) - [ ] Day 1: Add global error handling (#6) - [ ] Day 2: Enhance input validation (#7) - [ ] Day 3-4: Add health checks (#8) - [ ] Day 5: Add response caching (#9) ### Week 3-4: Architecture Improvements (P2) - [ ] Days 1-3: Implement repository pattern (#10) - [ ] Day 4: Add API versioning (#11) ### Future Iterations: Nice to Have (P3) - [ ] Add Swagger/OpenAPI (#12) - [ ] Add compression (#13) - [ ] Database migration checks (#14) - [ ] Rate limiting middleware (#15) - [ ] Component reorganization (#16) - [ ] Client-side validation (#17) --- ## 🎯 Success Metrics ### Security - ✅ No credentials in logs or browser history - ✅ All forms protected with CSRF tokens - ✅ Rate limiting prevents brute force attacks ### Performance - ⏱️ Page load times reduced by 30% (caching) - ⏱️ Reduced database queries (repository pattern) - ⏱️ Smaller payload sizes (compression) ### Code Quality - 📊 No misleading variable names - 📊 Structured logging for debugging - 📊 Clear error messages for users - 📊 Consistent validation across forms ### Maintainability - 🔧 Repository pattern isolates data access - 🔧 Error boundaries prevent cascading failures - 🔧 Health checks enable monitoring - 🔧 Organized component structure --- ## 📝 Notes - All changes should be tested in development before deploying to production - Consider creating feature branches for larger changes (repository pattern, reorganization) - Update documentation as features are implemented - Add unit tests for new repository classes - Monitor logs after implementing structured logging - Review health check endpoints with DevOps team --- **Document Version**: 1.0 **Last Updated**: 2025-12-02 **Next Review**: After P0 items are completed