diff --git a/WebHandler/ADCompareWebHandler.cs b/WebHandler/ADCompareWebHandler.cs
new file mode 100644
index 0000000..efedd1c
--- /dev/null
+++ b/WebHandler/ADCompareWebHandler.cs
@@ -0,0 +1,295 @@
+using Disco.Plugins.ADCompare.Features;
+using Disco.Plugins.ADCompare.Models;
+using Disco.Services.Plugins;
+using Newtonsoft.Json;
+using System;
+using System.Text;
+using System.Web.Mvc;
+
+namespace Disco.Plugins.ADCompare.WebHandler
+{
+ public class ADCompareWebHandler : PluginWebHandler
+ {
+ public override ActionResult ExecuteAction(string ActionName)
+ {
+ switch (ActionName?.ToLower())
+ {
+ case null:
+ case "":
+ case "index":
+ return Index();
+ case "compare":
+ return Compare();
+ case "compareuser":
+ return CompareUser();
+ case "export":
+ return ExportCsv();
+ default:
+ return new HttpNotFoundResult();
+ }
+ }
+
+ ///
+ /// Landing page with a button to run comparison
+ ///
+ private ActionResult Index()
+ {
+ var html = BuildIndexPage();
+ return new ContentResult
+ {
+ Content = html,
+ ContentType = "text/html",
+ ContentEncoding = Encoding.UTF8
+ };
+ }
+
+ ///
+ /// Run full comparison and return results as JSON
+ ///
+ private ActionResult Compare()
+ {
+ var service = new ADCompareService(Database);
+ var summary = service.CompareAllUsers();
+
+ return new ContentResult
+ {
+ Content = JsonConvert.SerializeObject(summary, Formatting.Indented),
+ ContentType = "application/json",
+ ContentEncoding = Encoding.UTF8
+ };
+ }
+
+ ///
+ /// Compare a single user - expects ?userId=DOMAIN\username
+ ///
+ private ActionResult CompareUser()
+ {
+ var userId = HostController.Request.QueryString["userId"];
+ if (string.IsNullOrWhiteSpace(userId))
+ {
+ return new HttpStatusCodeResult(400, "userId parameter required");
+ }
+
+ var user = Database.Users.Find(userId);
+ if (user == null)
+ {
+ return new HttpStatusCodeResult(404, "User not found in Disco");
+ }
+
+ var service = new ADCompareService(Database);
+ var result = service.CompareUser(user);
+
+ return new ContentResult
+ {
+ Content = JsonConvert.SerializeObject(result, Formatting.Indented),
+ ContentType = "application/json",
+ ContentEncoding = Encoding.UTF8
+ };
+ }
+
+ ///
+ /// Export comparison results as CSV
+ ///
+ private ActionResult ExportCsv()
+ {
+ var service = new ADCompareService(Database);
+ var summary = service.CompareAllUsers();
+
+ var sb = new StringBuilder();
+ sb.AppendLine("UserId,DisplayName,FoundInAD,ADDisabled,MismatchedFields,Details");
+
+ foreach (var result in summary.Results)
+ {
+ var mismatchFields = result.HasMismatches
+ ? string.Join("; ", result.Mismatches.ConvertAll(m => m.FieldName))
+ : "None";
+
+ var details = result.HasMismatches
+ ? string.Join("; ", result.Mismatches.ConvertAll(m =>
+ $"{m.FieldName}: Disco='{m.DiscoValue}' AD='{m.ADValue}'"))
+ : "";
+
+ sb.AppendLine($"\"{CsvEscape(result.UserId)}\",\"{CsvEscape(result.DisplayName)}\",{result.UserFoundInAD},{result.ADAccountDisabled},\"{CsvEscape(mismatchFields)}\",\"{CsvEscape(details)}\"");
+ }
+
+ var fileName = $"AD_Compare_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
+
+ HostController.Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{fileName}\"");
+
+ return new ContentResult
+ {
+ Content = sb.ToString(),
+ ContentType = "text/csv",
+ ContentEncoding = Encoding.UTF8
+ };
+ }
+
+ private string CsvEscape(string value)
+ {
+ if (string.IsNullOrEmpty(value)) return "";
+ return value.Replace("\"", "\"\"");
+ }
+
+ #region HTML Page Builder
+
+ private string BuildIndexPage()
+ {
+ var pluginUrl = Url.Action(MVC.API.Disco.Plugin.PluginWebAction(Manifest.Id, null));
+
+ return $@"
+
+
AD Compare - User Detail Comparison
+
Compare Active Directory user details against Disco ICT records to identify mismatches.
+
+
+
+
+
+ Comparing users against AD... This may take a moment.
+
+
+
+
+
Summary
+
+
Total Users: -
+
In Sync: -
+
Mismatches: -
+
Not in AD: -
+
AD Disabled: -
+
+
+
+
+
+
+
+
+
+
+
+ | Status |
+ User ID |
+ Display Name |
+ Mismatched Fields |
+ Details |
+
+
+
+
+
+
+";
+ }
+
+ #endregion
+ }
+}