diff --git a/ContosoUniversity/.github/modernize/ContosoUniversity/plan.md b/ContosoUniversity/.github/modernize/ContosoUniversity/plan.md new file mode 100644 index 00000000..c08c4eb6 --- /dev/null +++ b/ContosoUniversity/.github/modernize/ContosoUniversity/plan.md @@ -0,0 +1,281 @@ +# Modernization Plan: ContosoUniversity + +## Overview + +This plan modernizes the **ContosoUniversity** ASP.NET MVC application from **.NET Framework 4.8** to **.NET 10** and migrates it to Azure cloud services. The plan is generated from the AppCAT assessment report (`report-20260514104511`) which identified **6 issue categories** with **25 total incidents** across the codebase, plus a user-requested .NET version upgrade. + +| Property | Value | +|---|---| +| **Application** | ContosoUniversity | +| **Current Framework** | .NET Framework 4.8 (ASP.NET MVC) | +| **Target Framework** | .NET 10 (ASP.NET Core) | +| **Language** | C# | +| **Build Tool** | MSBuild | +| **Total Issues** | 7 (6 from assessment + 1 user-requested) | +| **Total Incidents** | 25+ | +| **Estimated Effort** | 75+ story points | + +--- + +## Task Summary + +| # | Task | Category | Severity | Incidents | Skill | +|---|---|---|---|---|---| +| 1 | Upgrade .NET Framework 4.8 β†’ .NET 10 | Framework Upgrade | mandatory | β€” | `dotnet-upgrade` | +| 2 | Migrate MSMQ to Azure Service Bus | Queue | mandatory | 12 | `dotnet-azure-servicebus` | +| 3 | Migrate Windows Auth to Managed Identity | Identity | mandatory | 1 | `dotnet-managed-identity` | +| 4 | Migrate local file I/O to Azure Storage | Local | potential | 8 | `dotnet-azure-storage-blob` | +| 5 | Migrate secrets to Azure Key Vault | Security | optional | 2 | `dotnet-azure-keyvault-secret` | +| 6 | Configure Azure SQL Database | Database | potential | 1 | `dotnet-azure-sql-database` | +| 7 | Move static content to Azure CDN | Scale | optional | 1 | `manual` | + +--- + +## Task Details + +### Task 1: Upgrade .NET Framework 4.8 to .NET 10 + +**Priority:** πŸ”΄ Mandatory β€” Must be completed first +**Effort:** High +**Category:** Framework Upgrade (user-requested) + +**Description:** +Upgrade the application from .NET Framework 4.8 (ASP.NET MVC 5) to .NET 10 (ASP.NET Core). This is a prerequisite for all other modernization tasks, as the Azure SDK libraries and modern authentication patterns target .NET Core/.NET 5+. + +**Scope:** +- Convert `ContosoUniversity.csproj` from legacy format to SDK-style project +- Migrate from `packages.config` to `` format +- Replace ASP.NET MVC 5 controllers/views with ASP.NET Core MVC patterns +- Replace `Web.config` with `appsettings.json` and `Program.cs` configuration +- Replace `Global.asax` with ASP.NET Core middleware pipeline +- Update Entity Framework 6 to Entity Framework Core +- Update all NuGet packages to .NET 10-compatible versions +- Replace `System.Web` dependencies with ASP.NET Core equivalents +- Update Razor views for ASP.NET Core Tag Helpers + +**Files Affected:** +- `ContosoUniversity.csproj` β€” Project file conversion +- `packages.config` β€” Remove (migrate to PackageReference) +- `Web.config` β€” Replace with `appsettings.json` +- `Global.asax` / `Global.asax.cs` β€” Replace with `Program.cs` +- `Controllers/*.cs` β€” Update base classes and namespaces +- `Views/**/*.cshtml` β€” Update Razor syntax +- `Models/*.cs` β€” Update EF annotations +- `Data/*.cs` β€” Migrate to EF Core DbContext +- `App_Start/*.cs` β€” Migrate to middleware configuration + +**Success Criteria:** +- Application compiles on .NET 10 +- All existing functionality preserved +- EF Core migrations work against the database + +--- + +### Task 2: Migrate MSMQ to Azure Service Bus + +**Priority:** πŸ”΄ Mandatory +**Effort:** 3 story points per incident Γ— 12 incidents = 36 story points +**Category:** Queue +**Rule:** Queue.0003 β€” MSMQ usage detected + +**Description:** +The application uses `System.Messaging.MessageQueue` (MSMQ) for notification processing. MSMQ is not supported on Azure App Service or containerized deployments. Migrate to Azure Service Bus for reliable cloud-native messaging. + +**Scope:** +- Replace `System.Messaging.MessageQueue` with `Azure.Messaging.ServiceBus.ServiceBusClient` +- Replace `System.Messaging.XmlMessageFormatter` with JSON serialization +- Replace `System.Messaging.MessageQueueAccessRights` with Azure Service Bus RBAC +- Configure Service Bus connection via Managed Identity (DefaultAzureCredential) +- Update notification send/receive patterns to use Service Bus SDK + +**Files Affected:** +- `Services/NotificationService.cs` β€” Lines 19, 21, 22, 26, 30 (all 12 MSMQ incidents) + +**References:** +- [Azure Service Bus queues](https://go.microsoft.com/fwlink/?LinkID=2243266) +- [Azure Queue Storage vs Service Bus comparison](https://go.microsoft.com/fwlink/?LinkID=2249263) + +**Success Criteria:** +- All MSMQ references removed +- Notifications sent and received via Azure Service Bus +- Authentication uses DefaultAzureCredential + +--- + +### Task 3: Migrate Authentication to Managed Identity + +**Priority:** πŸ”΄ Mandatory +**Effort:** 3 story points +**Category:** Identity +**Rule:** Identity.0002 β€” Windows authentication detected + +**Description:** +The application uses connection-string-based authentication which includes Windows authentication patterns not supported on Azure App Service, ACA, or AKS. Migrate to Azure Managed Identity with `DefaultAzureCredential` for all Azure service connections. + +**Scope:** +- Replace connection string authentication with `DefaultAzureCredential` +- Configure Azure SQL connection to use Managed Identity token-based auth +- Ensure all Azure SDK clients (Service Bus, Storage, Key Vault) use `DefaultAzureCredential` + +**Files Affected:** +- `Web.config` β†’ `appsettings.json` β€” Connection string configuration + +**References:** +- [Azure App Service authentication](https://go.microsoft.com/fwlink/?LinkID=2242883) + +**Success Criteria:** +- No passwords or secrets in connection strings +- All Azure services accessed via Managed Identity + +--- + +### Task 4: Migrate Local File I/O to Azure Storage + +**Priority:** 🟑 Potential +**Effort:** 3 story points per incident Γ— 8 incidents = 24 story points +**Category:** Local +**Rule:** Local.0003 β€” Local or network IO operations detected + +**Description:** +The application uses `System.IO.File` and `System.IO.Directory` for file operations (teaching material uploads). Local file system access may not be persistent or scalable on Azure App Service. Migrate to Azure Blob Storage or Azure Storage mount paths. + +**Scope:** +- Replace `System.IO.Directory` calls with Azure Blob Storage container operations or mounted storage paths +- Replace `System.IO.File` calls with blob upload/download operations +- Update file upload handling in `CoursesController` to use `BlobServiceClient` +- Configure storage access via Managed Identity + +**Files Affected:** +- `Controllers/CoursesController.cs` β€” Lines 76, 78, 159, 161 (`System.IO.Directory`) +- `Controllers/CoursesController.cs` β€” Lines 172, 174, 229, 233 (`System.IO.File`) + +**References:** +- [Azure Blob Storage](https://go.microsoft.com/fwlink/?linkid=2250574) +- [Azure File Shares](https://go.microsoft.com/fwlink/?LinkID=2242591) +- [Storage mounts for Managed Instance](https://go.microsoft.com/fwlink/?linkid=2346952) + +**Success Criteria:** +- All file operations use Azure Blob Storage or mounted storage +- File uploads persist across app restarts and scale-out +- Authentication uses DefaultAzureCredential + +--- + +### Task 5: Migrate Secrets to Azure Key Vault + +**Priority:** 🟒 Optional (recommended) +**Effort:** 3 story points per incident Γ— 2 incidents = 6 story points +**Category:** Security +**Rule:** Security.0002 β€” Connection strings without configuration builders detected + +**Description:** +The application stores connection strings and app settings directly in `Web.config` without configuration builders. This is a security risk and violates compliance standards (PCI DSS, GDPR). Migrate secrets to Azure Key Vault. + +**Scope:** +- Move `` values to Azure Key Vault +- Move `` secrets to Azure Key Vault +- Configure ASP.NET Core to load secrets from Key Vault via `Azure.Extensions.AspNetCore.Configuration.Secrets` +- Access Key Vault via Managed Identity + +**Files Affected:** +- `Web.config` β†’ `appsettings.json` β€” `` section +- `Web.config` β†’ `appsettings.json` β€” `` section + +**References:** +- [Configuration builders](https://go.microsoft.com/fwlink/?LinkID=2250915) +- [Storing application secrets](https://go.microsoft.com/fwlink/?LinkID=2250916) +- [Centralized app configuration and security](https://go.microsoft.com/fwlink/?LinkID=2250733) + +**Success Criteria:** +- No secrets stored in configuration files or source code +- All secrets retrieved from Azure Key Vault at runtime +- Key Vault access uses DefaultAzureCredential + +--- + +### Task 6: Configure Azure SQL Database + +**Priority:** 🟑 Potential +**Effort:** 3 story points +**Category:** Database +**Rule:** Database.0002 β€” SQL database connection detected + +**Description:** +Ensure the SQL database is available on Azure. Migrate the on-premises SQL Server database to Azure SQL Database and update connection configuration for cloud deployment. + +**Scope:** +- Update `DefaultConnection` connection string for Azure SQL Database +- Configure Entity Framework Core to connect to Azure SQL with Managed Identity +- Validate database schema compatibility with Azure SQL + +**Files Affected:** +- `Web.config` β†’ `appsettings.json` β€” `DefaultConnection` connection string +- `Data/*.cs` β€” DbContext configuration + +**References:** +- [Migrate SQL Server database to Azure](https://go.microsoft.com/fwlink/?LinkID=2251731) +- [Azure SQL Managed Instance](https://go.microsoft.com/fwlink/?LinkID=2251613) +- [Azure Migrate](https://go.microsoft.com/fwlink/?linkid=2252410) + +**Success Criteria:** +- Application connects to Azure SQL Database +- Connection uses Managed Identity (no password in connection string) +- All queries and migrations work on Azure SQL + +--- + +### Task 7: Move Static Content to Azure CDN + +**Priority:** 🟒 Optional +**Effort:** 3 story points +**Category:** Scale +**Rule:** Scale.0001 β€” Static content detected + +**Description:** +The application bundles 16 static files (CSS, JavaScript, images) directly in the project. Serving static content from the application increases costs, reduces performance, and requires redeployment for content changes. Consider offloading to Azure Blob Storage with Azure CDN. + +**Scope:** +- Move static files (Content/, Scripts/, favicon.ico) to Azure Blob Storage +- Configure Azure CDN for global content delivery +- Update Razor views to reference CDN URLs +- Keep local fallback for development + +**Files Affected:** +- `Content/bootstrap.css`, `Content/bootstrap.min.css`, `Content/Site.css` +- `Scripts/bootstrap.js`, `Scripts/bootstrap.min.js` +- `Scripts/jquery-*.js`, `Scripts/jquery.validate*.js` +- `Scripts/modernizr-2.6.2.js`, `Scripts/respond.js`, `Scripts/respond.min.js` +- `favicon.ico` +- `Uploads/TeachingMaterials/*` + +**References:** +- [Azure Blob Storage](https://go.microsoft.com/fwlink/?linkid=2250574) +- [Azure CDN](https://go.microsoft.com/fwlink/?linkid=2250392) + +**Success Criteria:** +- Static files served via CDN +- Improved load times for end users +- Content updates possible without app redeployment + +--- + +## Execution Order + +The tasks should be executed in the following dependency order: + +``` +Task 1: .NET Upgrade (prerequisite for all Azure SDK tasks) + β”œβ”€β”€ Task 2: MSMQ β†’ Azure Service Bus (mandatory) + β”œβ”€β”€ Task 3: Managed Identity (mandatory, enables other Azure tasks) + β”‚ β”œβ”€β”€ Task 4: Local I/O β†’ Azure Storage + β”‚ β”œβ”€β”€ Task 5: Secrets β†’ Azure Key Vault + β”‚ └── Task 6: Azure SQL Database + └── Task 7: Static Content β†’ CDN (independent, optional) +``` + +## Assessment Source + +- **Report:** `.github/modernize/assessment/reports/report-20260514104511/report.json` +- **Producer:** .NET AppCAT CLI v1.0.0 +- **Analysis Date:** 2026-05-14 +- **Target Platforms:** Azure App Service, AKS, ACA, App Service Container, App Service Managed Instance diff --git a/ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json b/ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json new file mode 100644 index 00000000..87e79e9d --- /dev/null +++ b/ContosoUniversity/.github/modernize/ContosoUniversity/tasks.json @@ -0,0 +1,315 @@ +{ + "tasks": [ + { + "id": "task-1", + "type": "upgrade", + "title": "Upgrade .NET Framework 4.8 to .NET 10", + "description": "Convert the ASP.NET MVC 5 application from .NET Framework 4.8 to ASP.NET Core on .NET 10. Includes project file conversion, package migration, middleware pipeline setup, EF Core migration, and Razor view updates.", + "skill": "builtin:dotnet-upgrade", + "category": "Framework Upgrade", + "severity": "mandatory", + "target": { + "current_framework": ".NETFramework,Version=v4.8", + "target_framework": "net10.0" + }, + "incidents": [], + "filesAffected": [ + "ContosoUniversity.csproj", + "packages.config", + "Web.config", + "Global.asax", + "Global.asax.cs", + "Controllers/*.cs", + "Views/**/*.cshtml", + "Models/*.cs", + "Data/*.cs", + "App_Start/*.cs" + ], + "dependencies": [], + "successCriteria": "Application compiles and runs on .NET 10 with all existing functionality preserved", + "status": "pending" + }, + { + "id": "task-2", + "type": "cloud-migration", + "title": "Migrate MSMQ to Azure Service Bus", + "description": "Replace System.Messaging.MessageQueue (MSMQ) usage in NotificationService with Azure.Messaging.ServiceBus. Replace XmlMessageFormatter with JSON serialization and configure Service Bus access via DefaultAzureCredential.", + "skill": "dotnet-azure-servicebus", + "category": "Queue", + "severity": "mandatory", + "ruleId": "Queue.0003", + "target": { + "from": "System.Messaging.MessageQueue (MSMQ)", + "to": "Azure.Messaging.ServiceBus" + }, + "incidents": [ + { + "incidentId": "efa216b2-6628-463b-8a8d-d29eb8011944", + "location": "Services/NotificationService.cs", + "line": 21, + "snippet": "System.Messaging.MessageQueue" + }, + { + "incidentId": "8193c7d5-9eff-4feb-82bb-546821534b32", + "location": "Services/NotificationService.cs", + "line": 26, + "snippet": "System.Messaging.MessageQueue" + }, + { + "incidentId": "e34bba0e-418d-4faf-b1b4-71ecbc734765", + "location": "Services/NotificationService.cs", + "line": 30, + "snippet": "System.Messaging.XmlMessageFormatter" + }, + { + "incidentId": "63ed7e4f-3185-40b0-97f5-2f3e3e130709", + "location": "Services/NotificationService.cs", + "line": 22, + "snippet": "System.Messaging.MessageQueueAccessRights" + }, + { + "incidentId": "52843c11-b423-4efe-a607-1028b921cacb", + "location": "Services/NotificationService.cs", + "line": 19, + "snippet": "System.Messaging.MessageQueue" + } + ], + "filesAffected": [ + "Services/NotificationService.cs" + ], + "dependencies": [ + "task-1" + ], + "successCriteria": "All MSMQ references removed; notifications sent/received via Azure Service Bus with DefaultAzureCredential", + "status": "pending" + }, + { + "id": "task-3", + "type": "cloud-migration", + "title": "Migrate Authentication to Managed Identity", + "description": "Replace connection-string-based and Windows authentication with Azure Managed Identity using DefaultAzureCredential. Configure all Azure service connections to use token-based authentication.", + "skill": "dotnet-managed-identity", + "category": "Identity", + "severity": "mandatory", + "ruleId": "Identity.0002", + "target": { + "from": "Windows authentication / connection string auth", + "to": "Azure Managed Identity (DefaultAzureCredential)" + }, + "incidents": [ + { + "incidentId": "10c96015-cf9a-4e3b-85bb-4a17b0e053d8", + "location": "Web.config", + "snippet": "" + } + ], + "filesAffected": [ + "Web.config" + ], + "dependencies": [ + "task-1" + ], + "successCriteria": "No passwords or secrets in connection strings; all Azure services accessed via Managed Identity", + "status": "pending" + }, + { + "id": "task-4", + "type": "cloud-migration", + "title": "Migrate Local File I/O to Azure Storage", + "description": "Replace System.IO.File and System.IO.Directory usage in CoursesController with Azure Blob Storage or mounted storage paths. Update file upload/download handling to use BlobServiceClient with DefaultAzureCredential.", + "skill": "dotnet-azure-storage-blob", + "category": "Local", + "severity": "potential", + "ruleId": "Local.0003", + "target": { + "from": "System.IO.File / System.IO.Directory (local file system)", + "to": "Azure Blob Storage or Azure Storage mount" + }, + "incidents": [ + { + "incidentId": "a0c9305c-f7ab-4215-acac-a4e7e97380e3", + "location": "Controllers/CoursesController.cs", + "line": 76, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "cdf5de15-2709-46ca-89ad-612ac7d493c9", + "location": "Controllers/CoursesController.cs", + "line": 78, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "8e42661a-16ee-4e47-a4ce-ba0dad6c9e46", + "location": "Controllers/CoursesController.cs", + "line": 159, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "7514c406-42ec-4fc6-94a4-d6ea973cfa51", + "location": "Controllers/CoursesController.cs", + "line": 161, + "snippet": "System.IO.Directory" + }, + { + "incidentId": "8e33f3b8-d19b-41a0-a48d-7ca952ac622b", + "location": "Controllers/CoursesController.cs", + "line": 172, + "snippet": "System.IO.File" + }, + { + "incidentId": "4dbc4e16-071c-4659-b7a0-ccb8695b7100", + "location": "Controllers/CoursesController.cs", + "line": 174, + "snippet": "System.IO.File" + }, + { + "incidentId": "f35a8b1d-69e0-4bc1-b660-615b41d5ab9d", + "location": "Controllers/CoursesController.cs", + "line": 229, + "snippet": "System.IO.File" + }, + { + "incidentId": "df2634d6-58ed-469b-8094-bd5897ab5fbb", + "location": "Controllers/CoursesController.cs", + "line": 233, + "snippet": "System.IO.File" + } + ], + "filesAffected": [ + "Controllers/CoursesController.cs" + ], + "dependencies": [ + "task-1", + "task-3" + ], + "successCriteria": "All file operations use Azure Blob Storage or mounted storage; files persist across app restarts and scale-out", + "status": "pending" + }, + { + "id": "task-5", + "type": "cloud-migration", + "title": "Migrate Secrets to Azure Key Vault", + "description": "Move connection strings and app settings from Web.config to Azure Key Vault. Configure ASP.NET Core to load secrets from Key Vault via Azure.Extensions.AspNetCore.Configuration.Secrets with DefaultAzureCredential.", + "skill": "dotnet-azure-keyvault-secret", + "category": "Security", + "severity": "optional", + "ruleId": "Security.0002", + "target": { + "from": "Secrets in Web.config (appSettings, connectionStrings)", + "to": "Azure Key Vault" + }, + "incidents": [ + { + "incidentId": "6c136b01-e007-4147-ba6d-ee5362412ee8", + "location": "Web.config", + "snippet": "" + }, + { + "incidentId": "517dbf2e-9f4e-4e10-9121-f828907d94fc", + "location": "Web.config", + "snippet": "" + } + ], + "filesAffected": [ + "Web.config" + ], + "dependencies": [ + "task-1", + "task-3" + ], + "successCriteria": "No secrets in configuration files or source code; all secrets retrieved from Azure Key Vault at runtime", + "status": "pending" + }, + { + "id": "task-6", + "type": "cloud-migration", + "title": "Configure Azure SQL Database", + "description": "Migrate the SQL Server database connection to Azure SQL Database. Update DefaultConnection configuration and configure Entity Framework Core to use Managed Identity for Azure SQL authentication.", + "skill": "dotnet-azure-sql-database", + "category": "Database", + "severity": "potential", + "ruleId": "Database.0002", + "target": { + "from": "On-premises SQL Server with connection string", + "to": "Azure SQL Database with Managed Identity" + }, + "incidents": [ + { + "incidentId": "764b27bd-807d-44bb-ba6f-add7904229d8", + "location": "Web.config", + "snippet": "" + } + ], + "filesAffected": [ + "Web.config", + "Data/SchoolContext.cs" + ], + "dependencies": [ + "task-1", + "task-3" + ], + "successCriteria": "Application connects to Azure SQL Database via Managed Identity; all queries and migrations work on Azure SQL", + "status": "pending" + }, + { + "id": "task-7", + "type": "optimization", + "title": "Move Static Content to Azure CDN", + "description": "Move static files (CSS, JavaScript, images) from the application to Azure Blob Storage with Azure CDN for improved performance, reduced costs, and independent content updates.", + "skill": "manual", + "category": "Scale", + "severity": "optional", + "ruleId": "Scale.0001", + "target": { + "from": "Static files bundled in project (Content/, Scripts/)", + "to": "Azure Blob Storage + Azure CDN" + }, + "incidents": [ + { + "incidentId": "f0e3c2b1-0a47-4046-b5bd-ff21956d2f4f", + "location": "ContosoUniversity.csproj", + "snippet": "16 static files detected (CSS, JS, images)" + } + ], + "filesAffected": [ + "Content/bootstrap.css", + "Content/bootstrap.min.css", + "Content/Site.css", + "Scripts/bootstrap.js", + "Scripts/bootstrap.min.js", + "Scripts/jquery-3.4.1.js", + "Scripts/jquery-3.4.1.min.js", + "Scripts/jquery.validate.js", + "Scripts/jquery.validate.min.js", + "Scripts/jquery.validate.unobtrusive.js", + "Scripts/jquery.validate.unobtrusive.min.js", + "Scripts/modernizr-2.6.2.js", + "Scripts/respond.js", + "Scripts/respond.min.js", + "favicon.ico" + ], + "dependencies": [ + "task-1" + ], + "successCriteria": "Static files served via CDN with improved load times", + "status": "pending" + } + ], + "metadata": { + "language": "dotnet", + "planName": "ContosoUniversity Modernization Plan", + "projectName": "ContosoUniversity", + "sourceFramework": ".NETFramework,Version=v4.8", + "targetFramework": "net10.0", + "assessmentReport": ".github/modernize/assessment/reports/report-20260514104511/report.json", + "createdAt": "2026-05-14T10:49:00Z", + "version": "1.0", + "totalTasks": 7, + "totalIncidents": 25, + "severityBreakdown": { + "mandatory": 3, + "potential": 2, + "optional": 2 + } + } +} diff --git a/ContosoUniversity/.github/modernize/code-migration/20260514105229/progress.md b/ContosoUniversity/.github/modernize/code-migration/20260514105229/progress.md new file mode 100644 index 00000000..f259fbaf --- /dev/null +++ b/ContosoUniversity/.github/modernize/code-migration/20260514105229/progress.md @@ -0,0 +1,196 @@ +# Migration Progress: .NET Framework 4.8 β†’ ASP.NET Core .NET 10 + +**Migration Session ID:** df0090f3-bf51-4f7b-b2bf-61beb7ab6971 +**Branch:** `appmod/dotnet-migration-20260514105229` +**Previous Branch:** `main` +**Language:** dotnet + +## Guidelines +1. When using terminal command tool, never input a long command with multiple lines, always use a single line command +2. When performing semantic or intent-based searches, DO NOT search content from .github/modernize/ folder +3. Never create a new project in the solution, always use the existing project to add new files or update the existing files +4. Minimize code changes: Update only what's necessary for the migration +5. Add New Package References to Projects, use "dotnet add package --version " + +## Progress + +- [βœ…] Migration Plan Generated ([plan.md](./.github/modernize/code-migration/20260514105229/plan.md)) +- [βœ…] Version Control Setup (branch: `appmod/dotnet-migration-20260514105229` already active) +- Code Migration + - [βœ…] `ContosoUniversity.csproj` - Replace legacy csproj with SDK-style format + - [βœ…] `Program.cs` - Create ASP.NET Core entry point + - [βœ…] `appsettings.json` - Create from Web.config + - [βœ…] `appsettings.Development.json` - Create dev settings + - [βœ…] `Views/_ViewImports.cshtml` - Create to replace Views/Web.config + - [βœ…] `Services/NotificationService.cs` - Replace MSMQ with in-memory queue + - [βœ…] `Data/SchoolContextFactory.cs` - Remove ConfigurationManager + - [βœ…] `Controllers/BaseController.cs` - Replace System.Web.Mvc, use DI + - [βœ…] `Controllers/HomeController.cs` - Update usings + - [βœ…] `Controllers/StudentsController.cs` - Update usings and API + - [βœ…] `Controllers/CoursesController.cs` - Update usings, IFormFile, WebRootPath + - [βœ…] `Controllers/InstructorsController.cs` - Update usings + - [βœ…] `Controllers/DepartmentsController.cs` - Update usings + - [βœ…] `Controllers/NotificationsController.cs` - Update usings + - [βœ…] `Views/Shared/_Layout.cshtml` - Replace bundle rendering + - [βœ…] `Views/Shared/Error.cshtml` - Remove MVC5 error model + - [βœ…] `Views/Students/Create.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Students/Edit.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Courses/Create.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Courses/Edit.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Instructors/Create.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Instructors/Edit.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Departments/Edit.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Departments/Create.cshtml` - Replace Scripts.Render + - [βœ…] `Views/Shared/_ValidationScriptsPartial.cshtml` - Create validation scripts partial + - [βœ…] Static files (wwwroot) - Move CSS/JS + - [βœ…] Delete legacy files (Global.asax, Web.config, App_Start, packages.config, etc.) +- Validation & Fixing + - [βœ…] Build and Fix (1 round - 6 errors fixed: WebApplication usings, TryUpdateModel, Scripts.Render) + - [βœ…] CVE Check (Newtonsoft.Json 13.0.3 - above vulnerable range <13.0.1; no action needed) + - [βœ…] Consistency Check (0 Critical, 0 Major, 3 Minor non-blocking issues) + - [βœ…] Test Fix (No test projects exist - 0 tests, N/A) + - [βœ…] Completeness Check (PASSED - all old technology patterns removed) + - [βœ…] Build Validation (Final) - PASSED +- [βœ…] Final Summary + - [βœ…] Final Code Commit (c390b4869d885692d593e1ac0a6186517c6a1da1) + - [βœ…] Migration Summary Generation + +## MSMQ β†’ Azure Service Bus Migration (Session 20260514105229) + +- [βœ…] Add NuGet packages: `Azure.Messaging.ServiceBus` 7.19.0, `Azure.Identity` 1.14.0 +- [βœ…] Update `appsettings.json` - Add `AzureServiceBus` section (namespace + queue name) +- [βœ…] Rewrite `Services/NotificationService.cs` - Replace in-memory ConcurrentQueue with Azure Service Bus + DefaultAzureCredential + JSON serialization +- [βœ…] Update `Controllers/NotificationsController.cs` - Use async ReceiveNotificationsAsync +- [βœ…] Update `Program.cs` - Update comment for Service Bus registration +- [βœ…] Build Verification - PASSED (0 errors, 98 pre-existing warnings) +- [βœ…] CVE Check - PASSED (Azure.Identity 1.14.0, Azure.Messaging.ServiceBus 7.19.0, Newtonsoft.Json 13.0.3 β€” all above vulnerable ranges) +- [βœ…] Consistency Check - PASSED (0 Critical, 0 Major, 0 Minor) +- [βœ…] Completeness Check - PASSED (no System.Messaging, MessageQueue, XmlMessageFormatter, ConcurrentQueue, or MSMQ config references remaining) +- [βœ…] Test Fix - N/A (no test projects) +- [βœ…] Final Commit + +## Issues Encountered (Previous Migration: .NET Framework β†’ .NET 10) +- **TryUpdateModel not available in ASP.NET Core**: Replaced with manual form field binding in InstructorsController.Edit POST action +- **Program.cs missing explicit usings**: Added `using Microsoft.AspNetCore.Builder` and other ASP.NET Core namespaces since ImplicitUsings was not enabled +- **Departments/Create.cshtml missed Scripts.Render**: Found during first build - fixed with `` +- **Duplicate content bug** (resolved): The `edit` tool caused partial replacement leaving old code appended. Fixed for CoursesController and InstructorsController by using PowerShell `Set-Content` to overwrite entire files + +--- + +## Local File I/O β†’ Azure Blob Storage Migration (Session 20260514105229) + +**KB Used:** `dotnet-azure-storage-blob` (trust: 225.19) β€” exact match: local file system storage migration + +### Guidelines +1. Use `BlobServiceClient` + `DefaultAzureCredential` (Managed Identity) β€” no connection strings or account keys +2. Container client via `blobServiceClient.GetBlobContainerClient(containerName)`; create if not exists with `CreateIfNotExists()` +3. Upload via `blobClient.Upload(stream, overwrite: true)`; delete via `blobClient.DeleteIfExists()` +4. Store blob URI (`blobClient.Uri.ToString()`) instead of local file path in `TeachingMaterialImagePath` +5. Extract blob name from stored URI using `Path.GetFileName(new Uri(path).LocalPath)` + +### Progress + +- [βœ…] Add NuGet package: `Azure.Storage.Blobs` 12.24.0 +- [βœ…] Update `appsettings.json` β€” Add `AzureStorageBlob` section (endpoint + container name) +- [βœ…] Update `Program.cs` β€” Register `BlobServiceClient` singleton with `DefaultAzureCredential` +- [βœ…] Rewrite `Controllers/CoursesController.cs` β€” Replace `System.IO.File`/`System.IO.Directory` with blob operations (8 incidents: CreateΓ—2, EditΓ—4, DeleteConfirmedΓ—2) +- [βœ…] Build Verification β€” PASSED (0 errors) +- [βœ…] CVE Check β€” PASSED (Azure.Storage.Blobs 12.24.0 above affected < 12.13.0; all others above vulnerable ranges) +- [βœ…] Consistency Check β€” PASSED (0 Critical, 0 Major β€” all file operations correctly translated to blob equivalents) +- [βœ…] Completeness Check β€” PASSED (no System.IO.File, System.IO.Directory, IWebHostEnvironment, FileStream, or local path constructions remain) +- [βœ…] Test Fix β€” N/A (no test projects; 0 tests) +- [βœ…] Final Commit (f28d6b8) + +--- + +# Migration Progress: Authentication β†’ Azure Managed Identity (DefaultAzureCredential) + +**Migration Session ID:** 20260514105229 +**Branch:** `appmod/dotnet-migration-20260514105229` +**Language:** dotnet +**KB Used:** `dotnet-managed-identity` (trust: 161.64) β€” exact match: Managed Identity migration for SQL authentication + +## Guidelines +1. Replace Windows/Integrated Security auth with Azure AD token-based auth (DefaultAzureCredential) +2. No passwords, secrets, or `Integrated Security=True` in connection strings +3. Use `Authentication=Active Directory Default` keyword in SQL connection string +4. Add `Azure.Identity` package to enable DefaultAzureCredential token acquisition + +## Progress + +- [βœ…] Migration Plan Generated ([plan.md](./.github/modernize/code-migration/20260514105229/plan.md)) +- [βœ…] Version Control Setup (branch `appmod/dotnet-migration-20260514105229` already active β€” no new branch needed) +- Code Migration + - [βœ…] `ContosoUniversity.csproj` β€” Add `Azure.Identity` 1.14.0 + `Microsoft.Data.SqlClient` 5.2.2 packages + - [βœ…] `appsettings.json` β€” Replace `Integrated Security=True` with `Authentication=Active Directory Default` + - [βœ…] `appsettings.Development.json` β€” Add dev connection string using `Authentication=Active Directory Default` +- Validation & Fixing + - [βœ…] Build and Fix (1 round β€” 0 errors, build passes cleanly) + - [βœ…] CVE Check (Azure.Identity 1.14.0 β€” all CVEs affect <1.11.4 βœ…; Microsoft.Data.SqlClient 5.2.2 β€” all CVEs affect <5.1.3 βœ…; Newtonsoft.Json 13.0.3 β€” CVE affects <13.0.1 βœ…; no action needed) + - [βœ…] Consistency Check (0 Critical, 0 Major, 1 Minor: sync-over-async in SendNotification β€” non-blocking) + - [βœ…] Test Fix (No test projects β€” 0 tests, N/A) + - [βœ…] Completeness Check (PASSED β€” no old auth patterns, Integrated Security=True, passwords, or connection string secrets remain) + - [βœ…] Build Validation (Final) β€” PASSED +- [βœ…] Final Summary + - [βœ…] Final Code Commit (43fa5c9f7937c8e91a75f19bb7e61ecbba53227a) + - [βœ…] Migration Summary Generation + +--- + +## Azure SQL Database Connection Migration (Session 20260514105229) + +**KB Used:** `dotnet-azure-sql-database` (trust: 566.80) β€” exact match: Migrate SQL Server to Azure SQL with Managed Identity + +### Guidelines +1. Use `Authentication=Active Directory Default` in connection string β€” no passwords or secrets +2. Connection string format: `Server=tcp:.database.windows.net,1433;Database=;Authentication=Active Directory Default;TrustServerCertificate=True` +3. Upgrade `Microsoft.Data.SqlClient` from 5.2.2 β†’ 6.0.2 (recommended by KB) +4. In production, connection string overridden by Azure Key Vault secret `ConnectionStrings--DefaultConnection` +5. EF Core `UseSqlServer` is already correct β€” `Microsoft.Data.SqlClient` handles token acquisition automatically + +### Progress + +- [βœ…] Migration Plan (appended to progress.md) +- [βœ…] Version Control Setup (branch `appmod/dotnet-migration-20260514105229` already active) +- Code Migration + - [βœ…] `ContosoUniversity.csproj` β€” Upgrade `Microsoft.Data.SqlClient` 5.2.2 β†’ 6.0.2 + - [βœ…] `appsettings.json` β€” Add `ConnectionStrings.DefaultConnection` with Azure SQL format (tcp: prefix, port 1433, Authentication=Active Directory Default, TrustServerCertificate=True) + - [βœ…] `appsettings.Development.json` β€” Add `ConnectionStrings.DefaultConnection` with dev Azure SQL format + - [βœ…] `Program.cs` β€” Add `using System;` to fix pre-existing build error from Key Vault migration +- Validation + - [βœ…] Build Verification β€” PASSED (0 errors, 98 pre-existing warnings) + - [βœ…] CVE Check β€” PASSED (all packages above affected version ranges) + - [βœ…] Consistency Check β€” PASSED (0 Critical, 0 Major, 0 Minor) + - [βœ…] Completeness Check β€” PASSED (no System.Data.SqlClient, Integrated Security=True, or password references remain) + - [βœ…] Test Fix β€” N/A (no test projects) + - [βœ…] Build Validation (Final) β€” PASSED +- [βœ…] Final Commit + +--- + +## Secrets β†’ Azure Key Vault Migration (Session 20260514105229) + +**KB Used:** `dotnet-azure-keyvault-secret` (trust: 555.03) β€” exact match: Migrate secrets/connection strings to Azure Key Vault with DefaultAzureCredential + +### Guidelines +1. Add `Azure.Security.KeyVault.Secrets` 4.8.0 and `Azure.Extensions.AspNetCore.Configuration.Secrets` 1.3.2 packages +2. Add `KeyVaultName` to `appsettings.json`; remove hard-coded connection strings and service bus settings (loaded from Key Vault at runtime) +3. Configure `Program.cs` to call `builder.Configuration.AddAzureKeyVault(...)` with `DefaultAzureCredential` +4. Key Vault secret names follow double-dash convention: e.g. `ConnectionStrings--DefaultConnection` + +### Progress +- [βœ…] Migration Plan / progress.md update +- [βœ…] `ContosoUniversity.csproj` β€” Add `Azure.Security.KeyVault.Secrets` 4.8.0, `Azure.Extensions.AspNetCore.Configuration.Secrets` 1.3.2, `Azure.Storage.Blobs` 12.24.0 +- [βœ…] `appsettings.json` β€” Remove `ConnectionStrings`+`AzureServiceBus`; add `KeyVaultName`; keep non-secret `AzureStorageBlob` config +- [βœ…] `appsettings.Development.json` β€” Remove `ConnectionStrings`; add `KeyVaultName` +- [βœ…] `Program.cs` β€” Add `builder.Configuration.AddAzureKeyVault(...)` + `BlobServiceClient` DI registration +- Validation & Fixing + - [βœ…] Build and Fix (0 errors β€” 1 round to add `using System;` + fix `BlobServiceClient` registration) + - [βœ…] CVE Check β€” PASSED (`Azure.Security.KeyVault.Secrets` 4.8.0, `Azure.Extensions.AspNetCore.Configuration.Secrets` 1.3.2, `Azure.Storage.Blobs` 12.24.0 β€” all above vulnerable ranges) + - [βœ…] Consistency Check β€” PASSED (0 Critical, 0 Major; minor: `Microsoft.Data.SqlClient` pre-existing version bump 5.2.2β†’6.0.2) + - [βœ…] Test Fix β€” N/A (no test projects) + - [βœ…] Completeness Check β€” PASSED (no ConnectionStrings/secrets in config; KeyVaultName+AddAzureKeyVault confirmed) + - [βœ…] Build Validation (Final) β€” PASSED (0 errors) +- [βœ…] Final Summary + - [βœ…] Final Code Commit + - [βœ…] Migration Summary Generation diff --git a/ContosoUniversity/.github/modernize/code-migration/20260514105229/summary.md b/ContosoUniversity/.github/modernize/code-migration/20260514105229/summary.md new file mode 100644 index 00000000..76f4b674 --- /dev/null +++ b/ContosoUniversity/.github/modernize/code-migration/20260514105229/summary.md @@ -0,0 +1,702 @@ +# ASP.NET MVC 5 (.NET Framework 4.8) to ASP.NET Core (.NET 10) + Azure Service Bus Migration Result + +> **Executive Summary**\ +> The ContosoUniversity application has been successfully migrated from ASP.NET MVC 5 targeting .NET Framework 4.8 to ASP.NET Core MVC targeting .NET 10, with the notification subsystem further modernized from MSMQ (and an interim in-memory queue) to Azure Service Bus using `DefaultAzureCredential` (Managed Identity). All legacy dependencies (System.Web, System.Messaging, BundleConfig, packages.config) have been removed; `Azure.Messaging.ServiceBus` and `Azure.Identity` replace the Windows-only messaging stack. The application compiles cleanly on .NET 10 with zero build errors, zero CVE vulnerabilities in all new packages, and zero completeness issues. + +--- + +# Local File I/O to Azure Blob Storage Migration Result + +> **Executive Summary**\ +> All local file system operations in `Controllers/CoursesController.cs` have been successfully replaced with Azure Blob Storage using `BlobServiceClient` and `DefaultAzureCredential` (Managed Identity). The 8 incidents of `System.IO.File` / `System.IO.Directory` usage across `Create`, `Edit`, and `DeleteConfirmed` actions were migrated to `BlobContainerClient.CreateIfNotExists()`, `BlobClient.Upload()`, and `BlobClient.DeleteIfExists()`. The teaching material image path is now stored as the blob URI. No secrets or connection strings are used; Managed Identity provides passwordless access to Azure Blob Storage. + +## 1. Migration Improvements + +Successfully migrated from local `System.IO` file operations to Azure Blob Storage. The migration replaces `IWebHostEnvironment`-based local path construction and `System.IO.File`/`System.IO.Directory` calls with `BlobServiceClient` authenticated via `DefaultAzureCredential`. The `Azure.Storage.Blobs` 12.24.0 package was added, `BlobServiceClient` was registered as a singleton in `Program.cs`, and `appsettings.json` was updated with the storage endpoint and container name. + +| Area | Before | After | Improvement | +|------|--------|-------|-------------| +| File Storage | Local `wwwroot/Uploads/TeachingMaterials/` | Azure Blob Storage container `teaching-materials` | Cloud-native; scalable; no local disk dependency | +| Authentication & Security | Local filesystem (no auth) | `DefaultAzureCredential` (Managed Identity) β€” no keys/connection strings | Passwordless; works with az login, Managed Identity, pod identity | +| SDK/Dependencies | `System.IO` + `Microsoft.AspNetCore.Hosting` | `Azure.Storage.Blobs` 12.24.0 + `Azure.Identity` 1.14.0 | Cloud SDK; built-in retry, telemetry, and SAS generation | +| File Path Storage | Relative local path `/Uploads/TeachingMaterials/{name}` | Full blob URI `https://.blob.core.windows.net//` | Globally addressable; no server-side path resolution needed | +| Container Management | `Directory.Exists` + `Directory.CreateDirectory` | `BlobContainerClient.CreateIfNotExists()` | Idempotent; automatic provisioning on first upload | +| File Deletion | `System.IO.File.Exists` + `System.IO.File.Delete` | `BlobClient.DeleteIfExists()` | Idempotent; no race condition; handles missing blobs gracefully | +| DI Injection | `IWebHostEnvironment` for path construction | `BlobServiceClient` + `IConfiguration` | Proper cloud SDK injection; testable via DI | +| Scalability | Single-server local storage | Geo-redundant Azure Blob Storage | High availability; replicated across Azure regions | + +## 2. Build and Validation + +All source files successfully compiled with `Azure.Storage.Blobs` 12.24.0 dependency. No test projects exist in the solution (0 tests β€” N/A). Build completed with zero errors. + +#### Build Validation +| Field | Value | +|-------|-------| +| Status | βœ… Success | +| Build Tool | dotnet build (.NET 10 / MSBuild) | +| Result | 0 errors, 98 pre-existing nullable warnings (unchanged from before migration) | + +#### Test Validation +| Field | Value | +|-------|-------| +| Status | βœ… N/A | +| Total Tests | 0 | +| Passed | 0 | +| Failed | 0 | +| Test Framework | N/A (no test projects in solution) | + +#### Code Quality Validation +| Check | Status | Details | +|-------|--------|---------| +| CVE Scan | βœ… Success | `Azure.Storage.Blobs` 12.24.0 above affected < 12.13.0; `Azure.Identity` 1.14.0 above affected < 1.11.4; all others above vulnerable ranges β€” no action needed | +| Consistency Check | βœ… Success | 0 Critical, 0 Major, 0 Minor β€” all file operations correctly translated to blob equivalents | +| Completeness Check | βœ… Success | 0 issues β€” no `System.IO.File`, `System.IO.Directory`, `IWebHostEnvironment`, `FileStream`, or local path constructions remain | + +## 3. Recommended Next Steps + +I. **Provision Azure Storage Account**: Create an Azure Storage Account and note its blob service endpoint (`https://.blob.core.windows.net`). + +II. **Configure appsettings.json**: Replace `` in `AzureStorageBlob:Endpoint` with your actual storage account name. The container `teaching-materials` will be auto-created on first upload. + +III. **Grant Managed Identity Access**: Assign the `Storage Blob Data Contributor` role to your app's Managed Identity on the storage account so `DefaultAzureCredential` can upload and delete blobs. + +IV. **Update Views**: If any Razor views render `TeachingMaterialImagePath` as an ``, they will now work directly with the full blob URI stored in the field β€” no changes required. + +V. **Create Pull Request**: After verifying the changes in staging, submit branch `appmod/dotnet-migration-20260514105229` for code review. + +VI. **Save as Custom Skill**: To reuse this migration pattern in other projects, save as `My Skill` from the `Tasks` section in the sidebar. + +## 4. Additional Details + +
Click to expand for migration details + +#### Project Details +| Field | Value | +|-------|-------| +| Session ID | `20260514105229` | +| Migration executed by | xuycao | +| Migration performed by | GitHub Copilot | +| Project Pathname | `C:\Users\xuycao\dev\testrepo\dotnet-migration-copilot-samples\ContosoUniversity` | +| Language | .NET (C#) | +| Files modified | 4 | +| Branch | `appmod/dotnet-migration-20260514105229` | + +#### Version Control Summary +| Field | Value | +|-------|-------| +| Version Control System | Git | +| Total Commits | 1 | +| Uncommitted Changes | None | + +**Commits:** +1. `f28d6b8` β€” Code migration completed: Migrate local file I/O to Azure Blob Storage - Replace System.IO.File/Directory in CoursesController with BlobServiceClient + DefaultAzureCredential; add Azure.Storage.Blobs 12.24.0; register BlobServiceClient singleton in Program.cs; add AzureStorageBlob config section + +#### Code Changes +**Source Files (2)** +- `Controllers/CoursesController.cs` β€” Removed `IWebHostEnvironment`; injected `BlobServiceClient` + `IConfiguration`; replaced `Directory.Exists/CreateDirectory` with `CreateIfNotExists()`; replaced `new FileStream` + `CopyTo` with `blobClient.Upload(stream)`; replaced `File.Exists/Delete` with `DeleteIfExists()`; path stored as blob URI +- `Program.cs` β€” Added `using System;`, `using Azure.Storage.Blobs;`; registered `BlobServiceClient` singleton using `DefaultAzureCredential` and endpoint from config + +**Configuration Files (1)** +- `appsettings.json` β€” Added `AzureStorageBlob` section with `Endpoint` and `ContainerName` + +**Build Files (1)** +- `ContosoUniversity.csproj` β€” Added `` + +#### Dependency Changes +**Removed:** +- `Microsoft.AspNetCore.Hosting` injection (`IWebHostEnvironment`) β€” no longer needed for file path construction + +**Added:** +- `Azure.Storage.Blobs` 12.24.0 β€” Azure Blob Storage SDK for upload, download, and delete operations + +#### Tasks +- Migrate Storage to Azure Storage Blob + +#### Knowledge Base Applied + +1 migration guideline was applied covering: + +| Migration Area | Description | +|----------------|-------------| +| Blob Client Setup | `BlobServiceClient(Uri, DefaultAzureCredential)` β€” Managed Identity authentication pattern | +| Container Management | `BlobContainerClient.CreateIfNotExists()` replaces `Directory.Exists/CreateDirectory` | +| Upload | `BlobClient.Upload(stream, overwrite: true)` replaces `new FileStream` + `CopyTo` | +| Delete | `BlobClient.DeleteIfExists()` replaces `File.Exists` + `File.Delete` | +| Path Storage | Full blob URI (`blobClient.Uri.ToString()`) replaces relative local path | + +#### Issues Fixed During Migration +| Severity | Issue | Resolution | +|----------|-------|------------| +| Minor | `using System;` missing in `Program.cs` preventing `Uri` resolution | Added `using System;` to usings block | +| Minor | `Azure.Storage.Blobs` not present in `.csproj` after `dotnet add package` ran against stale csproj | Added `` directly to csproj | + +
+ +## 1. Migration Improvements + +Successfully migrated from ASP.NET MVC 5 (.NET Framework 4.8) to ASP.NET Core (.NET 10), then further migrated the notification subsystem from MSMQ (and interim in-memory queue) to Azure Service Bus. The migration replaces the legacy `System.Web`-based request pipeline with the ASP.NET Core middleware pipeline, replaces `Web.config`/`Global.asax` with `Program.cs`/`appsettings.json`, upgrades Entity Framework 6 to EF Core 9.0.5, replaces `System.Messaging.MessageQueue` (MSMQ) with `Azure.Messaging.ServiceBus.ServiceBusClient` authenticated via `DefaultAzureCredential`, and uses `Newtonsoft.Json` for message serialization. All dependencies, configuration, and implementation code have been updated. + +| Area | Before | After | Improvement | +|------|--------|-------|-------------| +| SDK/Framework | ASP.NET MVC 5, .NET Framework 4.8 | ASP.NET Core MVC, .NET 10 | Cross-platform, modern runtime | +| Project File | Legacy MSBuild `.csproj` + `packages.config` | SDK-style `` + PackageReference | Simplified, transitive dependencies | +| App Entry Point | `Global.asax` / `Global.asax.cs` | `Program.cs` | Unified startup with DI and middleware | +| Configuration | `Web.config` (XML) | `appsettings.json` + `IConfiguration` | JSON-based, environment-aware, no config transforms | +| Data Access | Entity Framework 6 | Entity Framework Core 9.0.5 | LINQ improvements, async support, .NET 10 compatible | +| Messaging Queue | MSMQ (`System.Messaging.MessageQueue`) + `XmlMessageFormatter` | Azure Service Bus (`Azure.Messaging.ServiceBus`) + JSON serialization | Cloud-native; cross-platform; no Windows service dependency | +| Authentication & Security | N/A (local MSMQ, no auth) | `DefaultAzureCredential` (Managed Identity) β€” no connection strings or secrets | Passwordless; works with local az login, Managed Identity, and env credentials | +| Dependency Injection | Manual factory pattern (`SchoolContextFactory.Create()`) | Constructor DI via `builder.Services.AddDbContext<>` / `AddSingleton` | Standard ASP.NET Core DI container | +| Razor Views | `@Scripts.Render` / `@Styles.Render` bundle helpers | CDN ` + + + + + @RenderSection("scripts", required: false) diff --git a/ContosoUniversity/Views/Shared/_ValidationScriptsPartial.cshtml b/ContosoUniversity/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 00000000..836de693 --- /dev/null +++ b/ContosoUniversity/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,2 @@ + + diff --git a/ContosoUniversity/Views/Students/Create.cshtml b/ContosoUniversity/Views/Students/Create.cshtml index 559b5587..ef40d8bf 100644 --- a/ContosoUniversity/Views/Students/Create.cshtml +++ b/ContosoUniversity/Views/Students/Create.cshtml @@ -51,5 +51,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Students/Edit.cshtml b/ContosoUniversity/Views/Students/Edit.cshtml index 65ab8c0a..97e5168e 100644 --- a/ContosoUniversity/Views/Students/Edit.cshtml +++ b/ContosoUniversity/Views/Students/Edit.cshtml @@ -53,5 +53,5 @@ @section Scripts { - @Scripts.Render("~/bundles/jqueryval") + } diff --git a/ContosoUniversity/Views/Web.config b/ContosoUniversity/Views/Web.config deleted file mode 100644 index e2602210..00000000 --- a/ContosoUniversity/Views/Web.config +++ /dev/null @@ -1,50 +0,0 @@ - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ContosoUniversity/Views/_ViewImports.cshtml b/ContosoUniversity/Views/_ViewImports.cshtml new file mode 100644 index 00000000..7aa7350e --- /dev/null +++ b/ContosoUniversity/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using ContosoUniversity +@using ContosoUniversity.Models +@using ContosoUniversity.Models.SchoolViewModels +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/ContosoUniversity/Web.config b/ContosoUniversity/Web.config deleted file mode 100644 index f9257e0e..00000000 --- a/ContosoUniversity/Web.config +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ContosoUniversity/appsettings.Development.json b/ContosoUniversity/appsettings.Development.json new file mode 100644 index 00000000..63552f9b --- /dev/null +++ b/ContosoUniversity/appsettings.Development.json @@ -0,0 +1,9 @@ +ο»Ώ{ + "KeyVaultName": "", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/ContosoUniversity/appsettings.json b/ContosoUniversity/appsettings.json new file mode 100644 index 00000000..58217a3a --- /dev/null +++ b/ContosoUniversity/appsettings.json @@ -0,0 +1,14 @@ +ο»Ώ{ + "KeyVaultName": "", + "AzureStorageBlob": { + "Endpoint": "https://.blob.core.windows.net", + "ContainerName": "teaching-materials" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/ContosoUniversity/packages.config b/ContosoUniversity/packages.config deleted file mode 100644 index 3fc0a3d8..00000000 --- a/ContosoUniversity/packages.config +++ /dev/null @@ -1,48 +0,0 @@ -ο»Ώ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ContosoUniversity/wwwroot/css/notifications.css b/ContosoUniversity/wwwroot/css/notifications.css new file mode 100644 index 00000000..e24e8fc8 --- /dev/null +++ b/ContosoUniversity/wwwroot/css/notifications.css @@ -0,0 +1,96 @@ +/* Notification Styles */ +.notification-container { + position: fixed; + top: 75px; /* Account for Bootstrap 5 navbar height with border */ + right: 20px; + z-index: 9999; + max-width: 350px; +} + +.notification { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 12px 16px; + margin-bottom: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease-in-out; + position: relative; +} + +.notification.show { + opacity: 1; + transform: translateX(0); +} + +.notification.notification-success { + background: #d1edff; + border-color: #0d6efd; + color: #0d6efd; +} + +.notification.notification-info { + background: #e2f3ff; + border-color: #17a2b8; + color: #17a2b8; +} + +.notification.notification-warning { + background: #fff3cd; + border-color: #ffc107; + color: #856404; +} + +.notification.notification-error { + background: #f8d7da; + border-color: #dc3545; + color: #721c24; +} + +.notification-title { + font-weight: 600; + margin-bottom: 4px; + font-size: 14px; +} + +.notification-message { + font-size: 13px; + margin-bottom: 4px; +} + +.notification-time { + font-size: 11px; + opacity: 0.7; +} + +.notification-close { + position: absolute; + top: 5px; + right: 8px; + background: none; + border: none; + font-size: 16px; + cursor: pointer; + opacity: 0.5; + transition: opacity 0.2s ease; +} + +.notification-close:hover { + opacity: 1; +} + +.notification-badge { + background-color: #dc3545; + color: white; + border-radius: 50%; + padding: 2px 6px; + font-size: 11px; + margin-left: 5px; + display: none; +} + +.notification-badge.show { + display: inline-block; +} diff --git a/ContosoUniversity/wwwroot/css/site.css b/ContosoUniversity/wwwroot/css/site.css new file mode 100644 index 00000000..8f776359 --- /dev/null +++ b/ContosoUniversity/wwwroot/css/site.css @@ -0,0 +1,628 @@ +/* Simplified 2017 Design - Clean Flat Design */ +@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700|Roboto:300,400,500,700'); + +body { + padding-top: 0; /* Remove body padding-top since we're using container padding */ + padding-bottom: 20px; + background: #f8f9fa; + font-family: 'Open Sans', 'Segoe UI', sans-serif; + font-size: 14px; + line-height: 1.6; + color: #2c3e50; + min-height: 100vh; +} + +/* Simplified Container */ +.body-content { + padding-left: 15px; + padding-right: 15px; + background: white; + margin: 0 auto; + max-width: 1200px; + border-radius: 0 0 8px 8px; /* Only bottom corners rounded */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e9ecef; + border-top: none; /* No top border to connect with navbar */ + padding: 40px; + margin-top: 70px; /* Account for fixed navbar height */ +} + +.navbar { + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + border: none; + box-shadow: 0 3px 10px rgba(30, 60, 114, 0.3); + min-height: 70px; /* Use min-height instead of fixed height */ + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100%; + z-index: 1030; + border-bottom: 3px solid #ffd700; +} + +.navbar-brand { + font-family: 'Roboto', serif; + font-weight: 700; + font-size: 24px; + color: white !important; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + letter-spacing: 1px; + padding: 15px 15px; + position: relative; +} + +.navbar-brand::before { + content: "πŸŽ“"; + margin-right: 8px; + font-size: 20px; +} + +.navbar-brand::after { + content: ''; + position: absolute; + bottom: 15px; + left: 15px; + width: 0; + height: 2px; + background: #ffd700; + transition: width 0.3s ease; +} + +.navbar-brand:hover::after { + width: calc(100% - 30px); +} + +.navbar-brand:hover { + color: #ffd700 !important; + text-decoration: none; +} + +.navbar-nav { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + align-items: center; + margin-left: 30px; + --bs-nav-link-padding-x: 0.5rem; + --bs-nav-link-padding-y: 0.5rem; +} + +.navbar-nav li { + list-style: none; + margin: 0 3px; + position: relative; +} + +.navbar-nav li::before { + display: none; /* Remove any bullets */ +} + +.navbar-nav > li > a, +.navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.9) !important; + font-weight: 500; + font-size: 14px; + padding: 15px 18px; + text-decoration: none; + border-radius: 0; + text-transform: uppercase; + letter-spacing: 0.8px; + transition: all 0.3s ease; + position: relative; + border-bottom: 3px solid transparent; +} + +.navbar-nav > li > a:hover, +.navbar-nav .nav-link:hover { + color: #ffd700 !important; + background: rgba(255, 215, 0, 0.1); + border-bottom: 3px solid #ffd700; + text-decoration: none; +} + +/* Active/Current page styling */ +.navbar-nav > li.active > a, +.navbar-nav > li > a.active, +.navbar-nav .nav-link.active { + color: #ffd700 !important; + background: rgba(255, 215, 0, 0.15); + border-bottom: 3px solid #ffd700; +} + +/* Sub-menu item icons */ +.navbar-nav > li > a::before { + margin-right: 6px; + font-size: 12px; +} + +/* Add specific icons for each menu item */ +.navbar-nav > li:nth-child(1) > a::before { content: "🏠"; } /* Home */ +.navbar-nav > li:nth-child(2) > a::before { content: "ℹ️"; } /* About */ +.navbar-nav > li:nth-child(3) > a::before { content: "πŸ‘₯"; } /* Students */ +.navbar-nav > li:nth-child(4) > a::before { content: "πŸ“š"; } /* Courses */ +.navbar-nav > li:nth-child(5) > a::before { content: "πŸ‘¨β€πŸ«"; } /* Instructors */ +.navbar-nav > li:nth-child(6) > a::before { content: "πŸ›οΈ"; } /* Departments */ + +/* Separator between menu groups */ +.navbar-nav > li:nth-child(2)::after { + content: ''; + position: absolute; + right: -8px; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 30px; + background: rgba(255, 255, 255, 0.3); +} + +/* University themed styling */ +.navbar-container { + position: relative; +} + +.navbar-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, #ffd700, #ffed4e, #ffd700); + animation: shimmer 3s ease-in-out infinite; +} + +@keyframes shimmer { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} + +/* Simplified Buttons */ +.btn { + border: none; + border-radius: 4px; + padding: 10px 20px; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.btn-primary { + background: #007bff; + color: white; +} + +.btn-success { + background: #28a745; + color: white; +} + +.btn-warning { + background: #ffc107; + color: #212529; +} + +.btn-danger { + background: #dc3545; + color: white; +} + +.btn-default { + background: #6c757d; + color: white; +} + +.btn-sm { + padding: 6px 12px; + font-size: 12px; + border-radius: 3px; +} + +/* Simplified Headers */ +h1, h2, h3, h4, h5, h6 { + font-family: 'Roboto', sans-serif; + color: #2c3e50; + font-weight: 400; + margin-bottom: 20px; +} + +h1 { + font-size: 32px; + color: #007bff; + margin-bottom: 25px; + border-bottom: 2px solid #007bff; + padding-bottom: 10px; +} + +h2 { + font-size: 26px; + color: #34495e; + margin-bottom: 18px; +} + +h4 { + font-size: 18px; + color: #495057; + margin-bottom: 15px; + font-weight: 500; +} + +/* Simplified Tables */ +.table { + background: white; + border-radius: 6px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 25px; + border: 1px solid #dee2e6; +} + +.table th { + background: #f8f9fa; + color: #495057; + font-weight: 600; + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.5px; + padding: 15px; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; +} + +.table td { + padding: 12px 15px; + border-bottom: 1px solid #f1f3f4; + vertical-align: middle; +} + +/* Simplified Forms */ +.form-control { + border: 2px solid #e9ecef; + border-radius: 4px; + padding: 10px 15px; + font-size: 14px; + box-shadow: none; + background: white; +} + +.form-control:focus { + border-color: #007bff; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + color: #495057; + font-weight: 600; + margin-bottom: 8px; + display: block; + font-size: 14px; +} + +/* Simplified Cards */ +.panel, .card { + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 25px; + overflow: hidden; + border: 1px solid #e9ecef; +} + +.panel-heading, .card-header { + background: #f8f9fa; + color: #495057; + padding: 15px 20px; + font-weight: 600; + border-bottom: 1px solid #dee2e6; +} + +.panel-body, .card-body { + padding: 20px; +} + +/* Simplified Alerts */ +.alert { + border: none; + border-radius: 6px; + padding: 15px 20px; + margin-bottom: 20px; + border-left: 4px solid; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.alert-success { + background: #d4edda; + color: #155724; + border-left-color: #28a745; +} + +.alert-danger { + background: #f8d7da; + color: #721c24; + border-left-color: #dc3545; +} + +.alert-warning { + background: #fff3cd; + color: #856404; + border-left-color: #ffc107; +} + +.alert-info { + background: #d1ecf1; + color: #0c5460; + border-left-color: #17a2b8; +} + +/* Simplified Images */ +img { + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Simplified Links */ +a { + color: #007bff; + text-decoration: none; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +/* Simplified File Upload */ +input[type="file"] { + max-width: 100%; + padding: 10px; + border: 2px dashed #dee2e6; + border-radius: 4px; + background: #f8f9fa; +} + +/* Simplified Image Display */ +.current-image { + background: #f8f9fa; + padding: 15px; + border-radius: 6px; + border: 1px solid #dee2e6; + text-align: center; +} + +.current-image img { + border: 2px solid #dee2e6; + padding: 5px; + background-color: white; +} + +/* Override the default bootstrap behavior where horizontal description lists + will truncate terms that are too long to fit in the left column +*/ +.dl-horizontal dt { + white-space: normal; + text-align: left; + width: 180px; + color: #495057; + font-weight: 600; +} + +.dl-horizontal dd { + margin-left: 200px; + color: #6c757d; +} + +/* Set width on the form input elements since they're 100% wide by default */ +input, +select, +textarea { + max-width: 400px; +} + +/* Simplified validation helpers */ +.field-validation-error { + color: #dc3545; + font-size: 12px; + margin-top: 5px; + display: block; + font-weight: 500; +} + +.field-validation-valid { + display: none; +} + +input.input-validation-error { + border: 2px solid #dc3545; + box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1); +} + +input[type="checkbox"].input-validation-error { + border: 0 none; +} + +.validation-summary-errors { + color: #dc3545; + background: #f8d7da; + border: 2px solid #f5c6cb; + border-radius: 6px; + padding: 15px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.validation-summary-valid { + display: none; +} + +/* Bootstrap 5 specific fixes */ +.navbar-nav { + --bs-nav-link-padding-x: 0.5rem; + --bs-nav-link-padding-y: 0.5rem; +} + +.navbar-toggler { + border: 1px solid rgba(255, 255, 255, 0.3); + padding: 4px 8px; +} + +.navbar-toggler:focus { + box-shadow: 0 0 0 0.2rem rgba(255, 215, 0, 0.25); +} + +.navbar-text { + color: rgba(255, 255, 255, 0.9) !important; + margin: 0; + padding: 0.5rem 0; +} + +.navbar-text .badge { + margin-left: 0.5rem; +} + +/* Badge styling for Bootstrap 5 */ +.badge.bg-success { background-color: #198754 !important; } +.badge.bg-warning { background-color: #ffc107 !important; color: #000 !important; } +.badge.bg-danger { background-color: #dc3545 !important; } +.badge.bg-info { background-color: #0dcaf0 !important; color: #000 !important; } + +/* Right-aligned navbar items */ +.navbar-nav.ms-auto .nav-item { + display: flex; + align-items: center; +} + +.navbar-nav.ms-auto .navbar-text { + white-space: nowrap; + font-size: 14px; + font-weight: 500; +} + +/* Ensure badges are properly styled in navbar */ +.navbar .badge { + font-size: 11px; + padding: 0.25em 0.5em; + border-radius: 0.25rem; +} + +/* Ensure proper spacing on mobile */ +@media (max-width: 991.98px) { + .body-content { + margin-top: 80px; /* Account for taller navbar on mobile */ + } + + .navbar-nav { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } + + .navbar-nav.ms-auto { + margin-top: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.2); + padding-top: 0.5rem; + } + + .navbar-nav.ms-auto .navbar-text { + padding: 0.5rem 0; + text-align: center; + width: 100%; + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .body-content { + margin: 10px; + padding: 20px; + border-radius: 6px; + } + + .navbar-nav { + flex-direction: column; + width: 100%; + } + + .navbar-nav li { + width: 100%; + margin: 2px 0; + } + + .navbar-nav > li > a { + border-radius: 4px; + margin: 2px 0; + } +} + +/* Additional Clean Styling */ +.page-header { + background: #f8f9fa; + padding: 20px; + border-radius: 6px; + margin-bottom: 25px; + border: 1px solid #e9ecef; +} + +.navbar-right { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + margin: 0; +} + +.navbar-text { + color: rgba(255, 255, 255, 0.9) !important; + font-size: 13px; + font-weight: 500; + margin: 0; + padding: 0; + line-height: 1.4; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); +} + +.navbar-text .label { + margin-left: 8px; + padding: 4px 8px; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +/* Role-specific label colors */ +.label-admin { + background: #ffd700; + color: #1e3c72; +} + +.label-teacher { + background: #28a745; + color: white; +} + +.label-student { + background: #17a2b8; + color: white; +} + +/* Responsive adjustment for mobile */ +@media (max-width: 768px) { + .navbar-right { + position: relative; + right: auto; + top: auto; + transform: none; + text-align: center; + margin-top: 10px; + width: 100%; + } +} diff --git a/ContosoUniversity/wwwroot/js/notifications.js b/ContosoUniversity/wwwroot/js/notifications.js new file mode 100644 index 00000000..f6fc1908 --- /dev/null +++ b/ContosoUniversity/wwwroot/js/notifications.js @@ -0,0 +1,153 @@ +// Notification System for Admin Users +(function() { + 'use strict'; + + var NotificationSystem = { + container: null, + notificationCount: 0, + checkInterval: 5000, // Check every 5 seconds + maxNotifications: 5, + + init: function() { + this.createContainer(); + this.startPolling(); + }, + + createContainer: function() { + this.container = document.createElement('div'); + this.container.className = 'notification-container'; + document.body.appendChild(this.container); + }, + + startPolling: function() { + var self = this; + // Check immediately + this.checkForNotifications(); + + // Then check every interval + setInterval(function() { + self.checkForNotifications(); + }, this.checkInterval); + }, + + checkForNotifications: function() { + var self = this; + + fetch('/Notifications/GetNotifications', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin' + }) + .then(function(response) { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(function(data) { + if (data.success && data.notifications && data.notifications.length > 0) { + data.notifications.forEach(function(notification) { + self.showNotification(notification); + }); + } + }) + .catch(function(error) { + console.log('Error fetching notifications:', error); + }); + }, + + showNotification: function(notification) { + var self = this; + + // Create notification element + var notificationEl = document.createElement('div'); + notificationEl.className = 'notification notification-info'; + + // Determine notification type based on operation + var type = 'info'; + if (notification.Operation === 'CREATE') { + type = 'success'; + } else if (notification.Operation === 'DELETE') { + type = 'warning'; + } + + notificationEl.className = 'notification notification-' + type; + + var timeAgo = this.getTimeAgo(new Date(notification.CreatedAt)); + + notificationEl.innerHTML = + '' + + '
' + notification.Operation + ' - ' + notification.EntityType + '
' + + '
' + notification.Message + '
' + + '
By ' + notification.CreatedBy + ' β€’ ' + timeAgo + '
'; + + // Add to container + this.container.appendChild(notificationEl); + + // Animate in + setTimeout(function() { + notificationEl.classList.add('show'); + }, 100); + + // Auto remove after 1 minute (60 seconds) + setTimeout(function() { + self.closeNotification(notificationEl.querySelector('.notification-close')); + }, 60000); + + // Limit number of notifications + this.limitNotifications(); + }, + + closeNotification: function(closeButton) { + var notification = closeButton.parentElement; + notification.classList.remove('show'); + + setTimeout(function() { + if (notification.parentElement) { + notification.parentElement.removeChild(notification); + } + }, 300); + }, + + limitNotifications: function() { + var notifications = this.container.querySelectorAll('.notification'); + while (notifications.length > this.maxNotifications) { + var oldest = notifications[0]; + this.closeNotification(oldest.querySelector('.notification-close')); + notifications = this.container.querySelectorAll('.notification'); + } + }, + + getTimeAgo: function(date) { + var now = new Date(); + var diffInSeconds = Math.floor((now - date) / 1000); + + if (diffInSeconds < 60) { + return 'just now'; + } else if (diffInSeconds < 3600) { + var minutes = Math.floor(diffInSeconds / 60); + return minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ago'; + } else if (diffInSeconds < 86400) { + var hours = Math.floor(diffInSeconds / 3600); + return hours + ' hour' + (hours > 1 ? 's' : '') + ' ago'; + } else { + var days = Math.floor(diffInSeconds / 86400); + return days + ' day' + (days > 1 ? 's' : '') + ' ago'; + } + } + }; + + // Auto-initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + NotificationSystem.init(); + }); + } else { + NotificationSystem.init(); + } + + // Make NotificationSystem globally available for close button + window.NotificationSystem = NotificationSystem; +})();