using System.Diagnostics; using System.Net; using System.Text.RegularExpressions; using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; using InnovEnergy.App.Backend.Services; using InnovEnergy.App.Backend.Websockets; using InnovEnergy.Lib.Utils; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; namespace InnovEnergy.App.Backend; using Token = String; // create JobStatus class to track download battery log job public class JobStatus { public string JobId { get; set; } public string Status { get; set; } public string FileName { get; set; } public DateTime StartTime { get; set; } } [Controller] [Route("api/")] //All the http requests from the frontend that contain "/api" will be forwarded to this controller from the nginx reverse proxy. public class Controller : ControllerBase { [HttpPost(nameof(Login))] public ActionResult Login(String username, String? password) { //Find the user to the database, verify its password and create a new session. //Store the new session to the database and return it to the frontend. //If the user log out, the session will be deleted. Each session is valid for 24 hours. The db deletes all invalid/expired sessions every 30 minutes. var user = Db.GetUserByEmail(username); if (user is null) throw new Exceptions(400, "Null User Exception", "Must provide a user to log in as.", Request.Path.Value!); if (!(user.Password.IsNullOrEmpty() && user.MustResetPassword) && !user.VerifyPassword(password)) { throw new Exceptions(401, "Wrong Password Exception", "Please try again.", Request.Path.Value!); } var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user)); return Db.Create(session) ? session : throw new Exceptions(401,"Session Creation Exception", "Not allowed to log in.", Request.Path.Value!); } [HttpPost(nameof(Logout))] public ActionResult Logout(Token authToken) { //Find the session and delete it from the database. var session = Db.GetSession(authToken); return session.Logout() ? Ok() : Unauthorized(); } [HttpGet(nameof(CreateWebSocket))] public async Task CreateWebSocket(Token authToken) { //Everytime a user logs in, this function is called var session = Db.GetSession(authToken)?.User; if (session is null) { Console.WriteLine("------------------------------------Unauthorized user----------------------------------------------"); HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; HttpContext.Abort(); return; } if (!HttpContext.WebSockets.IsWebSocketRequest) { Console.WriteLine("------------------------------------Not a websocket request ----------------------------------------------"); HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; HttpContext.Abort(); return; } //Create a websocket and pass its descriptor to the HandleWebSocketConnection method. //This descriptor is returned to the frontend on the background var webSocketContext = await HttpContext.WebSockets.AcceptWebSocketAsync(); var webSocket = webSocketContext; //Handle the WebSocket connection await WebsocketManager.HandleWebSocketConnection(webSocket); } [HttpGet(nameof(GetAllErrorsForInstallation))] public ActionResult> GetAllErrorsForInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Db.Errors .Where(error => error.InstallationId == id) .OrderByDescending(error => error.Date) .ThenByDescending(error => error.Time) .ToList(); } [HttpGet(nameof(GetHistoryForInstallation))] public ActionResult> GetHistoryForInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Db.UserActions .Where(action =>action.InstallationId == id) .OrderByDescending(action => action.Timestamp) .ToList(); } [HttpGet(nameof(GetAllWarningsForInstallation))] public ActionResult> GetAllWarningsForInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Db.Warnings .Where(error => error.InstallationId == id) .OrderByDescending(error => error.Date) .ThenByDescending(error => error.Time) .ToList(); } [HttpGet(nameof(GetCsvTimestampsForInstallation))] public ActionResult> GetCsvTimestampsForInstallation(Int64 id, Int32 start, Int32 end, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var sampleSize = 100; var allTimestamps = new List(); static string FindCommonPrefix(string str1, string str2) { int minLength = Math.Min(str1.Length, str2.Length); int i = 0; while (i < minLength && str1[i] == str2[i]) { i++; } return str1.Substring(0, i); } Int64 startTimestamp = Int64.Parse(start.ToString().Substring(0,5)); Int64 endTimestamp = Int64.Parse(end.ToString().Substring(0,5)); if (installation.Product == (int)ProductType.Salidomo) { start = Int32.Parse(start.ToString().Substring(0, start.ToString().Length - 2)); end = Int32.Parse(end.ToString().Substring(0, end.ToString().Length - 2)); } string configPath = "/home/ubuntu/.s3cfg"; while (startTimestamp <= endTimestamp) { string bucketPath; if (installation.Product == (int)ProductType.Salimax || installation.Product == (int)ProductType.SodiStoreMax) bucketPath = "s3://" + installation.S3BucketId + "-3e5b3069-214a-43ee-8d85-57d72000c19d/" + startTimestamp; else if (installation.Product == (int)ProductType.SodioHome) bucketPath = "s3://" + installation.S3BucketId + "-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa/" + startTimestamp; else bucketPath = "s3://" + installation.S3BucketId + "-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/" + startTimestamp; Console.WriteLine("Fetching data for "+startTimestamp); try { // Set up process start info ProcessStartInfo startInfo = new ProcessStartInfo { FileName = "s3cmd", Arguments = $"--config {configPath} ls {bucketPath}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; // Start the process Process process = new Process { StartInfo = startInfo }; process.Start(); // Read the output string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); process.WaitForExit(); // Check for errors if (process.ExitCode != 0) { Console.WriteLine("Error executing command:"); Console.WriteLine(error); } else { // Define a regex pattern to match the filenames without .csv extension var pattern = @"/([^/]+)\.(csv|json)$"; var regex = new Regex(pattern); // Process each line of the output foreach (var line in output.Split('\n')) { var match = regex.Match(line); if (match.Success && long.Parse(match.Groups[1].Value) >= start && long.Parse(match.Groups[1].Value) <= end) { allTimestamps.Add(long.Parse(match.Groups[1].Value)); //Console.WriteLine(match.Groups[1].Value); } } } } catch (Exception e) { Console.WriteLine($"Exception: {e.Message}"); } startTimestamp++; } int totalRecords = allTimestamps.Count; if (totalRecords <= sampleSize) { // If the total records are less than or equal to the sample size, return all records Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end); Console.WriteLine("SampledTimestamps = " + allTimestamps.Count); return allTimestamps; } int interval = totalRecords / sampleSize; var sampledTimestamps = new List(); for (int i = 0; i < totalRecords; i += interval) { sampledTimestamps.Add(allTimestamps[i]); } // If we haven't picked enough records (due to rounding), add the latest record to ensure completeness if (sampledTimestamps.Count < sampleSize) { sampledTimestamps.Add(allTimestamps.Last()); } Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end); Console.WriteLine("TotalRecords = "+totalRecords + " interval = "+ interval); Console.WriteLine("SampledTimestamps = " + sampledTimestamps.Count); return sampledTimestamps; } [HttpGet(nameof(GetUserById))] public ActionResult GetUserById(Int64 id, Token authToken) { var session = Db.GetSession(authToken)?.User; if (session == null) return Unauthorized(); var user = Db.GetUserById(id); if (user is null || !session.HasAccessTo(user)) return Unauthorized(); return user .HidePassword() .HideParentIfUserHasNoAccessToParent(session); } [HttpGet(nameof(GetInstallationById))] public ActionResult GetInstallationById(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return installation .FillOrderNumbers() .HideParentIfUserHasNoAccessToParent(user) .HideWriteKeyIfUserIsNotAdmin(user.UserType); } [HttpGet(nameof(GetUsersWithDirectAccessToInstallation))] public ActionResult> GetUsersWithDirectAccessToInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return installation .UsersWithDirectAccess() .Where(u => u.IsDescendantOf(user)) .Select(u => u.HidePassword()) .ToList(); } [HttpGet(nameof(GetInstallationsTheUserHasAccess))] public ActionResult> GetInstallationsTheUserHasAccess(Int64 userId, Token authToken) { var user = Db.GetUserById(userId); if (user == null) return Unauthorized(); return user.AccessibleInstallations().ToList(); } [HttpGet(nameof(GetDirectInstallationAccessForUser))] public ActionResult> GetDirectInstallationAccessForUser(Int64 userId, Token authToken) { var sessionUser = Db.GetSession(authToken)?.User; if (sessionUser == null) return Unauthorized(); var user = Db.GetUserById(userId); if (user == null) return Unauthorized(); return user.DirectlyAccessibleInstallations() .Select(i => new { i.Id, i.Name }) .ToList(); } [HttpGet(nameof(GetDirectFolderAccessForUser))] public ActionResult> GetDirectFolderAccessForUser(Int64 userId, Token authToken) { var sessionUser = Db.GetSession(authToken)?.User; if (sessionUser == null) return Unauthorized(); var user = Db.GetUserById(userId); if (user == null) return Unauthorized(); return user.DirectlyAccessibleFolders() .Select(f => new { f.Id, f.Name }) .ToList(); } [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))] public ActionResult> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(id); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return installation .Ancestors() .SelectMany(f => f.UsersWithDirectAccess() .Where(u => u.IsDescendantOf(user)) .Select(u => new { folderId = f.Id, folderName = f.Name, user = u.HidePassword() })) .ToList(); } [HttpGet(nameof(GetUsersWithDirectAccessToFolder))] public ActionResult> GetUsersWithDirectAccessToFolder(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var folder = Db.GetFolderById(id); if (folder is null || !user.HasAccessTo(folder)) return Unauthorized(); return folder .UsersWithDirectAccess() .Where(u => u.IsDescendantOf(user)) .Select(u => u.HidePassword()) .ToList(); } [HttpGet(nameof(GetUsersWithInheritedAccessToFolder))] public ActionResult> GetUsersWithInheritedAccessToFolder(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var folder = Db.GetFolderById(id); if (folder is null || !user.HasAccessTo(folder)) return Unauthorized(); return folder .Ancestors() .SelectMany(f => f.UsersWithDirectAccess() .Where(u => u.IsDescendantOf(user)) .Select(u => new { folderId = f.Id, folderName = f.Name, user = u.HidePassword() })) .ToList(); } [HttpGet(nameof(GetFolderById))] public ActionResult GetFolderById(Int64 id, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var folder = Db.GetFolderById(id); if (folder is null || !user.HasAccessTo(folder)) return Unauthorized(); return folder.HideParentIfUserHasNoAccessToParent(user); } [HttpGet(nameof(GetAllDirectChildUsers))] public ActionResult> GetAllDirectChildUsers(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); return user.ChildUsers().Select(u => u.HidePassword()).ToList(); } [HttpGet(nameof(GetAllChildUsers))] public ActionResult> GetAllChildUsers(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); return user .DescendantUsers() .Select(u => u.HidePassword()) .ToList(); } [HttpGet(nameof(GetAllInstallationsFromProduct))] public ActionResult> GetAllInstallationsFromProduct(int product,Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return user .AccessibleInstallations(product) .Select(i => i.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(user).HideWriteKeyIfUserIsNotAdmin(user.UserType)) .ToList(); } [HttpGet(nameof(GetAllInstallations))] public ActionResult> GetAllInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return user .AccessibleInstallations(product:(int)ProductType.Salimax) .ToList(); } [HttpGet(nameof(GetAllSalidomoInstallations))] public ActionResult> GetAllSalidomoInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return user .AccessibleInstallations(product:(int)ProductType.Salidomo) .ToList(); } [HttpGet(nameof(GetAllSodioHomeInstallations))] public ActionResult> GetAllSodioHomeInstallations(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return user .AccessibleInstallations(product:(int)ProductType.SodioHome) .ToList(); } [HttpGet(nameof(GetAllFolders))] public ActionResult> GetAllFolders(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); return new(user.AccessibleFolders().HideParentIfUserHasNoAccessToParent(user)); } [HttpGet(nameof(GetAllFoldersAndInstallations))] public ActionResult> GetAllFoldersAndInstallations(int productId, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user is null) return Unauthorized(); var foldersAndInstallations = user .AccessibleFoldersAndInstallations() .Do(o => o.FillOrderNumbers()) .Select(o => o.HideParentIfUserHasNoAccessToParent(user)) .OfType(); // Important! JSON serializer must see Objects otherwise // it will just serialize the members of TreeNode %&@#!!! // TODO Filter out write keys return new (foldersAndInstallations); } [HttpPost(nameof(CreateUser))] public async Task> CreateUser([FromBody] User newUser, Token authToken) { var create = Db.GetSession(authToken).Create(newUser); if (create) { var mail_success= await Db.SendNewUserEmail(newUser); if (!mail_success) { Db.GetSession(authToken).Delete(newUser); return StatusCode(500, "Welcome email failed to send"); } return mail_success ? newUser.HidePassword():Unauthorized(); } return Unauthorized() ; } [HttpPost(nameof(CreateInstallation))] public async Task> CreateInstallation([FromBody] Installation installation, Token authToken) { var session = Db.GetSession(authToken); if (! await session.Create(installation)) return Unauthorized(); return installation; } [HttpPost(nameof(CreateFolder))] public ActionResult CreateFolder([FromBody] Folder folder, Token authToken) { var session = Db.GetSession(authToken); if (!session.Create(folder)) return Unauthorized(); return folder.HideParentIfUserHasNoAccessToParent(session!.User); } [HttpPost(nameof(GrantUserAccessToFolder))] public ActionResult GrantUserAccessToFolder(FolderAccess folderAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var folder = Db.GetFolderById(folderAccess.FolderId); var user = Db.GetUserById(folderAccess.UserId); // Check if user already has access - treat as idempotent (success) if (user is not null && folder is not null && user.HasAccessTo(folder)) { Console.WriteLine($"GrantUserAccessToFolder: User {user.Id} ({user.Name}) already has access to folder {folder.Id} ({folder.Name}) - returning success"); return Ok(); } return session.GrantUserAccessTo(user, folder) ? Ok() : Unauthorized(); } [HttpPost(nameof(RevokeUserAccessToFolder))] public ActionResult RevokeUserAccessToFolder(FolderAccess folderAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var folder = Db.GetFolderById(folderAccess.FolderId); var user = Db.GetUserById(folderAccess.UserId); return session.RevokeUserAccessTo(user, folder) ? Ok() : Unauthorized(); } [HttpPost(nameof(GrantUserAccessToInstallation))] public ActionResult GrantUserAccessToInstallation(InstallationAccess installationAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var installation = Db.GetInstallationById(installationAccess.InstallationId); var user = Db.GetUserById(installationAccess.UserId); // Check if user already has access - treat as idempotent (success) if (user is not null && installation is not null && user.HasAccessTo(installation)) { Console.WriteLine($"GrantUserAccessToInstallation: User {user.Id} ({user.Name}) already has access to installation {installation.Id} ({installation.Name}) - returning success"); return Ok(); } return session.GrantUserAccessTo(user, installation) ? Ok() : Unauthorized(); } [HttpPost(nameof(RevokeUserAccessToInstallation))] public ActionResult RevokeUserAccessToInstallation(InstallationAccess installationAccess, Token authToken) { var session = Db.GetSession(authToken); // TODO: automatic BadRequest when properties are null during deserialization var installation = Db.GetInstallationById(installationAccess.InstallationId); var user = Db.GetUserById(installationAccess.UserId); return session.RevokeUserAccessTo(user, installation) ? Ok() : Unauthorized(); } [HttpPut(nameof(UpdateUser))] public ActionResult UpdateUser([FromBody] User updatedUser, Token authToken) { var session = Db.GetSession(authToken); if (!session.Update(updatedUser)) return Unauthorized(); return updatedUser.HidePassword(); } [HttpPut(nameof(UpdatePassword))] public ActionResult UpdatePassword(String newPassword, Token authToken) { var session = Db.GetSession(authToken); return session.UpdatePassword(newPassword) ? Ok() : Unauthorized(); } [HttpPut(nameof(UpdateInstallation))] public ActionResult UpdateInstallation([FromBody] Installation installation, Token authToken) { var session = Db.GetSession(authToken); if (!session.Update(installation)) return Unauthorized(); if (installation.Product == (int)ProductType.Salimax) { return installation.FillOrderNumbers().HideParentIfUserHasNoAccessToParent(session!.User).HideWriteKeyIfUserIsNotAdmin(session.User.UserType); } return installation.HideParentIfUserHasNoAccessToParent(session!.User); } [HttpPost(nameof(AcknowledgeError))] public ActionResult AcknowledgeError(Int64 id, Token authToken) { var session = Db.GetSession(authToken); if (session == null) return Unauthorized(); var error=Db.Errors .FirstOrDefault(error => error.Id == id); error.Seen = true; return Db.Update(error) ? Ok() : Unauthorized(); } [HttpPost(nameof(AcknowledgeWarning))] public ActionResult AcknowledgeWarning(Int64 id, Token authToken) { var session = Db.GetSession(authToken); if (session == null) return Unauthorized(); var warning=Db.Warnings .FirstOrDefault(warning => warning.Id == id); warning.Seen = true; return Db.Update(warning) ? Ok() : Unauthorized(); } /// /// Returns an AI-generated diagnosis for a single error/alarm description. /// Responses are cached in memory — repeated calls for the same error code /// do not hit Mistral again. /// [HttpGet(nameof(DiagnoseError))] public async Task> DiagnoseError(Int64 installationId, string errorDescription, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); // AI diagnostics are scoped to SodistoreHome and SodiStoreMax only if (installation.Product != (int)ProductType.SodioHome && installation.Product != (int)ProductType.SodiStoreMax) return BadRequest("AI diagnostics not available for this product."); var result = await DiagnosticService.DiagnoseAsync(installationId, errorDescription, user.Language ?? "en"); if (result is null) return NoContent(); // no diagnosis available (not in knowledge base, no API key) return result; } /// /// Test endpoint for AlarmKnowledgeBase - no authentication required. /// Tests multiple Sinexcel and Growatt alarms to verify the knowledge base works. /// Remove this endpoint in production if not needed. /// [HttpGet(nameof(TestAlarmKnowledgeBase))] public ActionResult TestAlarmKnowledgeBase() { var testCases = new[] { // Sinexcel alarms (keys match SinexcelRecord.Api.cs property names) "FanFault", "AbnormalGridVoltage", "Battery1NotConnected", "InverterPowerTubeFault", "IslandProtection", // Growatt alarms (keys match GrowattWarningCode/GrowattErrorCode enum names) "NoUtilityGrid", "BatteryCommunicationFailure", "BmsFault", "OverTemperature", "AFCI Fault", // Unknown alarm (should return null - would call Mistral) "Some unknown alarm XYZ123" }; var results = new List(); foreach (var alarm in testCases) { var diagnosis = AlarmKnowledgeBase.TryGetDiagnosis(alarm); results.Add(new { Alarm = alarm, FoundInKnowledgeBase = diagnosis != null, Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call Mistral API", CausesCount = diagnosis?.Causes.Count ?? 0, NextStepsCount = diagnosis?.NextSteps.Count ?? 0 }); } return Ok(new { TestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TotalTests = testCases.Length, FoundInKnowledgeBase = results.Count(r => ((dynamic)r).FoundInKnowledgeBase), WouldCallMistral = results.Count(r => !((dynamic)r).FoundInKnowledgeBase), Results = results }); } /// /// Test endpoint for the full AI diagnostic flow (knowledge base + Mistral API). /// No auth required. Remove before production. /// Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm /// [HttpGet(nameof(TestDiagnoseError))] public async Task TestDiagnoseError(string errorDescription = "AbnormalGridVoltage", string language = "en") { // 1. Try static lookup (KB for English, pre-generated translations for others) var staticResult = DiagnosticService.TryGetTranslation(errorDescription, language); if (staticResult is not null) { return Ok(new { Source = "KnowledgeBase", Alarm = errorDescription, MistralEnabled = DiagnosticService.IsEnabled, staticResult.Explanation, staticResult.Causes, staticResult.NextSteps }); } // 2. If not found, try Mistral with the correct language if (!DiagnosticService.IsEnabled) return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and Mistral API key not configured." }); var aiResult = await DiagnosticService.TestCallMistralAsync(errorDescription, language); if (aiResult is null) return Ok(new { Source = "MistralFailed", Alarm = errorDescription, Message = "Mistral API call failed or returned empty." }); return Ok(new { Source = "MistralAI", Alarm = errorDescription, aiResult.Explanation, aiResult.Causes, aiResult.NextSteps }); } // ── Weekly Performance Report ────────────────────────────────────── /// /// Generates a weekly performance report from an Excel file in tmp_report/{installationId}.xlsx /// Returns JSON with daily data, weekly totals, ratios, and AI insight. /// [HttpGet(nameof(GetWeeklyReport))] public async Task> GetWeeklyReport( Int64 installationId, Token authToken, String? language = null, String? weekStart = null) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); // Parse optional weekStart override (must be a Monday; any valid date accepted for flexibility) DateOnly? weekStartDate = null; if (!String.IsNullOrEmpty(weekStart)) { if (!DateOnly.TryParseExact(weekStart, "yyyy-MM-dd", out var parsed)) return BadRequest("weekStart must be in yyyy-MM-dd format."); weekStartDate = parsed; } try { var lang = language ?? user.Language ?? "en"; var report = await WeeklyReportService.GenerateReportAsync( installationId, installation.InstallationName, lang, weekStartDate); // Persist weekly summary and seed AiInsightCache for this language ReportAggregationService.SaveWeeklySummary(installationId, report, lang); return Ok(report); } catch (Exception ex) { Console.Error.WriteLine($"[GetWeeklyReport] Error: {ex.Message}"); return BadRequest($"Failed to generate report: {ex.Message}"); } } /// /// Sends the weekly report as a formatted HTML email. /// [HttpPost(nameof(SendWeeklyReportEmail))] public async Task SendWeeklyReportEmail(Int64 installationId, string emailAddress, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); try { var lang = user.Language ?? "en"; var report = await WeeklyReportService.GenerateReportAsync(installationId, installation.InstallationName, lang); await ReportEmailService.SendReportEmailAsync(report, emailAddress, lang); return Ok(new { message = $"Report sent to {emailAddress}" }); } catch (Exception ex) { Console.Error.WriteLine($"[SendWeeklyReportEmail] Error: {ex.Message}"); return BadRequest($"Failed to send report: {ex.Message}"); } } // ── Monthly & Yearly Reports ───────────────────────────────────── [HttpGet(nameof(GetPendingMonthlyAggregations))] public ActionResult> GetPendingMonthlyAggregations(Int64 installationId, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Ok(ReportAggregationService.GetPendingMonthlyAggregations(installationId)); } [HttpGet(nameof(GetPendingYearlyAggregations))] public ActionResult> GetPendingYearlyAggregations(Int64 installationId, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); return Ok(ReportAggregationService.GetPendingYearlyAggregations(installationId)); } [HttpGet(nameof(GetMonthlyReports))] public async Task>> GetMonthlyReports( Int64 installationId, Token authToken, String? language = null) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var lang = language ?? user.Language ?? "en"; var reports = Db.GetMonthlyReports(installationId); foreach (var report in reports) report.AiInsight = await ReportAggregationService.GetOrGenerateMonthlyInsightAsync(report, lang); return Ok(reports); } [HttpGet(nameof(GetYearlyReports))] public async Task>> GetYearlyReports( Int64 installationId, Token authToken, String? language = null) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var lang = language ?? user.Language ?? "en"; var reports = Db.GetYearlyReports(installationId); foreach (var report in reports) report.AiInsight = await ReportAggregationService.GetOrGenerateYearlyInsightAsync(report, lang); return Ok(reports); } /// /// Manually trigger monthly aggregation for an installation. /// Computes monthly report from daily records for the specified year/month. /// [HttpPost(nameof(TriggerMonthlyAggregation))] public async Task TriggerMonthlyAggregation(Int64 installationId, Int32 year, Int32 month, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); if (month < 1 || month > 12) return BadRequest("Month must be between 1 and 12."); try { var lang = user.Language ?? "en"; var dayCount = await ReportAggregationService.TriggerMonthlyAggregationAsync(installationId, year, month, lang); if (dayCount == 0) return NotFound($"No daily records found for {year}-{month:D2}."); return Ok(new { message = $"Monthly report created from {dayCount} daily records for {year}-{month:D2}." }); } catch (Exception ex) { Console.Error.WriteLine($"[TriggerMonthlyAggregation] Error: {ex.Message}"); return BadRequest($"Failed to aggregate: {ex.Message}"); } } /// /// Manually trigger yearly aggregation for an installation. /// Aggregates monthly reports for the specified year into a yearly report. /// [HttpPost(nameof(TriggerYearlyAggregation))] public async Task TriggerYearlyAggregation(Int64 installationId, Int32 year, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); try { var lang = user.Language ?? "en"; var monthCount = await ReportAggregationService.TriggerYearlyAggregationAsync(installationId, year, lang); if (monthCount == 0) return NotFound($"No monthly reports found for {year}."); return Ok(new { message = $"Yearly report created from {monthCount} monthly reports for {year}." }); } catch (Exception ex) { Console.Error.WriteLine($"[TriggerYearlyAggregation] Error: {ex.Message}"); return BadRequest($"Failed to aggregate: {ex.Message}"); } } /// /// Manually trigger xlsx ingestion for all SodioHome installations. /// Scans tmp_report/ for all matching xlsx files and ingests any new days. /// [HttpPost(nameof(IngestAllDailyData))] public async Task IngestAllDailyData(Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); try { await DailyIngestionService.IngestAllInstallationsAsync(); return Ok(new { message = "Daily data ingestion triggered for all installations." }); } catch (Exception ex) { Console.Error.WriteLine($"[IngestAllDailyData] Error: {ex.Message}"); return BadRequest($"Failed to ingest: {ex.Message}"); } } /// /// Manually trigger xlsx ingestion for one installation. /// Parses tmp_report/{installationId}*.xlsx and stores any new days in DailyEnergyRecord. /// [HttpPost(nameof(IngestDailyData))] public async Task IngestDailyData(Int64 installationId, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); try { await DailyIngestionService.IngestInstallationAsync(installationId); return Ok(new { message = $"Daily data ingestion triggered for installation {installationId}." }); } catch (Exception ex) { Console.Error.WriteLine($"[IngestDailyData] Error: {ex.Message}"); return BadRequest($"Failed to ingest: {ex.Message}"); } } // ── Debug / Inspection Endpoints ────────────────────────────────── /// /// Returns the stored DailyEnergyRecord rows for an installation and date range. /// Use this to verify that xlsx ingestion worked correctly before generating reports. /// [HttpGet(nameof(GetDailyRecords))] public ActionResult> GetDailyRecords( Int64 installationId, String from, String to, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); if (!DateOnly.TryParseExact(from, "yyyy-MM-dd", out var fromDate) || !DateOnly.TryParseExact(to, "yyyy-MM-dd", out var toDate)) return BadRequest("from and to must be in yyyy-MM-dd format."); var records = Db.GetDailyRecords(installationId, fromDate, toDate); return Ok(new { count = records.Count, records }); } /// /// Deletes DailyEnergyRecord rows for an installation in the given date range. /// Safe to use during testing — only removes daily records, not report summaries. /// Allows re-ingesting the same xlsx files after correcting data. /// [HttpDelete(nameof(DeleteDailyRecords))] public ActionResult DeleteDailyRecords( Int64 installationId, String from, String to, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); if (!DateOnly.TryParseExact(from, "yyyy-MM-dd", out var fromDate) || !DateOnly.TryParseExact(to, "yyyy-MM-dd", out var toDate)) return BadRequest("from and to must be in yyyy-MM-dd format."); var fromStr = fromDate.ToString("yyyy-MM-dd"); var toStr = toDate.ToString("yyyy-MM-dd"); var toDelete = Db.DailyRecords .Where(r => r.InstallationId == installationId) .ToList() .Where(r => String.Compare(r.Date, fromStr, StringComparison.Ordinal) >= 0 && String.Compare(r.Date, toStr, StringComparison.Ordinal) <= 0) .ToList(); foreach (var record in toDelete) Db.DailyRecords.Delete(r => r.Id == record.Id); Console.WriteLine($"[DeleteDailyRecords] Deleted {toDelete.Count} records for installation {installationId} ({from}–{to})."); return Ok(new { deleted = toDelete.Count, from, to }); } [HttpPost(nameof(SendMonthlyReportEmail))] public async Task SendMonthlyReportEmail(Int64 installationId, Int32 year, Int32 month, String emailAddress, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var report = Db.GetMonthlyReports(installationId).FirstOrDefault(r => r.Year == year && r.Month == month); if (report == null) return BadRequest($"No monthly report found for {year}-{month:D2}."); try { var lang = user.Language ?? "en"; report.AiInsight = await ReportAggregationService.GetOrGenerateMonthlyInsightAsync(report, lang); await ReportEmailService.SendMonthlyReportEmailAsync(report, installation.InstallationName, emailAddress, lang); return Ok(new { message = $"Monthly report sent to {emailAddress}" }); } catch (Exception ex) { Console.Error.WriteLine($"[SendMonthlyReportEmail] Error: {ex.Message}"); return BadRequest($"Failed to send report: {ex.Message}"); } } [HttpPost(nameof(SendYearlyReportEmail))] public async Task SendYearlyReportEmail(Int64 installationId, Int32 year, String emailAddress, Token authToken) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var report = Db.GetYearlyReports(installationId).FirstOrDefault(r => r.Year == year); if (report == null) return BadRequest($"No yearly report found for {year}."); try { var lang = user.Language ?? "en"; report.AiInsight = await ReportAggregationService.GetOrGenerateYearlyInsightAsync(report, lang); await ReportEmailService.SendYearlyReportEmailAsync(report, installation.InstallationName, emailAddress, lang); return Ok(new { message = $"Yearly report sent to {emailAddress}" }); } catch (Exception ex) { Console.Error.WriteLine($"[SendYearlyReportEmail] Error: {ex.Message}"); return BadRequest($"Failed to send report: {ex.Message}"); } } [HttpGet(nameof(GetWeeklyReportSummaries))] public async Task>> GetWeeklyReportSummaries( Int64 installationId, Int32 year, Int32 month, Token authToken, String? language = null) { var user = Db.GetSession(authToken)?.User; if (user == null) return Unauthorized(); var installation = Db.GetInstallationById(installationId); if (installation is null || !user.HasAccessTo(installation)) return Unauthorized(); var lang = language ?? user.Language ?? "en"; var summaries = Db.GetWeeklyReportsForMonth(installationId, year, month); foreach (var s in summaries) s.AiInsight = await ReportAggregationService.GetOrGenerateWeeklyInsightAsync(s, lang); return Ok(summaries); } [HttpPut(nameof(UpdateFolder))] public ActionResult UpdateFolder([FromBody] Folder folder, Token authToken) { var session = Db.GetSession(authToken); if (!session.Update(folder)) return Unauthorized(); return folder.HideParentIfUserHasNoAccessToParent(session!.User); } [HttpPut(nameof(MoveInstallation))] public ActionResult MoveInstallation(Int64 installationId,Int64 parentId, Token authToken) { var session = Db.GetSession(authToken); return session.MoveInstallation(installationId, parentId) ? Ok() : Unauthorized(); } [HttpPost(nameof(UpdateFirmware))] public async Task UpdateFirmware(Int64 batteryNode, Int64 installationId,String version,Token authToken) { var session = Db.GetSession(authToken); var installationToUpdate = Db.GetInstallationById(installationId); if (installationToUpdate != null) { _ = session.RunScriptInBackground(installationToUpdate.VpnIp, batteryNode,version,installationToUpdate.Product); } return Ok(); } private static Dictionary JobStatuses = new Dictionary(); [HttpPost("StartDownloadBatteryLog")] public async Task> StartDownloadBatteryLog(long batteryNode, long installationId, Token authToken) { var session = Db.GetSession(authToken); var installationToDownload = Db.GetInstallationById(installationId); if (installationToDownload != null) { string jobId = Guid.NewGuid().ToString(); _ = Task.Run(async () => { await session.RunDownloadLogScript(installationToDownload.VpnIp, batteryNode, installationToDownload.Product); string fileName = $"{installationToDownload.VpnIp}-node{batteryNode}-{DateTime.Now:dd-MM-yyyy}.bin"; string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}"; if (System.IO.File.Exists(filePath)) { SaveJobStatus(jobId, "Completed", fileName:fileName); } else { SaveJobStatus(jobId, "Failed"); } }); // Store initial job status in in-memory storage SaveJobStatus(jobId, "Processing"); return Ok(jobId); } return NotFound(); } [HttpGet("DownloadBatteryLog")] public async Task DownloadBatteryLog(string jobId) { Console.WriteLine("-----------------------------------Start uploading battery log-----------------------------------"); var jobStatus = JobStatuses.TryGetValue(jobId, out var status) ? status : null; if (jobStatus == null || jobStatus.Status != "Completed" || string.IsNullOrEmpty(jobStatus.FileName)) { return NotFound(); } string fileName = jobStatus.FileName; string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}"; if (!System.IO.File.Exists(filePath)) { return NotFound(); } string contentType = "application/octet-stream"; var memory = new MemoryStream(); await using (var stream = new FileStream(filePath, FileMode.Open)) { await stream.CopyToAsync(memory); } memory.Position = 0; var fileContentResult = new FileContentResult(memory.ToArray(), contentType) { //FileDownloadName = Path.GetFileName(filePath) FileDownloadName = fileName }; Console.WriteLine("-----------------------------------Stop uploading battery log-----------------------------------"); return fileContentResult; } [HttpDelete("DeleteBatteryLog")] public IActionResult DeleteBatteryLog(string fileName) { Console.WriteLine("-----------------------------------Start deleting downloaded battery log-----------------------------------"); string filePath = $"/home/ubuntu/backend/downloadBatteryLog/{fileName}"; try { if (System.IO.File.Exists(filePath)) { System.IO.File.Delete(filePath); Console.WriteLine("-----------------------------------Stop deleting downloaded battery log-----------------------------------"); return Ok(); } else { return NotFound("File not found."); } } catch (Exception ex) { return StatusCode(500, $"Internal server error: {ex.Message}"); } } private void SaveJobStatus(string jobId, string status, string fileName = null) { JobStatuses[jobId] = new JobStatus { JobId = jobId, Status = status, FileName = fileName, StartTime = DateTime.UtcNow // Initialize StartTime when saving }; } [HttpGet("GetJobResult")] public ActionResult GetJobResult(string jobId) { if (string.IsNullOrEmpty(jobId)) { return BadRequest(new { status = "Error", message = "Job ID is required." }); } if (!JobStatuses.TryGetValue(jobId, out var jobStatus)) { return NotFound(); } if (jobStatus.Status == "Completed") { return Ok(new { status = "Completed", fileName = jobStatus.FileName }); } else if (jobStatus.Status == "Failed") { return StatusCode(500, new { status = "Failed", message = "Job processing failed." }); } else if (jobStatus.Status == "Processing") { // Check for timeout var startTime = jobStatus.StartTime; var currentTime = DateTime.UtcNow; if ((currentTime - startTime).TotalMinutes > 60)//60 minutes as timeout => Running multiple tasks in parallel on a crowded backend server will increase the time each task takes to complete { return StatusCode(500, new { status = "Failed", message = "Job in back end timeout exceeded." }); } return Ok(new { status = "Processing" }); } else { return BadRequest(new { status = "Unknown", message = "Unknown job status." }); } } [HttpPost(nameof(InsertNewAction))] public async Task>> InsertNewAction([FromBody] UserAction action, Token authToken) { var session = Db.GetSession(authToken); var actionSuccess = await session.InsertUserAction(action); return actionSuccess ? Ok() : Unauthorized(); } [HttpPost(nameof(UpdateAction))] public async Task>> UpdateAction([FromBody] UserAction action, Token authToken) { var session = Db.GetSession(authToken); var actionSuccess = await session.UpdateUserAction(action); return actionSuccess ? Ok() : Unauthorized(); } [HttpPost(nameof(DeleteAction))] public async Task>> DeleteAction(Int64 actionId, Token authToken) { var session = Db.GetSession(authToken); var actionSuccess = await session.DeleteUserAction(actionId); return actionSuccess ? Ok() : Unauthorized(); } [HttpPost(nameof(EditInstallationConfig))] public async Task>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,int product,Token authToken) { var session = Db.GetSession(authToken); string configString = product switch { 0 => config.GetConfigurationSalimax(), // Salimax 3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax 2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome _ => config.GetConfigurationString() // fallback }; Console.WriteLine("CONFIG IS " + configString); //Send configuration changes var success = await session.SendInstallationConfig(installationId, config); // Record configuration change if (success) { // // Update Configuration colum in Installation table // var installation = Db.GetInstallationById(installationId); // // installation.Configuration = JsonConvert.SerializeObject(config); // // if (!installation.Apply(Db.Update)) // return StatusCode(500, "Failed to update installation configuration in database"); var action = new UserAction { InstallationId = installationId, Timestamp = DateTime.UtcNow, Description = configString }; var actionSuccess = await session.InsertUserAction(action); return actionSuccess ? Ok() : StatusCode(500, "Failed to record Configuration changes in History of Action"); } return Unauthorized(); } [HttpPut(nameof(MoveFolder))] public ActionResult MoveFolder(Int64 folderId,Int64 parentId, Token authToken) { var session = Db.GetSession(authToken); return session.MoveFolder(folderId, parentId) ? Ok() : Unauthorized(); } [HttpDelete(nameof(DeleteUser))] public ActionResult DeleteUser(Int64 userId, Token authToken) { var session = Db.GetSession(authToken); var user = Db.GetUserById(userId); return session.Delete(user) ? Ok() : Unauthorized(); } [HttpDelete(nameof(DeleteInstallation))] public async Task DeleteInstallation(Int64 installationId, Token authToken) { var session = Db.GetSession(authToken); var installation = Db.GetInstallationById(installationId); return await session.Delete(installation) ? Ok() : Unauthorized(); } [HttpDelete(nameof(DeleteFolder))] public ActionResult DeleteFolder(Int64 folderId, Token authToken) { var session = Db.GetSession(authToken); var folder = Db.GetFolderById(folderId); return session.Delete(folder) ? Ok() : Unauthorized(); } [HttpPost(nameof(ResetPasswordRequest))] public async Task>> ResetPasswordRequest(String username) { var user = Db.GetUserByEmail(username); if (user is null) return Unauthorized(); var session = new Session(user.HidePassword().HideParentIfUserHasNoAccessToParent(user)); var success = Db.Create(session); return success && await Db.SendPasswordResetEmail(user, session.Token) ? Ok() : Unauthorized(); } [HttpGet(nameof(ResetPassword))] public ActionResult ResetPassword(Token token) { var user = Db.GetSession(token)?.User; if (user is null) return Unauthorized(); Db.DeleteUserPassword(user); return Redirect($"https://monitor.inesco.energy/?username={user.Email}&reset=true"); // TODO: move to settings file } // ── Alarm Review Campaign ──────────────────────────────────────────────── [HttpPost(nameof(SendTestAlarmReview))] public async Task SendTestAlarmReview() { await AlarmReviewService.SendTestBatchAsync(); return Ok(new { message = "Test review email sent to liu@inesco.energy. Check your inbox." }); } [HttpPost(nameof(StartAlarmReviewCampaign))] public ActionResult StartAlarmReviewCampaign() { AlarmReviewService.StartCampaign(); return Ok(new { message = "Alarm review campaign started." }); } [HttpPost(nameof(StopAlarmReviewCampaign))] public ActionResult StopAlarmReviewCampaign() { AlarmReviewService.StopCampaign(); return Ok(new { message = "Campaign paused — progress preserved. Use ResumeAlarmReviewCampaign to restart timers." }); } [HttpPost(nameof(ResumeAlarmReviewCampaign))] public ActionResult ResumeAlarmReviewCampaign() { AlarmReviewService.ResumeCampaign(); return Ok(new { message = "Campaign resumed — timers restarted from existing progress." }); } [HttpPost(nameof(ResetAlarmReviewCampaign))] public ActionResult ResetAlarmReviewCampaign() { AlarmReviewService.ResetCampaign(); return Ok(new { message = "Campaign fully reset — all progress deleted. Use StartAlarmReviewCampaign to begin again." }); } [HttpGet(nameof(CorrectAlarm))] public ActionResult CorrectAlarm(int batch, string key) { var html = AlarmReviewService.GetCorrectionPage(batch, key); return Content(html, "text/html"); } [HttpPost(nameof(ApplyAlarmCorrection))] public ActionResult ApplyAlarmCorrection([FromBody] AlarmCorrectionRequest req) { if (req == null) return BadRequest(); var correction = new DiagnosticResponse { Explanation = req.Explanation ?? "", Causes = req.Causes ?? new List(), NextSteps = req.NextSteps ?? new List(), }; var ok = AlarmReviewService.ApplyCorrection(req.BatchNumber, req.AlarmKey ?? "", correction); return ok ? Ok(new { message = "Korrektur gespeichert." }) : BadRequest("Batch or alarm not found."); } [HttpGet(nameof(ReviewAlarms))] public ActionResult ReviewAlarms(int batch, string reviewer) { var html = AlarmReviewService.GetReviewPage(batch, reviewer); if (html is null) return NotFound("Batch not found or reviewer not recognised."); return Content(html, "text/html"); } [HttpPost(nameof(SubmitAlarmReview))] public async Task SubmitAlarmReview(int batch, string? reviewer, [FromBody] List? feedbacks) { // Batch 0 = test mode — run dry-run synthesis and return preview HTML (nothing is saved) if (batch == 0) { var previewHtml = await AlarmReviewService.PreviewSynthesisAsync(feedbacks); return Ok(new { preview = previewHtml }); } var ok = AlarmReviewService.SubmitFeedback(batch, reviewer, feedbacks); return ok ? Ok(new { message = "Feedback saved. Thank you!" }) : BadRequest("Batch not found, reviewer not recognised, or already submitted."); } [HttpGet(nameof(GetAlarmReviewStatus))] public ActionResult GetAlarmReviewStatus() { return Ok(AlarmReviewService.GetStatus()); } [HttpGet(nameof(DownloadCheckedKnowledgeBase))] public ActionResult DownloadCheckedKnowledgeBase() { var content = AlarmReviewService.GetCheckedFileContent(); if (content is null) return NotFound("AlarmKnowledgeBaseChecked.cs has not been generated yet."); return File(System.Text.Encoding.UTF8.GetBytes(content), "text/plain", "AlarmKnowledgeBaseChecked.cs"); } }