Innovenergy_trunk/csharp/App/Backend/Controller.cs

1726 lines
63 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Session> 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<IEnumerable<Error>> 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<IEnumerable<UserAction>> 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<IEnumerable<Warning>> 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<IEnumerable<Int64>> 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<Int64>();
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<Int64>();
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<User> 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<Installation> 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<IEnumerable<Object>> 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<IEnumerable<Object>> GetInstallationsTheUserHasAccess(Int64 userId, Token authToken)
{
var user = Db.GetUserById(userId);
if (user == null)
return Unauthorized();
return user.AccessibleInstallations().ToList();
}
[HttpGet(nameof(GetDirectInstallationAccessForUser))]
public ActionResult<IEnumerable<Object>> 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<Object>();
}
[HttpGet(nameof(GetDirectFolderAccessForUser))]
public ActionResult<IEnumerable<Object>> 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<Object>();
}
[HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
public ActionResult<IEnumerable<Object>> 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<IEnumerable<Object>> 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<IEnumerable<Object>> 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<Folder> 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<IEnumerable<User>> 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<IEnumerable<User>> 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<IEnumerable<Installation>> 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<IEnumerable<Installation>> 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<IEnumerable<Installation>> 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<IEnumerable<Installation>> 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<IEnumerable<Folder>> 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<IEnumerable<Object>> 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<Object>(); // 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<ActionResult<User>> 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<ActionResult<Installation>> 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<Folder> 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<User> 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<User> UpdatePassword(String newPassword, Token authToken)
{
var session = Db.GetSession(authToken);
return session.UpdatePassword(newPassword)
? Ok()
: Unauthorized();
}
[HttpPut(nameof(UpdateInstallation))]
public ActionResult<Installation> 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();
}
/// <summary>
/// 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.
/// </summary>
[HttpGet(nameof(DiagnoseError))]
public async Task<ActionResult<DiagnosticResponse>> 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;
}
/// <summary>
/// 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.
/// </summary>
[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<object>();
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
});
}
/// <summary>
/// Test endpoint for the full AI diagnostic flow (knowledge base + Mistral API).
/// No auth required. Remove before production.
/// Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
/// </summary>
[HttpGet(nameof(TestDiagnoseError))]
public async Task<ActionResult> 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 ──────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
[HttpGet(nameof(GetWeeklyReport))]
public async Task<ActionResult<WeeklyReportResponse>> 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}");
}
}
/// <summary>
/// Sends the weekly report as a formatted HTML email.
/// </summary>
[HttpPost(nameof(SendWeeklyReportEmail))]
public async Task<ActionResult> 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<List<PendingMonth>> 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<List<PendingYear>> 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<ActionResult<List<MonthlyReportSummary>>> 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<ActionResult<List<YearlyReportSummary>>> 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);
}
/// <summary>
/// Manually trigger monthly aggregation for an installation.
/// Computes monthly report from daily records for the specified year/month.
/// </summary>
[HttpPost(nameof(TriggerMonthlyAggregation))]
public async Task<ActionResult> 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}");
}
}
/// <summary>
/// Manually trigger yearly aggregation for an installation.
/// Aggregates monthly reports for the specified year into a yearly report.
/// </summary>
[HttpPost(nameof(TriggerYearlyAggregation))]
public async Task<ActionResult> 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}");
}
}
/// <summary>
/// Manually trigger xlsx ingestion for all SodioHome installations.
/// Scans tmp_report/ for all matching xlsx files and ingests any new days.
/// </summary>
[HttpPost(nameof(IngestAllDailyData))]
public async Task<ActionResult> 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}");
}
}
/// <summary>
/// Manually trigger xlsx ingestion for one installation.
/// Parses tmp_report/{installationId}*.xlsx and stores any new days in DailyEnergyRecord.
/// </summary>
[HttpPost(nameof(IngestDailyData))]
public async Task<ActionResult> 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 ──────────────────────────────────
/// <summary>
/// Returns the stored DailyEnergyRecord rows for an installation and date range.
/// Use this to verify that xlsx ingestion worked correctly before generating reports.
/// </summary>
[HttpGet(nameof(GetDailyRecords))]
public ActionResult<List<DailyEnergyRecord>> 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 });
}
/// <summary>
/// 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.
/// </summary>
[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<ActionResult> 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<ActionResult> 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<ActionResult<List<WeeklyReportSummary>>> 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<Folder> 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<ActionResult> 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<string, JobStatus> JobStatuses = new Dictionary<string, JobStatus>();
[HttpPost("StartDownloadBatteryLog")]
public async Task<ActionResult<string>> 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<ActionResult> 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<ActionResult<IEnumerable<Object>>> 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<ActionResult<IEnumerable<Object>>> 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<ActionResult<IEnumerable<Object>>> 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<ActionResult<IEnumerable<Object>>> 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<ActionResult> 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<ActionResult<IEnumerable<Object>>> 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<Object> 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<ActionResult> 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<string>(),
NextSteps = req.NextSteps ?? new List<string>(),
};
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<ActionResult> SubmitAlarmReview(int batch, string? reviewer, [FromBody] List<ReviewFeedback>? 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");
}
}