Compare commits
7 Commits
a86dc963b2
...
4ac1bc78ab
| Author | SHA1 | Date |
|---|---|---|
|
|
4ac1bc78ab | |
|
|
f82190afc1 | |
|
|
584abe5b53 | |
|
|
7aacddd761 | |
|
|
7df4842980 | |
|
|
79f695f9b4 | |
|
|
25b961dc93 |
|
|
@ -200,6 +200,8 @@ public class Controller : ControllerBase
|
|||
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);
|
||||
|
|
@ -546,6 +548,19 @@ public class Controller : ControllerBase
|
|||
.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))]
|
||||
|
|
@ -1513,6 +1528,7 @@ public class Controller : ControllerBase
|
|||
0 => config.GetConfigurationSalimax(), // Salimax
|
||||
3 => config.GetConfigurationSodistoreMax(), // SodiStoreMax
|
||||
2 => config.GetConfigurationSodistoreHome(), // SodiStoreHome
|
||||
4 => config.GetConfigurationSodistoreGrid(), // SodistoreGrid
|
||||
_ => config.GetConfigurationString() // fallback
|
||||
};
|
||||
|
||||
|
|
@ -1719,6 +1735,17 @@ public class Controller : ControllerBase
|
|||
"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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ public class Configuration
|
|||
$"BatteriesCount: {BatteriesCount}, ClusterNumber: {ClusterNumber}, PvNumber: {PvNumber}, ControlPermission:{ControlPermission}, "+
|
||||
$"SinexcelTimeChargeandDischargePower: {TimeChargeandDischargePower}, SinexcelStartTimeChargeandDischargeDayandTime: {StartTimeChargeandDischargeDayandTime}, SinexcelStopTimeChargeandDischargeDayandTime: {StopTimeChargeandDischargeDayandTime}";
|
||||
}
|
||||
|
||||
// TODO: SodistoreGrid — update configuration fields when defined
|
||||
public string GetConfigurationSodistoreGrid()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public enum CalibrationChargeType
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ public enum ProductType
|
|||
Salimax = 0,
|
||||
Salidomo = 1,
|
||||
SodioHome =2,
|
||||
SodiStoreMax=3
|
||||
SodiStoreMax=3,
|
||||
SodistoreGrid=4
|
||||
}
|
||||
|
||||
public enum StatusType
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ public static class ExoCmd
|
|||
const String method = "iam-role";
|
||||
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
|
||||
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
|
||||
installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
|
||||
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
|
||||
|
||||
|
||||
|
|
@ -263,8 +264,6 @@ public static class ExoCmd
|
|||
|
||||
public static async Task<Boolean> RevokeReadKey(this Installation installation)
|
||||
{
|
||||
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
|
||||
|
||||
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
|
||||
var method = $"access-key/{installation.S3Key}";
|
||||
|
||||
|
|
@ -277,14 +276,28 @@ public static class ExoCmd
|
|||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
||||
|
||||
var response = await client.DeleteAsync(url);
|
||||
return response.IsSuccessStatusCode;
|
||||
try
|
||||
{
|
||||
var response = await client.DeleteAsync(url);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Successfully revoked read key for installation {installation.Id}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Failed to revoke read key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error occurred while revoking read key: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Boolean> RevokeWriteKey(this Installation installation)
|
||||
{
|
||||
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
|
||||
|
||||
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3WriteKey}";
|
||||
var method = $"access-key/{installation.S3WriteKey}";
|
||||
|
||||
|
|
@ -297,8 +310,24 @@ public static class ExoCmd
|
|||
client.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
|
||||
|
||||
var response = await client.DeleteAsync(url);
|
||||
return response.IsSuccessStatusCode;
|
||||
try
|
||||
{
|
||||
var response = await client.DeleteAsync(url);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Successfully revoked write key for installation {installation.Id}.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Failed to revoke write key. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error occurred while revoking write key: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<(String, String)> CreateWriteKey(this Installation installation)
|
||||
|
|
@ -320,6 +349,7 @@ public static class ExoCmd
|
|||
const String method = "iam-role";
|
||||
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:
|
||||
installation.Product==(int)ProductType.SodiStoreMax?Db.Installations.Count(f => f.Product == (int)ProductType.SodiStoreMax) + installation.Name:
|
||||
installation.Product==(int)ProductType.SodistoreGrid?Db.Installations.Count(f => f.Product == (int)ProductType.SodistoreGrid) + installation.Name:
|
||||
Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
|
||||
|
||||
var contentString = $$"""
|
||||
|
|
@ -371,10 +401,35 @@ public static class ExoCmd
|
|||
return await s3Region.PutBucket(installation.BucketName()) != null;
|
||||
}
|
||||
|
||||
public static async Task<Boolean> DeleteBucket(this Installation installation)
|
||||
public static async Task<Boolean> PurgeAndDeleteBucket(this Installation installation)
|
||||
{
|
||||
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
||||
return await s3Region.DeleteBucket(installation.BucketName()) ;
|
||||
var bucket = s3Region.Bucket(installation.BucketName());
|
||||
|
||||
try
|
||||
{
|
||||
var purged = await bucket.PurgeBucket();
|
||||
if (!purged)
|
||||
{
|
||||
Console.WriteLine($"Failed to purge bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var deleted = await s3Region.DeleteBucket(installation.BucketName());
|
||||
if (!deleted)
|
||||
{
|
||||
Console.WriteLine($"Failed to delete bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Successfully purged and deleted bucket {installation.BucketName()} for installation {installation.Id}.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error occurred while purging/deleting bucket {installation.BucketName()}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public static class InstallationMethods
|
|||
private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
|
||||
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
|
||||
private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa";
|
||||
private static readonly String SodistoreGridBucketNameSalt = "5109c126-e141-43ab-8658-f3c44c838ae8";
|
||||
|
||||
public static String BucketName(this Installation installation)
|
||||
{
|
||||
|
|
@ -23,6 +24,11 @@ public static class InstallationMethods
|
|||
return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}";
|
||||
}
|
||||
|
||||
if (installation.Product == (int)ProductType.SodistoreGrid)
|
||||
{
|
||||
return $"{installation.S3BucketId}-{SodistoreGridBucketNameSalt}";
|
||||
}
|
||||
|
||||
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ public static class SessionMethods
|
|||
|
||||
}
|
||||
|
||||
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome)
|
||||
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome || installation.Product == (int)ProductType.SodistoreGrid)
|
||||
{
|
||||
return user is not null
|
||||
&& user.UserType != 0
|
||||
|
|
@ -295,7 +295,7 @@ public static class SessionMethods
|
|||
.Apply(Db.Update);
|
||||
}
|
||||
|
||||
if (installation.Product == (int)ProductType.SodiStoreMax)
|
||||
if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodistoreGrid)
|
||||
{
|
||||
|
||||
return user is not null
|
||||
|
|
@ -324,23 +324,22 @@ public static class SessionMethods
|
|||
{
|
||||
var user = session?.User;
|
||||
|
||||
if (user is not null
|
||||
&& installation is not null
|
||||
&& user.UserType != 0)
|
||||
{
|
||||
if (user is null || installation is null || user.UserType == 0)
|
||||
return false;
|
||||
|
||||
return
|
||||
Db.Delete(installation)
|
||||
&& await installation.RevokeReadKey()
|
||||
&& await installation.RevokeWriteKey()
|
||||
&& await installation.RemoveReadRole()
|
||||
&& await installation.RemoveWriteRole()
|
||||
&& await installation.DeleteBucket();
|
||||
// Try all Exoscale operations independently (don't short-circuit)
|
||||
var readKeyOk = await installation.RevokeReadKey();
|
||||
var writeKeyOk = await installation.RevokeWriteKey();
|
||||
var readRoleOk = await installation.RemoveReadRole();
|
||||
var writeRoleOk = await installation.RemoveWriteRole();
|
||||
var bucketOk = await installation.PurgeAndDeleteBucket();
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!readKeyOk || !writeKeyOk || !readRoleOk || !writeRoleOk || !bucketOk)
|
||||
Console.WriteLine($"[Delete] Partial Exoscale cleanup for installation {installation.Id}: " +
|
||||
$"readKey={readKeyOk}, writeKey={writeKeyOk}, readRole={readRoleOk}, writeRole={writeRoleOk}, bucket={bucketOk}");
|
||||
|
||||
// Always delete from DB (best-effort — admin wants it gone)
|
||||
return Db.Delete(installation);
|
||||
}
|
||||
|
||||
public static Boolean Create(this Session? session, User newUser)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ public static partial class Db
|
|||
Connection.Execute("UPDATE User SET Language = 'fr' WHERE Language = 'french'");
|
||||
Connection.Execute("UPDATE User SET Language = 'it' WHERE Language = 'italian'");
|
||||
|
||||
// One-time migration: rebrand to inesco Energy
|
||||
Connection.Execute("UPDATE Folder SET Name = 'inesco Energy' WHERE Name = 'InnovEnergy'");
|
||||
Connection.Execute("UPDATE Folder SET Name = 'Sodistore Max Installations' WHERE Name = 'SodistoreMax Installations'");
|
||||
Connection.Execute("UPDATE User SET Name = 'inesco Energy Master Admin' WHERE Name = 'InnovEnergy Master Admin'");
|
||||
|
||||
//UpdateKeys();
|
||||
CleanupSessions().SupressAwaitWarning();
|
||||
DeleteSnapshots().SupressAwaitWarning();
|
||||
|
|
|
|||
|
|
@ -102,10 +102,28 @@ public static partial class Db
|
|||
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
|
||||
if (installation.Product == (int)ProductType.Salimax)
|
||||
{
|
||||
//For Salimax, delete the OrderNumber2Installation entries associated with this installation id.
|
||||
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
|
||||
}
|
||||
|
||||
// Clean up AI insight cache entries linked to this installation's reports
|
||||
var weeklyIds = WeeklyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||
var monthlyIds = MonthlyReports.Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||
var yearlyIds = YearlyReports .Where(r => r.InstallationId == installation.Id).Select(r => r.Id).ToList();
|
||||
|
||||
foreach (var id in weeklyIds) AiInsightCaches.Delete(c => c.ReportType == "weekly" && c.ReportId == id);
|
||||
foreach (var id in monthlyIds) AiInsightCaches.Delete(c => c.ReportType == "monthly" && c.ReportId == id);
|
||||
foreach (var id in yearlyIds) AiInsightCaches.Delete(c => c.ReportType == "yearly" && c.ReportId == id);
|
||||
|
||||
// Clean up energy records, report summaries, errors, warnings, and user actions
|
||||
DailyRecords .Delete(r => r.InstallationId == installation.Id);
|
||||
HourlyRecords .Delete(r => r.InstallationId == installation.Id);
|
||||
WeeklyReports .Delete(r => r.InstallationId == installation.Id);
|
||||
MonthlyReports.Delete(r => r.InstallationId == installation.Id);
|
||||
YearlyReports .Delete(r => r.InstallationId == installation.Id);
|
||||
Errors .Delete(e => e.InstallationId == installation.Id);
|
||||
Warnings .Delete(w => w.InstallationId == installation.Id);
|
||||
UserActions .Delete(a => a.InstallationId == installation.Id);
|
||||
|
||||
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +1,157 @@
|
|||
using System.Diagnostics;
|
||||
using InnovEnergy.App.Backend.Database;
|
||||
using InnovEnergy.App.Backend.DataTypes;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using InnovEnergy.App.Backend.DataTypes.Methods;
|
||||
using InnovEnergy.Lib.S3Utils;
|
||||
using InnovEnergy.Lib.S3Utils.DataTypes;
|
||||
|
||||
namespace InnovEnergy.App.Backend.DeleteOldData;
|
||||
|
||||
public class DeleteOldDataFromS3
|
||||
public static class DeleteOldDataFromS3
|
||||
{
|
||||
private static Timer? _cleanupTimer;
|
||||
|
||||
public static void DeleteFrom(Installation installation, int timestamps_to_delete)
|
||||
public static void StartScheduler()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var next = new DateTime(now.Year, now.Month, now.Day, 3, 0, 0, DateTimeKind.Utc);
|
||||
if (next <= now) next = next.AddDays(1);
|
||||
|
||||
string configPath = "/home/ubuntu/.s3cfg";
|
||||
string bucketPath = installation.Product ==(int)ProductType.Salidomo ? $"s3://{installation.S3BucketId}-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e/{timestamps_to_delete}*" : $"s3://{installation.S3BucketId}-3e5b3069-214a-43ee-8d85-57d72000c19d/{timestamps_to_delete}*" ;
|
||||
|
||||
//Console.WriteLine($"Deleting old data from {bucketPath}");
|
||||
|
||||
Console.WriteLine("Deleting data for timestamp prefix: " + timestamps_to_delete);
|
||||
|
||||
try
|
||||
{
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
_cleanupTimer = new Timer(
|
||||
_ =>
|
||||
{
|
||||
FileName = "s3cmd",
|
||||
Arguments = $"--config {configPath} rm {bucketPath}",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using Process process = new Process { StartInfo = startInfo };
|
||||
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
Console.WriteLine("[s3cmd] " + e.Data);
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
Console.WriteLine("[s3cmd-ERR] " + e.Data);
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Exception occurred during deletion: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task DeleteOldData()
|
||||
{
|
||||
while (true){
|
||||
var installations = Db.Installations.ToList();
|
||||
foreach (var installation in installations){
|
||||
Console.WriteLine("DELETE S3 DATA FOR INSTALLATION "+installation.Name);
|
||||
long oneYearAgoTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
|
||||
|
||||
Console.WriteLine("delete data before "+oneYearAgoTimestamp);
|
||||
for (int lastDigit=4;lastDigit>=0; lastDigit--)
|
||||
{
|
||||
int timestamps_to_delete = int.Parse(oneYearAgoTimestamp.ToString().Substring(0, lastDigit+1));
|
||||
timestamps_to_delete--;
|
||||
Console.WriteLine(timestamps_to_delete);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (timestamps_to_delete % 10 == 0)
|
||||
{
|
||||
Console.WriteLine("delete " + timestamps_to_delete + "*");
|
||||
DeleteFrom(installation,timestamps_to_delete);
|
||||
break;
|
||||
}
|
||||
Console.WriteLine("delete " + timestamps_to_delete + "*");
|
||||
DeleteFrom(installation,timestamps_to_delete);
|
||||
timestamps_to_delete--;
|
||||
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
CleanupAllInstallations().GetAwaiter().GetResult();
|
||||
}
|
||||
Console.WriteLine("FINISHED DELETING S3 DATA FOR ALL INSTALLATIONS\n");
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[S3Cleanup] Scheduler error: {ex.Message}");
|
||||
}
|
||||
},
|
||||
null,
|
||||
next - now,
|
||||
TimeSpan.FromDays(1)
|
||||
);
|
||||
|
||||
await Task.Delay(TimeSpan.FromDays(1));
|
||||
}
|
||||
Console.WriteLine($"[S3Cleanup] Scheduled daily at 03:00 UTC, first run in {(next - now).TotalHours:F1}h");
|
||||
}
|
||||
|
||||
private static async Task CleanupAllInstallations()
|
||||
{
|
||||
var cutoffTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
|
||||
var cutoffKey = cutoffTimestamp.ToString();
|
||||
var installations = Db.Installations.ToList();
|
||||
|
||||
Console.WriteLine($"[S3Cleanup] Starting cleanup for {installations.Count} installations, cutoff: {cutoffKey}");
|
||||
|
||||
foreach (var installation in installations)
|
||||
{
|
||||
try
|
||||
{
|
||||
var s3Region = new S3Region(
|
||||
$"https://{installation.S3Region}.{installation.S3Provider}",
|
||||
ExoCmd.S3Credentials
|
||||
);
|
||||
var bucket = s3Region.Bucket(installation.BucketName());
|
||||
|
||||
Console.WriteLine($"[S3Cleanup] Processing {installation.Name} (bucket: {bucket.Name})");
|
||||
var deleted = await DeleteObjectsBefore(bucket, cutoffKey);
|
||||
Console.WriteLine($"[S3Cleanup] {installation.Name}: deleted {deleted} objects");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[S3Cleanup] Failed for {installation.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("[S3Cleanup] Finished cleanup for all installations");
|
||||
}
|
||||
|
||||
public static async Task<string> DryRun(long? installationId = null)
|
||||
{
|
||||
var cutoffTimestamp = DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds();
|
||||
var cutoffKey = cutoffTimestamp.ToString();
|
||||
var allInstallations = Db.Installations.ToList();
|
||||
var installations = installationId.HasValue
|
||||
? allInstallations.Where(i => i.Id == installationId.Value).ToList()
|
||||
: allInstallations;
|
||||
var results = new List<string>();
|
||||
|
||||
results.Add($"Cutoff: {cutoffKey} ({DateTimeOffset.FromUnixTimeSeconds(cutoffTimestamp):yyyy-MM-dd HH:mm:ss} UTC)");
|
||||
results.Add($"Installations: {installations.Count} (of {allInstallations.Count} total)");
|
||||
results.Add("");
|
||||
|
||||
foreach (var installation in installations)
|
||||
{
|
||||
try
|
||||
{
|
||||
var s3Region = new S3Region(
|
||||
$"https://{installation.S3Region}.{installation.S3Provider}",
|
||||
ExoCmd.S3Credentials
|
||||
);
|
||||
var bucket = s3Region.Bucket(installation.BucketName());
|
||||
|
||||
var sampleKeys = new List<string>();
|
||||
var hasOldData = false;
|
||||
|
||||
await foreach (var obj in bucket.ListObjects())
|
||||
{
|
||||
if (string.Compare(obj.Path, cutoffKey, StringComparison.Ordinal) >= 0)
|
||||
break;
|
||||
|
||||
hasOldData = true;
|
||||
if (sampleKeys.Count < 5)
|
||||
sampleKeys.Add(obj.Path);
|
||||
else
|
||||
break; // only need a sample, not full count
|
||||
}
|
||||
|
||||
results.Add($"{installation.Name} (bucket: {bucket.Name})");
|
||||
results.Add($" Has old data: {(hasOldData ? "YES" : "NO")}");
|
||||
if (sampleKeys.Count > 0)
|
||||
results.Add($" Sample keys: {string.Join(", ", sampleKeys)}");
|
||||
results.Add("");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add($"{installation.Name}: ERROR - {ex.Message}");
|
||||
results.Add("");
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("\n", results);
|
||||
}
|
||||
|
||||
private static async Task<int> DeleteObjectsBefore(S3Bucket bucket, string cutoffKey)
|
||||
{
|
||||
var totalDeleted = 0;
|
||||
var keysToDelete = new List<string>();
|
||||
|
||||
await foreach (var obj in bucket.ListObjects())
|
||||
{
|
||||
if (string.Compare(obj.Path, cutoffKey, StringComparison.Ordinal) >= 0)
|
||||
break;
|
||||
|
||||
keysToDelete.Add(obj.Path);
|
||||
|
||||
if (keysToDelete.Count >= 1000)
|
||||
{
|
||||
if (await bucket.DeleteObjects(keysToDelete))
|
||||
totalDeleted += keysToDelete.Count;
|
||||
else
|
||||
Console.Error.WriteLine($"[S3Cleanup] Failed to delete batch of {keysToDelete.Count} objects from {bucket.Name}");
|
||||
|
||||
keysToDelete.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (keysToDelete.Count > 0)
|
||||
{
|
||||
if (await bucket.DeleteObjects(keysToDelete))
|
||||
totalDeleted += keysToDelete.Count;
|
||||
else
|
||||
Console.Error.WriteLine($"[S3Cleanup] Failed to delete batch of {keysToDelete.Count} objects from {bucket.Name}");
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ public static class Program
|
|||
WebsocketManager.MonitorInstallationTable().SupressAwaitWarning();
|
||||
|
||||
|
||||
// Task.Run(() => DeleteOldDataFromS3.DeleteOldData());
|
||||
DeleteOldDataFromS3.StartScheduler();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddProblemDetails(setup =>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public class Session : Relation<String, Int64>
|
|||
public Boolean AccessToSalidomo { get; set; } = false;
|
||||
public Boolean AccessToSodistoreMax { get; set; } = false;
|
||||
public Boolean AccessToSodioHome { get; set; } = false;
|
||||
public Boolean AccessToSodistoreGrid { get; set; } = false;
|
||||
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
|
||||
|
||||
// Private backing field
|
||||
|
|
@ -49,6 +50,7 @@ public class Session : Relation<String, Int64>
|
|||
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
|
||||
AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0;
|
||||
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
|
||||
AccessToSodistoreGrid = user.AccessibleInstallations(product: (int)ProductType.SodistoreGrid).ToList().Count > 0;
|
||||
|
||||
Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count);
|
||||
Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,11 @@ public static class RabbitMqManager
|
|||
monitorLink =
|
||||
$"https://monitor.inesco.energy/sodistore_installations/list/installation/{installation.S3BucketId}/batteryview";
|
||||
}
|
||||
else if (installation.Product == (int)ProductType.SodistoreGrid)
|
||||
{
|
||||
monitorLink =
|
||||
$"https://monitor.inesco.energy/sodistoregrid_installations/list/installation/{installation.S3BucketId}/batteryview";
|
||||
}
|
||||
else
|
||||
{
|
||||
monitorLink =
|
||||
|
|
@ -126,7 +131,7 @@ public static class RabbitMqManager
|
|||
Console.WriteLine("Send replace battery email to the support team for installation "+installationId);
|
||||
string recipient = "support@innov.energy";
|
||||
string subject = $"Battery Alarm from {installation.InstallationName}: 2 or more strings broken";
|
||||
string text = $"Dear InnovEnergy Support Team,\n" +
|
||||
string text = $"Dear inesco Energy Support Team,\n" +
|
||||
$"\n"+
|
||||
$"Installation Name: {installation.InstallationName}\n"+
|
||||
$"\n"+
|
||||
|
|
@ -138,7 +143,7 @@ public static class RabbitMqManager
|
|||
$"\n"+
|
||||
$"Thank you for your great support:)";
|
||||
//Disable this function now
|
||||
//Mailer.Send("InnovEnergy Support Team", recipient, subject, text);
|
||||
//Mailer.Send("inesco Energy Support Team", recipient, subject, text);
|
||||
}
|
||||
//Create a new error and add it to the database
|
||||
Db.HandleError(newError, installationId);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ public static class WebsocketManager
|
|||
if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(4)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
||||
(installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) ||
|
||||
(installationConnection.Value.Product == (int)ProductType.SodistoreGrid && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2))
|
||||
)
|
||||
{
|
||||
Console.WriteLine("Installation ID is " + installationConnection.Key);
|
||||
|
|
|
|||
|
|
@ -201,6 +201,61 @@ public static class S3
|
|||
return response.HttpStatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public static async Task<Boolean> PurgeBucket(this S3Bucket bucket)
|
||||
{
|
||||
var client = bucket.Region.GetS3Client();
|
||||
var totalDeleted = 0;
|
||||
|
||||
Console.WriteLine($"[PurgeBucket] Starting purge of bucket {bucket.Name}");
|
||||
|
||||
while (true)
|
||||
{
|
||||
var listResponse = await client.ListObjectsV2Async(new ListObjectsV2Request
|
||||
{
|
||||
BucketName = bucket.Name,
|
||||
MaxKeys = 1000
|
||||
});
|
||||
|
||||
if (listResponse.S3Objects.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"[PurgeBucket] Completed purge of bucket {bucket.Name}: {totalDeleted} objects deleted");
|
||||
return true;
|
||||
}
|
||||
|
||||
var deleteResponse = await client.DeleteObjectsAsync(new DeleteObjectsRequest
|
||||
{
|
||||
BucketName = bucket.Name,
|
||||
Objects = listResponse.S3Objects
|
||||
.Select(o => new KeyVersion { Key = o.Key })
|
||||
.ToList()
|
||||
});
|
||||
|
||||
if (deleteResponse.HttpStatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
Console.WriteLine($"[PurgeBucket] Failed at batch after {totalDeleted} objects deleted from bucket {bucket.Name}");
|
||||
return false;
|
||||
}
|
||||
|
||||
totalDeleted += listResponse.S3Objects.Count;
|
||||
|
||||
if (totalDeleted % 10000 == 0)
|
||||
Console.WriteLine($"[PurgeBucket] Progress: {totalDeleted} objects deleted from bucket {bucket.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Boolean> DeleteObjects(this S3Bucket bucket, IReadOnlyList<String> keys)
|
||||
{
|
||||
if (keys.Count == 0) return true;
|
||||
|
||||
var response = await bucket.Region.GetS3Client().DeleteObjectsAsync(new DeleteObjectsRequest
|
||||
{
|
||||
BucketName = bucket.Name,
|
||||
Objects = keys.Select(k => new KeyVersion { Key = k }).ToList()
|
||||
});
|
||||
|
||||
return response.HttpStatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public static async Task<Boolean> DeleteBucket(this S3Region region, String bucketName)
|
||||
{
|
||||
var request = new DeleteBucketRequest { BucketName = bucketName };
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ function App() {
|
|||
setAccessToSalimax,
|
||||
setAccessToSalidomo,
|
||||
setAccessToSodiohome,
|
||||
setAccessToSodistore
|
||||
setAccessToSodistore,
|
||||
setAccessToSodistoreGrid
|
||||
} = useContext(ProductIdContext);
|
||||
|
||||
const [language, setLanguage] = useState<string>(
|
||||
|
|
@ -102,12 +103,15 @@ function App() {
|
|||
setAccessToSalidomo(response.data.accessToSalidomo);
|
||||
setAccessToSodiohome(response.data.accessToSodioHome);
|
||||
setAccessToSodistore(response.data.accessToSodistoreMax);
|
||||
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
|
||||
if (response.data.accessToSalimax) {
|
||||
navigate(routes.installations);
|
||||
} else if (response.data.accessToSalidomo) {
|
||||
navigate(routes.salidomo_installations);
|
||||
} else if (response.data.accessToSodistoreMax) {
|
||||
navigate(routes.sodistore_installations);
|
||||
} else if (response.data.accessToSodistoreGrid) {
|
||||
navigate(routes.sodistoregrid_installations);
|
||||
} else {
|
||||
navigate(routes.sodiohome_installations);
|
||||
}
|
||||
|
|
@ -215,6 +219,15 @@ function App() {
|
|||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={routes.sodistoregrid_installations + '*'}
|
||||
element={
|
||||
<AccessContextProvider>
|
||||
<InstallationTabs product={4} />
|
||||
</AccessContextProvider>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path={routes.users + '*'} element={<Users />} />
|
||||
<Route
|
||||
path={'*'}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"salidomo_installations": "/salidomo_installations/",
|
||||
"sodistore_installations": "/sodistore_installations/",
|
||||
"sodiohome_installations": "/sodiohome_installations/",
|
||||
"sodistoregrid_installations": "/sodistoregrid_installations/",
|
||||
"installation": "installation/",
|
||||
"login": "/login/",
|
||||
"forgotPassword": "/forgotPassword/",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function Logo() {
|
|||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TooltipWrapper title="InnovEnergy" arrow>
|
||||
<TooltipWrapper title="inesco Energy" arrow>
|
||||
<LogoWrapper to="/overview">
|
||||
<Badge
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ function Login() {
|
|||
setAccessToSalimax,
|
||||
setAccessToSalidomo,
|
||||
setAccessToSodiohome,
|
||||
setAccessToSodistore
|
||||
setAccessToSodistore,
|
||||
setAccessToSodistoreGrid
|
||||
} = useContext(ProductIdContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ function Login() {
|
|||
setAccessToSalidomo(response.data.accessToSalidomo);
|
||||
setAccessToSodiohome(response.data.accessToSodioHome);
|
||||
setAccessToSodistore(response.data.accessToSodistoreMax);
|
||||
setAccessToSodistoreGrid(response.data.accessToSodistoreGrid);
|
||||
|
||||
if (rememberMe) {
|
||||
cookies.set('rememberedUsername', username, { path: '/' });
|
||||
|
|
@ -95,6 +97,8 @@ function Login() {
|
|||
navigate(routes.salidomo_installations);
|
||||
} else if (response.data.accessToSodistoreMax) {
|
||||
navigate(routes.sodistore_installations);
|
||||
} else if (response.data.accessToSodistoreGrid) {
|
||||
navigate(routes.sodistoregrid_installations);
|
||||
} else {
|
||||
navigate(routes.sodiohome_installations);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
) : (
|
||||
<TableCell align="center">Max Cell Voltage</TableCell>
|
||||
)}
|
||||
{product === 3 && (
|
||||
{(product === 3 || product === 4) && (
|
||||
<TableCell align="center">Voltage Difference</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
|
|
@ -469,7 +469,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{product === 3 && (
|
||||
{(product === 3 || product === 4) && (
|
||||
<>
|
||||
{(() => {
|
||||
const cellVoltagesString =
|
||||
|
|
@ -524,7 +524,7 @@ function BatteryView(props: BatteryViewProps) {
|
|||
})()}
|
||||
</>
|
||||
)}
|
||||
{product === 3 && (
|
||||
{(product === 3 || product === 4) && (
|
||||
<>
|
||||
{(() => {
|
||||
const cellVoltagesString =
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ function Information(props: InformationProps) {
|
|||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
width: 450,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
|
|
@ -143,7 +143,37 @@ function Information(props: InformationProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
<FormattedMessage
|
||||
id="confirmDeleteInstallation"
|
||||
defaultMessage="Do you want to delete this installation?"
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
userSelect: 'all',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{props.s3Credentials.s3Bucket}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallationWarning"
|
||||
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
@ -328,9 +358,12 @@ function Information(props: InformationProps) {
|
|||
label="S3 Bucket Name"
|
||||
name="s3bucketname"
|
||||
value={
|
||||
product === 0 || product == 3
|
||||
formValues.product === 0 || formValues.product == 3
|
||||
? formValues.s3BucketId +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
|
||||
: formValues.product == 4
|
||||
? formValues.s3BucketId +
|
||||
'-5109c126-e141-43ab-8658-f3c44c838ae8'
|
||||
: formValues.s3BucketId +
|
||||
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ function InformationSalidomo(props: InformationSalidomoProps) {
|
|||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
width: 450,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
|
|
@ -134,7 +134,37 @@ function InformationSalidomo(props: InformationSalidomoProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
<FormattedMessage
|
||||
id="confirmDeleteInstallation"
|
||||
defaultMessage="Do you want to delete this installation?"
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
userSelect: 'all',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{props.s3Credentials.s3Bucket}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallationWarning"
|
||||
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 350,
|
||||
width: 450,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 4,
|
||||
boxShadow: 24,
|
||||
|
|
@ -208,7 +208,37 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
|
|||
gutterBottom
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Do you want to delete this installation?
|
||||
<FormattedMessage
|
||||
id="confirmDeleteInstallation"
|
||||
defaultMessage="Do you want to delete this installation?"
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 1,
|
||||
p: 1,
|
||||
bgcolor: '#f5f5f5',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
wordBreak: 'break-all',
|
||||
userSelect: 'all',
|
||||
width: '100%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{props.s3Credentials.s3Bucket}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mt: 1, fontSize: '0.75rem', color: 'text.secondary', textAlign: 'center' }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="deleteInstallationWarning"
|
||||
defaultMessage="Please note the bucket name above. Purging S3 data may take several minutes. If deletion fails, you can manually remove the bucket from Exoscale."
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -57,9 +57,13 @@ function Installation(props: singleInstallationProps) {
|
|||
s3BucketId: props.current_installation.s3BucketId
|
||||
};
|
||||
|
||||
// TODO: SodistoreGrid — uses its own bucket salt
|
||||
const s3BucketSalt =
|
||||
props.current_installation.product === 4
|
||||
? '5109c126-e141-43ab-8658-f3c44c838ae8'
|
||||
: '3e5b3069-214a-43ee-8d85-57d72000c19d';
|
||||
const s3Bucket =
|
||||
props.current_installation.s3BucketId.toString() +
|
||||
'-3e5b3069-214a-43ee-8d85-57d72000c19d';
|
||||
props.current_installation.s3BucketId.toString() + '-' + s3BucketSalt;
|
||||
|
||||
const s3Credentials = { s3Bucket, ...S3data };
|
||||
|
||||
|
|
@ -427,12 +431,15 @@ function Installation(props: singleInstallationProps) {
|
|||
}
|
||||
></Route>
|
||||
|
||||
<Route
|
||||
path={routes.pvview + '/*'}
|
||||
element={
|
||||
<PvView values={values} connected={connected}></PvView>
|
||||
}
|
||||
></Route>
|
||||
{/* TODO: SodistoreGrid — PV View excluded, add back when data path is ready */}
|
||||
{props.current_installation.product !== 4 && (
|
||||
<Route
|
||||
path={routes.pvview + '/*'}
|
||||
element={
|
||||
<PvView values={values} connected={connected}></PvView>
|
||||
}
|
||||
></Route>
|
||||
)}
|
||||
|
||||
<Route
|
||||
path={routes.overview}
|
||||
|
|
@ -447,11 +454,28 @@ function Installation(props: singleInstallationProps) {
|
|||
<Route
|
||||
path={routes.live}
|
||||
element={
|
||||
<Topology
|
||||
values={values}
|
||||
connected={connected}
|
||||
loading={loading}
|
||||
></Topology>
|
||||
props.current_installation.product === 4 ? (
|
||||
// TODO: SodistoreGrid — implement actual topology layout
|
||||
<Container
|
||||
maxWidth="xl"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '40vh'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Live view coming soon
|
||||
</Typography>
|
||||
</Container>
|
||||
) : (
|
||||
<Topology
|
||||
values={values}
|
||||
connected={connected}
|
||||
loading={loading}
|
||||
></Topology>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
@ -470,10 +494,27 @@ function Installation(props: singleInstallationProps) {
|
|||
<Route
|
||||
path={routes.configuration}
|
||||
element={
|
||||
<Configuration
|
||||
values={values}
|
||||
id={props.current_installation.id}
|
||||
></Configuration>
|
||||
props.current_installation.product === 4 ? (
|
||||
// TODO: SodistoreGrid — implement actual configuration
|
||||
<Container
|
||||
maxWidth="xl"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '40vh'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Configuration not yet available
|
||||
</Typography>
|
||||
</Container>
|
||||
) : (
|
||||
<Configuration
|
||||
values={values}
|
||||
id={props.current_installation.id}
|
||||
></Configuration>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,18 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
|
||||
const {
|
||||
salimax_or_sodistore_Installations,
|
||||
sodistoreGridInstallations,
|
||||
fetchAllInstallations,
|
||||
socket,
|
||||
openSocket,
|
||||
closeSocket
|
||||
} = useContext(InstallationsContext);
|
||||
|
||||
// Use the correct installations array based on product
|
||||
const installations =
|
||||
props.product === 4
|
||||
? sodistoreGridInstallations
|
||||
: salimax_or_sodistore_Installations;
|
||||
const { product, setProduct } = useContext(ProductIdContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -93,7 +100,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
return ret_path;
|
||||
};
|
||||
|
||||
const singleInstallationTabs =
|
||||
// TODO: SodistoreGrid — PV View excluded for product 4, add back when data path is ready
|
||||
const hidePvView = props.product === 4;
|
||||
|
||||
const singleInstallationTabs = (
|
||||
currentUser.userType == UserType.admin
|
||||
? [
|
||||
{
|
||||
|
|
@ -204,7 +214,8 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
<FormattedMessage id="information" defaultMessage="Information" />
|
||||
)
|
||||
}
|
||||
];
|
||||
]
|
||||
).filter((tab) => !(hidePvView && tab.value === 'pvview'));
|
||||
|
||||
const tabs =
|
||||
currentTab != 'list' &&
|
||||
|
|
@ -372,7 +383,12 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
}
|
||||
];
|
||||
|
||||
return salimax_or_sodistore_Installations.length > 1 ? (
|
||||
// Filter out PV View for SodistoreGrid
|
||||
const filteredTabs = hidePvView
|
||||
? tabs.filter((tab) => tab.value !== 'pvview')
|
||||
: tabs;
|
||||
|
||||
return installations.length > 1 ? (
|
||||
<>
|
||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||
<TabsContainerWrapper>
|
||||
|
|
@ -384,7 +400,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
{filteredTabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
|
|
@ -415,7 +431,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
<Grid item xs={12}>
|
||||
<Box p={4}>
|
||||
<InstallationSearch
|
||||
installations={salimax_or_sodistore_Installations}
|
||||
installations={installations}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
|
@ -428,6 +444,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
element={
|
||||
props.product === 0 ? (
|
||||
<Navigate to={routes.installations + routes.list} />
|
||||
) : props.product === 4 ? (
|
||||
<Navigate
|
||||
to={routes.sodistoregrid_installations + routes.list}
|
||||
/>
|
||||
) : (
|
||||
<Navigate
|
||||
to={routes.sodistore_installations + routes.list}
|
||||
|
|
@ -441,7 +461,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
</Container>
|
||||
<Footer />
|
||||
</>
|
||||
) : salimax_or_sodistore_Installations.length === 1 ? (
|
||||
) : installations.length === 1 ? (
|
||||
<>
|
||||
<Container maxWidth="xl" sx={{ marginTop: '20px' }} className="mainframe">
|
||||
<TabsContainerWrapper>
|
||||
|
|
@ -480,7 +500,7 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
<Box p={4}>
|
||||
<Installation
|
||||
current_installation={
|
||||
salimax_or_sodistore_Installations[0]
|
||||
installations[0]
|
||||
}
|
||||
type="installation"
|
||||
></Installation>
|
||||
|
|
|
|||
|
|
@ -112,11 +112,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
}
|
||||
|
||||
const fetchDataPeriodically = async () => {
|
||||
var timeperiodToSearch = 200;
|
||||
var timeperiodToSearch = 350;
|
||||
let res;
|
||||
let timestampToFetch;
|
||||
|
||||
for (var i = 0; i < timeperiodToSearch; i += 2) {
|
||||
for (var i = 0; i < timeperiodToSearch; i += 30) {
|
||||
if (!continueFetching.current) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -184,13 +184,13 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
};
|
||||
|
||||
const fetchDataForOneTime = async () => {
|
||||
var timeperiodToSearch = 300; // 5 minutes to cover ~4 upload cycles
|
||||
var timeperiodToSearch = 300; // 5 minutes to cover ~2 upload cycles (150s each)
|
||||
let res;
|
||||
let timestampToFetch;
|
||||
|
||||
// Search from NOW backward to find the most recent data
|
||||
// Step by 10 seconds - balances between finding files quickly and reducing 404s
|
||||
for (var i = 0; i < timeperiodToSearch; i += 10) {
|
||||
// Step by 50 seconds - data is uploaded every ~150s, so finer steps are wasteful
|
||||
for (var i = 0; i < timeperiodToSearch; i += 50) {
|
||||
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
||||
try {
|
||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
||||
|
|
@ -264,7 +264,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
const configRefreshInterval = setInterval(() => {
|
||||
console.log('Refreshing configuration data from S3...');
|
||||
fetchDataForOneTime();
|
||||
}, 15000); // Refresh every 15 seconds
|
||||
}, 60000); // Refresh every 60 seconds (data uploads every ~150s)
|
||||
|
||||
return () => {
|
||||
continueFetching.current = false;
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
|||
const submittedAt = pendingConfig.submittedAt || 0;
|
||||
const timeSinceSubmit = Date.now() - submittedAt;
|
||||
|
||||
// Within 150 seconds of submit: use localStorage (waiting for S3 sync)
|
||||
// This covers two full S3 upload cycles (75 sec × 2) to ensure new file is available
|
||||
if (timeSinceSubmit < 150000) {
|
||||
// Within 300 seconds of submit: use localStorage (waiting for S3 sync)
|
||||
// This covers two full S3 upload cycles (150 sec × 2) to ensure new file is available
|
||||
if (timeSinceSubmit < 300000) {
|
||||
// Check if S3 now matches - if so, sync is complete
|
||||
const s3MatchesPending =
|
||||
s3Values.controlPermission === pendingConfig.values.controlPermission &&
|
||||
|
|
@ -247,7 +247,7 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
|||
setLoading(false);
|
||||
|
||||
// Save submitted values to localStorage for optimistic UI update
|
||||
// This ensures the form shows correct values even before S3 syncs (up to 75 sec delay)
|
||||
// This ensures the form shows correct values even before S3 syncs (up to 150 sec delay)
|
||||
localStorage.setItem(pendingConfigKey, JSON.stringify({
|
||||
values: formValues,
|
||||
submittedAt: Date.now()
|
||||
|
|
|
|||
|
|
@ -159,7 +159,10 @@ function FormattedBullet({ text }: { text: string }) {
|
|||
return <>{parts}</>;
|
||||
}
|
||||
|
||||
const MONTH_NAMES = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
function getMonthName(month: number, locale: string): string {
|
||||
const date = new Date(2000, month - 1, 1);
|
||||
return date.toLocaleDateString(locale, { month: 'long' });
|
||||
}
|
||||
|
||||
// ── Email Bar (shared) ──────────────────────────────────────────
|
||||
|
||||
|
|
@ -545,7 +548,7 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
|||
</Box>
|
||||
{report.dailyData.map((d, i) => {
|
||||
const dt = new Date(d.date);
|
||||
const dayLabel = dt.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
const dayLabel = dt.toLocaleDateString(intl.locale, { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
const isCurrentWeek = report.dailyData.length > 7 ? i >= report.dailyData.length - 7 : true;
|
||||
return (
|
||||
<Box key={d.date} sx={{ mb: 1.5, opacity: isCurrentWeek ? 1 : 0.6 }}>
|
||||
|
|
@ -555,7 +558,7 @@ function WeeklySection({ installationId, latestMonthlyPeriodEnd }: { installatio
|
|||
{!isCurrentWeek && <span style={{ color: '#999', marginLeft: 4 }}><FormattedMessage id="prevWeek" defaultMessage="(prev week)" /></span>}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#888', fontSize: '11px' }}>
|
||||
PV {d.pvProduction.toFixed(1)} | Load {d.loadConsumption.toFixed(1)} | Grid {d.gridImport.toFixed(1)} kWh
|
||||
{intl.formatMessage({ id: 'pvProduction' })} {d.pvProduction.toFixed(1)} | {intl.formatMessage({ id: 'consumption' })} {d.loadConsumption.toFixed(1)} | {intl.formatMessage({ id: 'gridImport' })} {d.gridImport.toFixed(1)} kWh
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: '2px', height: 18 }}>
|
||||
|
|
@ -737,7 +740,7 @@ function MonthlySection({
|
|||
? intl.formatMessage({ id: 'generatingMonthly', defaultMessage: 'Generating...' })
|
||||
: intl.formatMessage(
|
||||
{ id: 'generateMonth', defaultMessage: 'Generate {month} {year} ({count} weeks)' },
|
||||
{ month: MONTH_NAMES[p.month], year: p.year, count: p.weekCount }
|
||||
{ month: getMonthName(p.month, intl.locale), year: p.year, count: p.weekCount }
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
|
|
@ -752,7 +755,7 @@ function MonthlySection({
|
|||
<AggregatedSection
|
||||
reports={reports}
|
||||
type="monthly"
|
||||
labelFn={(r: MonthlyReport) => `${MONTH_NAMES[r.month]} ${r.year}`}
|
||||
labelFn={(r: MonthlyReport) => `${getMonthName(r.month, intl.locale)} ${r.year}`}
|
||||
countLabelId="weeksAggregated"
|
||||
countFn={(r: MonthlyReport) => r.weekCount}
|
||||
sendEndpoint="/SendMonthlyReportEmail"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,12 @@ function TreeInformation(props: TreeInformationProps) {
|
|||
setProduct(e.target.value); // Directly update the product state
|
||||
};
|
||||
|
||||
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax'];
|
||||
const ProductTypes = ['Salimax', 'Salidomo', 'SodistoreHome', 'SodistoreMax', 'SodistoreGrid'];
|
||||
const ProductDisplayNames: Record<string, string> = {
|
||||
'SodistoreHome': 'Sodistore Home',
|
||||
'SodistoreMax': 'Sodistore Max',
|
||||
'SodistoreGrid': 'Sodistore Grid'
|
||||
};
|
||||
|
||||
const isMobile = window.innerWidth <= 1490;
|
||||
|
||||
|
|
@ -282,7 +287,7 @@ function TreeInformation(props: TreeInformationProps) {
|
|||
>
|
||||
{ProductTypes.map((type) => (
|
||||
<MenuItem key={type} value={type}>
|
||||
{type}
|
||||
{ProductDisplayNames[type] || type}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
|
@ -323,7 +328,8 @@ function TreeInformation(props: TreeInformationProps) {
|
|||
)}
|
||||
{openModalInstallation &&
|
||||
(product == 'Salimax' ||
|
||||
product == 'SodistoreMax') && (
|
||||
product == 'SodistoreMax' ||
|
||||
product == 'SodistoreGrid') && (
|
||||
<InstallationForm
|
||||
cancel={handleFormCancel}
|
||||
submit={handleInstallationFormSubmit}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function InstallationTree() {
|
|||
key={installation.id}
|
||||
path={routes.installation + installation.id + '/*'}
|
||||
element={
|
||||
installation.product == 0 || installation.product == 3 ? (
|
||||
installation.product == 0 || installation.product == 3 || installation.product == 4 ? (
|
||||
<Installation
|
||||
key={installation.id}
|
||||
current_installation={installation}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ function Status500() {
|
|||
<Container maxWidth="sm">
|
||||
<Box textAlign="center">
|
||||
<TypographyPrimary variant="h1" sx={{ my: 2 }}>
|
||||
InnovEnergy{' '}
|
||||
inesco Energy{' '}
|
||||
</TypographyPrimary>
|
||||
<TypographySecondary
|
||||
variant="h4"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ const InstallationsContextProvider = ({
|
|||
const [sodiohomeInstallations, setSodiohomeInstallations] = useState<
|
||||
I_Installation[]
|
||||
>([]);
|
||||
const [sodistoreGridInstallations, setSodistoreGridInstallations] = useState<
|
||||
I_Installation[]
|
||||
>([]);
|
||||
const [foldersAndInstallations, setFoldersAndInstallations] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
|
@ -89,16 +92,31 @@ const InstallationsContextProvider = ({
|
|||
: installation;
|
||||
});
|
||||
|
||||
const updatedSodistoreGrid = sodistoreGridInstallations.map(
|
||||
(installation) => {
|
||||
const update = pendingUpdates.current[installation.id];
|
||||
return update
|
||||
? {
|
||||
...installation,
|
||||
status: update.status,
|
||||
testingMode: update.testingMode
|
||||
}
|
||||
: installation;
|
||||
}
|
||||
);
|
||||
|
||||
setSalidomoInstallations(updatedSalidomo);
|
||||
setSalimax_Or_Sodistore_Installations(updatedSalimax);
|
||||
setSodiohomeInstallations(updatedSodiohome);
|
||||
setSodistoreGridInstallations(updatedSodistoreGrid);
|
||||
|
||||
// Clear the pending updates after applying
|
||||
pendingUpdates.current = {};
|
||||
}, [
|
||||
salidomoInstallations,
|
||||
salimax_or_sodistore_Installations,
|
||||
sodiohomeInstallations
|
||||
sodiohomeInstallations,
|
||||
sodistoreGridInstallations
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -172,6 +190,8 @@ const InstallationsContextProvider = ({
|
|||
setSalidomoInstallations(res.data);
|
||||
} else if (product === 0 || product === 3) {
|
||||
setSalimax_Or_Sodistore_Installations(res.data);
|
||||
} else if (product === 4) {
|
||||
setSodistoreGridInstallations(res.data);
|
||||
}
|
||||
|
||||
if (open_socket) {
|
||||
|
|
@ -390,6 +410,7 @@ const InstallationsContextProvider = ({
|
|||
salimax_or_sodistore_Installations,
|
||||
salidomoInstallations,
|
||||
sodiohomeInstallations,
|
||||
sodistoreGridInstallations,
|
||||
foldersAndInstallations,
|
||||
fetchAllInstallations,
|
||||
fetchAllFoldersAndInstallations,
|
||||
|
|
@ -416,6 +437,7 @@ const InstallationsContextProvider = ({
|
|||
salimax_or_sodistore_Installations,
|
||||
salidomoInstallations,
|
||||
sodiohomeInstallations,
|
||||
sodistoreGridInstallations,
|
||||
foldersAndInstallations,
|
||||
loading,
|
||||
error,
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ interface ProductIdContextType {
|
|||
accessToSalidomo: boolean;
|
||||
accessToSodiohome: boolean;
|
||||
accessToSodistore: boolean;
|
||||
accessToSodistoreGrid: boolean;
|
||||
setAccessToSalimax: (access: boolean) => void;
|
||||
setAccessToSalidomo: (access: boolean) => void;
|
||||
setAccessToSodiohome: (access: boolean) => void;
|
||||
setAccessToSodistore: (access: boolean) => void;
|
||||
setAccessToSodistoreGrid: (access: boolean) => void;
|
||||
}
|
||||
|
||||
// Create the context.
|
||||
|
|
@ -43,6 +45,10 @@ export const ProductIdContextProvider = ({
|
|||
const storedValue = localStorage.getItem('accessToSodistore');
|
||||
return storedValue === 'true';
|
||||
});
|
||||
const [accessToSodistoreGrid, setAccessToSodistoreGrid] = useState(() => {
|
||||
const storedValue = localStorage.getItem('accessToSodistoreGrid');
|
||||
return storedValue === 'true';
|
||||
});
|
||||
// const [product, setProduct] = useState(location.includes('salidomo') ? 1 : 0);
|
||||
// const [product, setProduct] = useState<number>(
|
||||
// productMapping[Object.keys(productMapping).find((key) => location.includes(key)) || ''] || -1
|
||||
|
|
@ -52,6 +58,8 @@ export const ProductIdContextProvider = ({
|
|||
return 1;
|
||||
} else if (location.includes('sodiohome')) {
|
||||
return 2;
|
||||
} else if (location.includes('sodistoregrid')) {
|
||||
return 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -80,6 +88,10 @@ export const ProductIdContextProvider = ({
|
|||
setAccessToSodistore(access);
|
||||
localStorage.setItem('accessToSodistore', JSON.stringify(access));
|
||||
};
|
||||
const changeAccessSodistoreGrid = (access: boolean) => {
|
||||
setAccessToSodistoreGrid(access);
|
||||
localStorage.setItem('accessToSodistoreGrid', JSON.stringify(access));
|
||||
};
|
||||
|
||||
return (
|
||||
<ProductIdContext.Provider
|
||||
|
|
@ -90,10 +102,12 @@ export const ProductIdContextProvider = ({
|
|||
accessToSalidomo,
|
||||
accessToSodiohome,
|
||||
accessToSodistore,
|
||||
accessToSodistoreGrid,
|
||||
setAccessToSalimax: changeAccessSalimax,
|
||||
setAccessToSalidomo: changeAccessSalidomo,
|
||||
setAccessToSodiohome: changeAccessSodiohome,
|
||||
setAccessToSodistore: changeAccessSodistore
|
||||
setAccessToSodistore: changeAccessSodistore,
|
||||
setAccessToSodistoreGrid: changeAccessSodistoreGrid
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,9 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
const categories = isSodioHome
|
||||
? ['Soc', 'Power', 'Voltage', 'Current', 'Soh']
|
||||
: ['Soc', 'Temperature', 'Power', 'Voltage', 'Current'];
|
||||
// TODO: SodistoreGrid — update data paths when installation data format is finalized
|
||||
const pathCategories =
|
||||
product === 3
|
||||
product === 3 || product === 4
|
||||
? [
|
||||
'.BatteryDeligreenDataRecord.Soc',
|
||||
'.BatteryDeligreenDataRecord.TemperaturesList.EnvironmentTemperature',
|
||||
|
|
@ -167,7 +168,7 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
);
|
||||
|
||||
const adjustedTimestamp =
|
||||
product == 0 || product == 2 || product == 3
|
||||
product == 0 || product == 2 || product == 3 || product == 4
|
||||
? new Date(timestampArray[i] * 1000)
|
||||
: new Date(timestampArray[i] * 100000);
|
||||
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
|
||||
|
|
@ -270,7 +271,7 @@ export const transformInputToBatteryViewDataJson = async (
|
|||
if (battery_nodes.length > old_length) {
|
||||
battery_nodes.forEach((node) => {
|
||||
const node_number =
|
||||
product == 3 ? Number(node) + 1 : Number(node) - 1;
|
||||
product == 3 || product == 4 ? Number(node) + 1 : Number(node) - 1;
|
||||
if (!pathsToSave.includes('Node' + node_number)) {
|
||||
pathsToSave.push('Node' + node_number);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@
|
|||
"status": "Status",
|
||||
"live": "Live Daten",
|
||||
"deleteInstallation": "Installation löschen",
|
||||
"confirmDeleteInstallation": "Möchten Sie diese Installation löschen?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Bitte notieren Sie den Bucket-Namen oben. Das Löschen der S3-Daten kann mehrere Minuten dauern. Überprüfen Sie nach dem Löschen in Exoscale, ob der Bucket entfernt wurde. Falls nicht, leeren und löschen Sie den Bucket manuell.",
|
||||
"errorOccured": "Ein Fehler ist aufgetreten",
|
||||
"successfullyUpdated": "Erfolgreich aktualisiert",
|
||||
"grantAccess": "Zugriff gewähren",
|
||||
|
|
@ -155,6 +158,7 @@
|
|||
"generatingMonthly": "Wird generiert...",
|
||||
"generatingYearly": "Wird generiert...",
|
||||
"thisMonthWeeklyReports": "Wöchentliche Berichte dieses Monats",
|
||||
"recentWeeklyReports": "Letzte Wochenberichte",
|
||||
"ai_analyzing": "KI analysiert...",
|
||||
"ai_show_details": "Details anzeigen",
|
||||
"ai_show_less": "Weniger anzeigen",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
"status": "Status",
|
||||
"live": "Live View",
|
||||
"deleteInstallation": "Delete Installation",
|
||||
"confirmDeleteInstallation": "Do you want to delete this installation?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Please note the bucket name above. Purging S3 data may take several minutes. After deletion, verify in Exoscale that the bucket has been removed. If not, purge and delete the bucket manually.",
|
||||
"errorOccured": "An error has occurred",
|
||||
"successfullyUpdated": "Successfully updated",
|
||||
"grantAccess": "Grant Access",
|
||||
|
|
@ -137,6 +140,7 @@
|
|||
"generatingMonthly": "Generating...",
|
||||
"generatingYearly": "Generating...",
|
||||
"thisMonthWeeklyReports": "This Month's Weekly Reports",
|
||||
"recentWeeklyReports": "Recent Weekly Reports",
|
||||
"ai_analyzing": "AI is analyzing...",
|
||||
"ai_show_details": "Show details",
|
||||
"ai_show_less": "Show less",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@
|
|||
"status": "Statut",
|
||||
"live": "Diffusion en direct",
|
||||
"deleteInstallation": "Supprimer l'installation",
|
||||
"confirmDeleteInstallation": "Voulez-vous supprimer cette installation ?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Veuillez noter le nom du bucket ci-dessus. La purge des données S3 peut prendre plusieurs minutes. Après la suppression, vérifiez dans Exoscale que le bucket a bien été supprimé. Sinon, purgez et supprimez le bucket manuellement.",
|
||||
"errorOccured": "Une erreur s'est produite",
|
||||
"successfullyUpdated": "Mise à jour réussie",
|
||||
"grantAccess": "Accorder l'accès",
|
||||
|
|
@ -149,6 +152,7 @@
|
|||
"generatingMonthly": "Génération en cours...",
|
||||
"generatingYearly": "Génération en cours...",
|
||||
"thisMonthWeeklyReports": "Rapports hebdomadaires de ce mois",
|
||||
"recentWeeklyReports": "Derniers rapports hebdomadaires",
|
||||
"ai_analyzing": "L'IA analyse...",
|
||||
"ai_show_details": "Afficher les détails",
|
||||
"ai_show_less": "Afficher moins",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
"status": "Stato",
|
||||
"live": "Vista in diretta",
|
||||
"deleteInstallation": "Elimina installazione",
|
||||
"confirmDeleteInstallation": "Vuoi eliminare questa installazione?",
|
||||
"bucketLabel": "Bucket",
|
||||
"deleteInstallationWarning": "Prendi nota del nome del bucket qui sopra. L'eliminazione dei dati S3 potrebbe richiedere diversi minuti. Dopo l'eliminazione, verifica in Exoscale che il bucket sia stato rimosso. In caso contrario, svuota ed elimina il bucket manualmente.",
|
||||
"errorOccured": "Si è verificato un errore",
|
||||
"successfullyUpdated": "Aggiornamento riuscito",
|
||||
"grantAccess": "Concedi accesso",
|
||||
|
|
@ -160,6 +163,7 @@
|
|||
"generatingMonthly": "Generazione in corso...",
|
||||
"generatingYearly": "Generazione in corso...",
|
||||
"thisMonthWeeklyReports": "Rapporti settimanali di questo mese",
|
||||
"recentWeeklyReports": "Ultimi rapporti settimanali",
|
||||
"ai_analyzing": "L'IA sta analizzando...",
|
||||
"ai_show_details": "Mostra dettagli",
|
||||
"ai_show_less": "Mostra meno",
|
||||
|
|
|
|||
|
|
@ -168,7 +168,8 @@ function SidebarMenu() {
|
|||
accessToSalimax,
|
||||
accessToSodistore,
|
||||
accessToSalidomo,
|
||||
accessToSodiohome
|
||||
accessToSodiohome,
|
||||
accessToSodistoreGrid
|
||||
} = useContext(ProductIdContext);
|
||||
|
||||
return (
|
||||
|
|
@ -213,7 +214,7 @@ function SidebarMenu() {
|
|||
<Box sx={{ marginTop: '3px' }}>
|
||||
<FormattedMessage
|
||||
id="sodistore"
|
||||
defaultMessage="SodistoreMax"
|
||||
defaultMessage="Sodistore Max"
|
||||
/>
|
||||
</Box>
|
||||
</Button>
|
||||
|
|
@ -242,6 +243,27 @@ function SidebarMenu() {
|
|||
</List>
|
||||
)}
|
||||
|
||||
{accessToSodistoreGrid && (
|
||||
<List component="div">
|
||||
<ListItem component="div">
|
||||
<Button
|
||||
disableRipple
|
||||
component={RouterLink}
|
||||
onClick={closeSidebar}
|
||||
to="/sodistoregrid_installations"
|
||||
startIcon={<BrightnessLowTwoToneIcon />}
|
||||
>
|
||||
<Box sx={{ marginTop: '3px' }}>
|
||||
<FormattedMessage
|
||||
id="sodistoregrid"
|
||||
defaultMessage="Sodistore Grid"
|
||||
/>
|
||||
</Box>
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
)}
|
||||
|
||||
{accessToSodiohome && (
|
||||
<List component="div">
|
||||
<ListItem component="div">
|
||||
|
|
|
|||
Loading…
Reference in New Issue