2074 lines
77 KiB
C#
2074 lines
77 KiB
C#
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 if (installation.Product == (int)ProductType.SodistoreGrid)
|
||
bucketPath = "s3://" + installation.S3BucketId + "-5109c126-e141-43ab-8658-f3c44c838ae8/" + 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(GetAllSodistoreGridInstallations))]
|
||
public ActionResult<IEnumerable<Installation>> GetAllSodistoreGridInstallations(Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
|
||
if (user is null)
|
||
return Unauthorized();
|
||
|
||
return user
|
||
.AccessibleInstallations(product:(int)ProductType.SodistoreGrid)
|
||
.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);
|
||
}
|
||
|
||
[HttpGet(nameof(GetNetworkProviders))]
|
||
public ActionResult<IReadOnlyList<string>> GetNetworkProviders(Token authToken)
|
||
{
|
||
var session = Db.GetSession(authToken);
|
||
if (session is null)
|
||
return Unauthorized();
|
||
|
||
return Ok(NetworkProviderService.GetProviders());
|
||
}
|
||
|
||
[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>
|
||
/// Returns a weekly performance report. Serves from cache if available;
|
||
/// generates fresh on first request or when forceRegenerate is true.
|
||
/// </summary>
|
||
[HttpGet(nameof(GetWeeklyReport))]
|
||
public async Task<ActionResult<WeeklyReportResponse>> GetWeeklyReport(
|
||
Int64 installationId, Token authToken, String? language = null,
|
||
String? weekStart = null, Boolean forceRegenerate = false)
|
||
{
|
||
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";
|
||
|
||
// Compute target week dates for cache lookup
|
||
DateOnly periodStart, periodEnd;
|
||
if (weekStartDate.HasValue)
|
||
{
|
||
periodStart = weekStartDate.Value;
|
||
periodEnd = weekStartDate.Value.AddDays(6);
|
||
}
|
||
else
|
||
{
|
||
(periodStart, periodEnd) = WeeklyReportService.LastCalendarWeek();
|
||
}
|
||
|
||
var periodStartStr = periodStart.ToString("yyyy-MM-dd");
|
||
var periodEndStr = periodEnd.ToString("yyyy-MM-dd");
|
||
|
||
// Cache-first: check if a cached report exists for this week
|
||
if (!forceRegenerate)
|
||
{
|
||
var cached = Db.GetWeeklyReportForWeek(installationId, periodStartStr, periodEndStr);
|
||
if (cached != null)
|
||
{
|
||
var cachedResponse = await ReportAggregationService.ToWeeklyReportResponseAsync(cached, lang);
|
||
if (cachedResponse != null)
|
||
{
|
||
Console.WriteLine($"[GetWeeklyReport] Serving cached report for installation {installationId}, period {periodStartStr}–{periodEndStr}, language={lang}");
|
||
return Ok(cachedResponse);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Cache miss or forceRegenerate: generate fresh
|
||
Console.WriteLine($"[GetWeeklyReport] Generating fresh report for installation {installationId}, period {periodStartStr}–{periodEndStr}");
|
||
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, user.Name);
|
||
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 });
|
||
}
|
||
|
||
[HttpGet(nameof(GetHourlyRecords))]
|
||
public ActionResult<List<HourlyEnergyRecord>> GetHourlyRecords(
|
||
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.GetHourlyRecords(installationId, fromDate, toDate);
|
||
return Ok(new { count = records.Count, records });
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns daily + hourly records for a date range.
|
||
/// DB first; if empty, falls back to xlsx parsing and caches results for future calls.
|
||
/// </summary>
|
||
[HttpGet(nameof(GetDailyDetailRecords))]
|
||
public ActionResult GetDailyDetailRecords(
|
||
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.");
|
||
|
||
// 1. Try DB
|
||
var dailyRecords = Db.GetDailyRecords(installationId, fromDate, toDate);
|
||
var hourlyRecords = Db.GetHourlyRecords(installationId, fromDate, toDate);
|
||
|
||
// 2. Fallback: parse xlsx + cache to DB
|
||
if (dailyRecords.Count == 0 || hourlyRecords.Count == 0)
|
||
{
|
||
var xlsxFiles = WeeklyReportService.GetRelevantXlsxFiles(installationId, fromDate, toDate);
|
||
if (xlsxFiles.Count > 0)
|
||
{
|
||
foreach (var xlsxPath in xlsxFiles)
|
||
{
|
||
if (dailyRecords.Count == 0)
|
||
{
|
||
foreach (var day in ExcelDataParser.Parse(xlsxPath))
|
||
{
|
||
if (Db.DailyRecordExists(installationId, day.Date))
|
||
continue;
|
||
Db.Create(new DailyEnergyRecord
|
||
{
|
||
InstallationId = installationId,
|
||
Date = day.Date,
|
||
PvProduction = day.PvProduction,
|
||
LoadConsumption = day.LoadConsumption,
|
||
GridImport = day.GridImport,
|
||
GridExport = day.GridExport,
|
||
BatteryCharged = day.BatteryCharged,
|
||
BatteryDischarged = day.BatteryDischarged,
|
||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||
});
|
||
}
|
||
}
|
||
|
||
if (hourlyRecords.Count == 0)
|
||
{
|
||
foreach (var hour in ExcelDataParser.ParseHourly(xlsxPath))
|
||
{
|
||
var dateHour = $"{hour.DateTime:yyyy-MM-dd HH}";
|
||
if (Db.HourlyRecordExists(installationId, dateHour))
|
||
continue;
|
||
Db.Create(new HourlyEnergyRecord
|
||
{
|
||
InstallationId = installationId,
|
||
Date = hour.DateTime.ToString("yyyy-MM-dd"),
|
||
Hour = hour.Hour,
|
||
DateHour = dateHour,
|
||
DayOfWeek = hour.DayOfWeek,
|
||
IsWeekend = hour.IsWeekend,
|
||
PvKwh = hour.PvKwh,
|
||
LoadKwh = hour.LoadKwh,
|
||
GridImportKwh = hour.GridImportKwh,
|
||
BatteryChargedKwh = hour.BatteryChargedKwh,
|
||
BatteryDischargedKwh = hour.BatteryDischargedKwh,
|
||
BattSoC = hour.BattSoC,
|
||
CreatedAt = DateTime.UtcNow.ToString("o"),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Re-read from DB (now cached)
|
||
dailyRecords = Db.GetDailyRecords(installationId, fromDate, toDate);
|
||
hourlyRecords = Db.GetHourlyRecords(installationId, fromDate, toDate);
|
||
}
|
||
}
|
||
|
||
return Ok(new
|
||
{
|
||
dailyRecords = new { count = dailyRecords.Count, records = dailyRecords },
|
||
hourlyRecords = new { count = hourlyRecords.Count, records = hourlyRecords },
|
||
});
|
||
}
|
||
|
||
/// <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, user.Name);
|
||
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, user.Name);
|
||
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
|
||
4 => config.GetConfigurationSodistoreGrid(), // SodistoreGrid
|
||
_ => 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");
|
||
}
|
||
|
||
[HttpGet(nameof(DryRunS3Cleanup))]
|
||
public async Task<ActionResult<String>> DryRunS3Cleanup(Token authToken, long? installationId = null)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user == null)
|
||
return Unauthorized();
|
||
|
||
var result = await DeleteOldData.DeleteOldDataFromS3.DryRun(installationId);
|
||
return Ok(result);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// TICKET ENDPOINTS (admin-only)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[HttpGet(nameof(GetAllTickets))]
|
||
public ActionResult<IEnumerable<Ticket>> GetAllTickets(Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
return Db.GetAllTickets();
|
||
}
|
||
|
||
[HttpGet(nameof(GetTicketsForInstallation))]
|
||
public ActionResult<IEnumerable<Ticket>> GetTicketsForInstallation(Int64 installationId, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
var installation = Db.GetInstallationById(installationId);
|
||
if (installation is null) return NotFound();
|
||
|
||
return Db.GetTicketsForInstallation(installationId);
|
||
}
|
||
|
||
[HttpGet(nameof(GetTicketById))]
|
||
public ActionResult<Ticket> GetTicketById(Int64 id, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
var ticket = Db.GetTicketById(id);
|
||
return ticket is null ? NotFound() : ticket;
|
||
}
|
||
|
||
[HttpPost(nameof(CreateTicket))]
|
||
public ActionResult<Ticket> CreateTicket([FromBody] Ticket ticket, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
ticket.CreatedByUserId = user.Id;
|
||
ticket.CreatedAt = DateTime.UtcNow;
|
||
ticket.UpdatedAt = DateTime.UtcNow;
|
||
ticket.Status = (Int32)TicketStatus.Open;
|
||
|
||
if (!Db.Create(ticket)) return StatusCode(500, "Failed to create ticket.");
|
||
|
||
Db.Create(new TicketTimelineEvent
|
||
{
|
||
TicketId = ticket.Id,
|
||
EventType = (Int32)TimelineEventType.Created,
|
||
Description = $"Ticket created by {user.Name}.",
|
||
ActorType = (Int32)TimelineActorType.Human,
|
||
ActorId = user.Id,
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
|
||
// Fire-and-forget AI diagnosis
|
||
TicketDiagnosticService.DiagnoseTicketAsync(ticket.Id).SupressAwaitWarning();
|
||
|
||
return ticket;
|
||
}
|
||
|
||
[HttpPut(nameof(UpdateTicket))]
|
||
public ActionResult<Ticket> UpdateTicket([FromBody] Ticket ticket, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
var existing = Db.GetTicketById(ticket.Id);
|
||
if (existing is null) return NotFound();
|
||
|
||
ticket.CreatedAt = existing.CreatedAt;
|
||
ticket.CreatedByUserId = existing.CreatedByUserId;
|
||
ticket.UpdatedAt = DateTime.UtcNow;
|
||
|
||
if (ticket.Status != existing.Status)
|
||
{
|
||
if (ticket.Status == (Int32)TicketStatus.Resolved)
|
||
ticket.ResolvedAt = DateTime.UtcNow;
|
||
|
||
Db.Create(new TicketTimelineEvent
|
||
{
|
||
TicketId = ticket.Id,
|
||
EventType = (Int32)TimelineEventType.StatusChanged,
|
||
Description = $"Status changed to {(TicketStatus)ticket.Status}.",
|
||
ActorType = (Int32)TimelineActorType.Human,
|
||
ActorId = user.Id,
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
}
|
||
|
||
return Db.Update(ticket) ? ticket : StatusCode(500, "Update failed.");
|
||
}
|
||
|
||
[HttpDelete(nameof(DeleteTicket))]
|
||
public ActionResult DeleteTicket(Int64 id, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
var ticket = Db.GetTicketById(id);
|
||
if (ticket is null) return NotFound();
|
||
|
||
return Db.Delete(ticket) ? Ok() : StatusCode(500, "Delete failed.");
|
||
}
|
||
|
||
[HttpGet(nameof(GetTicketComments))]
|
||
public ActionResult<IEnumerable<TicketComment>> GetTicketComments(Int64 ticketId, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
return Db.GetCommentsForTicket(ticketId);
|
||
}
|
||
|
||
[HttpPost(nameof(AddTicketComment))]
|
||
public ActionResult<TicketComment> AddTicketComment([FromBody] TicketComment comment, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
var ticket = Db.GetTicketById(comment.TicketId);
|
||
if (ticket is null) return NotFound();
|
||
|
||
comment.AuthorType = (Int32)CommentAuthorType.Human;
|
||
comment.AuthorId = user.Id;
|
||
comment.CreatedAt = DateTime.UtcNow;
|
||
|
||
if (!Db.Create(comment)) return StatusCode(500, "Failed to add comment.");
|
||
|
||
Db.Create(new TicketTimelineEvent
|
||
{
|
||
TicketId = comment.TicketId,
|
||
EventType = (Int32)TimelineEventType.CommentAdded,
|
||
Description = $"Comment added by {user.Name}.",
|
||
ActorType = (Int32)TimelineActorType.Human,
|
||
ActorId = user.Id,
|
||
CreatedAt = DateTime.UtcNow
|
||
});
|
||
|
||
ticket.UpdatedAt = DateTime.UtcNow;
|
||
Db.Update(ticket);
|
||
|
||
return comment;
|
||
}
|
||
|
||
[HttpGet(nameof(GetTicketDetail))]
|
||
public ActionResult<Object> GetTicketDetail(Int64 id, Token authToken)
|
||
{
|
||
var user = Db.GetSession(authToken)?.User;
|
||
if (user is null || user.UserType != 2) return Unauthorized();
|
||
|
||
var ticket = Db.GetTicketById(id);
|
||
if (ticket is null) return NotFound();
|
||
|
||
return new
|
||
{
|
||
ticket,
|
||
comments = Db.GetCommentsForTicket(id),
|
||
diagnosis = Db.GetDiagnosisForTicket(id),
|
||
timeline = Db.GetTimelineForTicket(id)
|
||
};
|
||
}
|
||
|
||
|
||
}
|