From ec3d9ce3c6643cf8395d682d216fdeb885506182 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 11:17:51 -0400 Subject: [PATCH 01/20] use APIQuery for openMIC health requests --- .../SystemCenter/Model/DeviceHealthReport.cs | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index 5d13f8b9d..a86f30840 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -32,6 +32,8 @@ using System.Data; using System.Linq; using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; using System.Web.Http; using SystemCenter.Controllers; using System.Collections.Generic; @@ -174,6 +176,12 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData [HttpGet, Route("OpenMICStatus")] public IHttpActionResult GetOpenMICStatus() { + + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Get; + } + AppStatus status = new AppStatus() { Status ="Success", @@ -184,8 +192,8 @@ public IHttpActionResult GetOpenMICStatus() try { - openMICResponse = GetHealth("OpenMIC", $"api/health/getsystemstatus/"); - + APIQuery apiQuery = GetAPIQuery(); + openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; } catch { @@ -270,9 +278,17 @@ public IHttpActionResult GetScadaTriggerStatus() Details = [] }; HttpResponseMessage openMICResponse = null; + + + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Get; + } + try { - openMICResponse = GetHealth("OpenMIC", $"api/health/getsystemstatus/"); + APIQuery apiQuery = GetAPIQuery(); + openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; } catch { @@ -292,7 +308,8 @@ public IHttpActionResult GetScadaTriggerStatus() HttpResponseMessage scadaTriggerResponse = null; try { - scadaTriggerResponse = GetHealth("OpenMIC", $"api/health/getscadatriggerhealth/"); + APIQuery apiQuery = GetAPIQuery(); + scadaTriggerResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getscadatriggerhealth/").Result; } catch { @@ -334,29 +351,18 @@ public IHttpActionResult GetScadaTriggerStatus() return Ok(status); } - /// - /// Processes Get request from an application using settings table parameters. - /// Exceptions are expected to be handled by the caller. - /// - /// Name of Application - /// Path to specific API request - /// string - public static HttpResponseMessage GetHealth(string application, string requestURI) + public static APIQuery GetAPIQuery() { using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) { - string url = new TableOperations(connection).QueryRecordWhere($"Name = '{application}.Url'")?.Value ?? ""; - string credential = new TableOperations(connection).QueryRecordWhere($"Name = '{application}.Credential'")?.Value ?? ""; - string password = new TableOperations(connection).QueryRecordWhere($"Name = '{application}.Password'")?.Value ?? ""; + string url = new TableOperations(connection).QueryRecordWhere($"Name = 'OpenMIC.Url'")?.Value ?? ""; + string credential = new TableOperations(connection).QueryRecordWhere($"Name = 'OpenMIC.Credential'")?.Value ?? ""; + string password = new TableOperations(connection).QueryRecordWhere($"Name = 'OpenMIC.Password'")?.Value ?? ""; - void ConfigureRequest(HttpRequestMessage request) - { - request.Method = HttpMethod.Get; - } //string token = GenerateAntiForgeryToken(application); //return Get(httpClient, url, requestURI, credential, password, token); APIQuery query = new APIQuery(credential, password, url); - return query.SendWebRequestAsync(ConfigureRequest, requestURI).Result; + return query; } } } From 8285ebdb4a4e20daaf0008f4bbaf907d3be24669 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 11:19:19 -0400 Subject: [PATCH 02/20] gather information from new openMIC endpoint, using filters and sorting --- .../SystemCenter/Model/DeviceHealthReport.cs | 338 +++++++++++++++++- 1 file changed, 321 insertions(+), 17 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index a86f30840..5fbb44232 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -112,6 +112,33 @@ public class DeviceHealthReport [RoutePrefix("api/DeviceHealthReport")] public class DeviceHealthReportController : ModelController { + public class DailyStatisticsRecord + { + [PrimaryKey(true)] + public int ID { get; set; } + + public DateTime Timestamp { get; set; } + + public int BadDays { get; set; } + + public string Meter { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime? LastSuccessfulConnection { get; set; } + + [FieldDataType(DbType.DateTime2, DatabaseType.SQLServer)] + public DateTime? LastUnsuccessfulConnection { get; set; } + + public string LastUnsuccessfulConnectionExplanation { get; set; } + + [NonRecordField] + public int TotalConnections => TotalSuccessfulConnections + TotalUnsuccessfulConnections; + + public int TotalUnsuccessfulConnections { get; set; } + + public int TotalSuccessfulConnections { get; set; } + + } public class StatusItem { public string Status { get; set; } @@ -130,29 +157,255 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { int warningLevel = 50; int errorLevel = 100; - DataTable table = GetSearchResults(postData); + + PostData openMicRequestBody = new() + { + Searches = [], + Ascending = true, + OrderBy = "Timestamp" + }; + + PostData systemCenterRequestBody = new() + { + Searches = [], + Ascending = true, + OrderBy = "Name" + }; + + bool openMICPrecedence = false; + + SQLSearchFilter[] badDayFilters = []; + + // for each filter, add it to the request body that would use it + foreach (SQLSearchFilter filter in postData.Searches) + { + if (filter.FieldName == "LastGood") + { + filter.FieldName = "LastSuccessfulConnection"; + openMicRequestBody.Searches = openMicRequestBody.Searches.Append(filter); + openMICPrecedence = true; + continue; + } + if (filter.FieldName == "MICStatus") + { + if (filter.SearchText == "(Warning)") + { + SQLSearchFilter minFilter = new() + { + FieldName = "TotalUnsuccessfulConnections", + Operator = ">=", + SearchText = warningLevel.ToString() + }; + SQLSearchFilter maxFilter = new() + { + FieldName = "TotalUnsuccessfulConnections", + Operator = "<", + SearchText = errorLevel.ToString() + }; + openMicRequestBody.Searches = openMicRequestBody.Searches.Append(minFilter); + openMicRequestBody.Searches = openMicRequestBody.Searches.Append(maxFilter); + } + if (filter.SearchText == "(Error)") + { + + SQLSearchFilter errorFilter = new() + { + FieldName = "TotalUnsuccessfulConnections", + Operator = ">=", + SearchText = errorLevel.ToString() + }; + openMicRequestBody.Searches = openMicRequestBody.Searches.Append(errorFilter); + } + if (filter.SearchText == "(Warning,Error)") + { + SQLSearchFilter warningFilter = new() + { + FieldName = "TotalUnsuccessfulConnections", + Operator = ">=", + SearchText = warningLevel.ToString() + }; + openMicRequestBody.Searches = openMicRequestBody.Searches.Append(warningFilter); + } + } + if (filter.FieldName == "BadDays") + { + badDayFilters = badDayFilters.Append(filter).ToArray(); + continue; + } + systemCenterRequestBody.Searches = systemCenterRequestBody.Searches.Append(filter); + } + + // pass the sorting argument to the correct application and sort the other by basic field + if (postData.OrderBy == "LastGood") + { + openMicRequestBody.OrderBy = "LastSuccessfulConnection"; + openMicRequestBody.Ascending = postData.Ascending; + openMICPrecedence = true; + } + else if (postData.OrderBy == "MICStatus") + { + // todo SORT + } + else + { + systemCenterRequestBody.OrderBy = postData.OrderBy; + systemCenterRequestBody.Ascending = postData.Ascending; + } + + DailyStatisticsRecord[] openMicStatistics; + + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Post; + request.Content = new StringContent(JsonConvert.SerializeObject(openMicRequestBody), Encoding.UTF8, "application/json"); + } + try + { + APIQuery apiQuery = GetAPIQuery(); + HttpResponseMessage response = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/DailyStatistics/SearchableList").Result; + string responseContent = response.Content.ReadAsStringAsync().Result; + string trimmedResponse = responseContent.Trim('"'); + string unescapedResponse = Regex.Unescape(trimmedResponse); + openMicStatistics = JsonConvert.DeserializeObject(unescapedResponse); + } + catch (Exception e) + { + return Ok(); + } + + DataTable systemCenterResult = GetSearchResults(systemCenterRequestBody); // add empty rows to table for openMIC info - table.Columns.Add("MICStatus"); - table.Columns.Add("MICBadDays", Type.GetType("System.Int32")); - table.Columns.Add("LastGood"); - foreach (DataRow devHealthReport in table.Rows) + systemCenterResult.Columns.Add("MICStatus"); + systemCenterResult.Columns.Add("MICBadDays", Type.GetType("System.Int32")); + systemCenterResult.Columns.Add("LastGood"); + + DataTable resultTable; + + + // if sorted by or filter by an openMIC field, stitch SystemCenter to existing openMIC data + if (openMICPrecedence) + { + resultTable = systemCenterResult.Clone(); + + foreach (DailyStatisticsRecord record in openMicStatistics) + { + string openMICAcronym = record.Meter; + // if there is already a statistic from the meter, delete the other if this timestamp is newer, otherwise skip + DataRow systemCenterRow = systemCenterResult.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("OpenMIC"), record.Meter)); + if (systemCenterRow is null) // could be possible due to filters + { + continue; + } + int totalUnsuccessfulConnections = record.TotalUnsuccessfulConnections; + systemCenterRow["MICStatus"] = ""; + if (totalUnsuccessfulConnections > errorLevel) + { + systemCenterRow["Status"] = "Error"; + + } + else if (totalUnsuccessfulConnections > warningLevel) + { + systemCenterRow["Status"] = "Warning"; + } + + systemCenterRow["MICBadDays"] = record.BadDays; + + if (record.BadDays > Convert.ToInt32(systemCenterRow["BadDays"])) + { + systemCenterRow["BadDays"] = record.BadDays; + } + + + if (!(record.LastSuccessfulConnection is null)) + { + systemCenterRow["LastGood"] = record.LastSuccessfulConnection; + } + resultTable.ImportRow(systemCenterRow); + } + if (postData.OrderBy == "LastGood") + { + DataView view = resultTable.DefaultView; + string asc = postData.Ascending ? "ASC" : "DESC"; + view.Sort = $"LastGood {asc}"; + resultTable = view.ToTable(); + } + if (openMicRequestBody.Searches.Count() == 0) // if not filtered by openMIC, add the rest of systemCenter below + { + foreach (DataRow systemCenterRow in systemCenterResult.Rows) { - if (string.IsNullOrEmpty(devHealthReport.ConvertField("OpenMic"))) + DataRow existingRow = resultTable.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("Name"), systemCenterRow["Name"])); + if (existingRow != null) { continue; } - var rawResponse = ControllerHelpers.Get("OpenMIC", $"api/Operations/Statistics/{devHealthReport["OpenMIC"]}"); - JObject? openMicResult = null; - try + resultTable.ImportRow(systemCenterRow); + } + } + foreach (SQLSearchFilter badDayFilter in badDayFilters) + { + DataTable filteredTable; + DataRow[] filteredRows = []; + // Bad Days can be =, <> (neq), <, <=, >, >=, with a number + switch (badDayFilter.Operator) + { + case "=": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) == int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<>": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) != int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) < int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<=": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) <= int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case ">": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) > int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case ">=": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) >= int.Parse(badDayFilter.SearchText)).ToArray(); + break; + default: + break; + } + if (filteredRows.Length == 0) + { + filteredTable = systemCenterResult.Clone(); + resultTable = filteredTable; + } + else + { + filteredTable = filteredRows.CopyToDataTable(); + resultTable = filteredTable; + } + } + return Ok(JsonConvert.SerializeObject(resultTable)); + } + + resultTable = systemCenterResult; + + // stitch openMIC data to SystemCenter info + foreach (DataRow devHealthReport in systemCenterResult.Rows) + { + if (string.IsNullOrEmpty(devHealthReport.ConvertField("OpenMic"))) { - openMicResult = JObject.Parse(rawResponse); + continue; } - catch(JsonReaderException) + DailyStatisticsRecord openMicRecord = openMicStatistics.FirstOrDefault(record => String.Equals(record.Meter, devHealthReport["OpenMic"])); + if (openMicRecord is null) { continue; } - int totalUnsuccessfulConnections = openMicResult["TotalUnsuccessfulConnections"]?.ToObject() ?? 0; + int totalUnsuccessfulConnections = openMicRecord.TotalUnsuccessfulConnections; + devHealthReport["MICStatus"] = ""; if (totalUnsuccessfulConnections > errorLevel) { devHealthReport["Status"] = "Error"; @@ -162,15 +415,66 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { devHealthReport["Status"] = "Warning"; } - devHealthReport["MICStatus"] = openMicResult["Status"]; - devHealthReport["MICBadDays"] = 0; // just a placeholder for now. - if (openMicResult["LastSuccessfulConnection"] != null) + devHealthReport["MICBadDays"] = openMicRecord.BadDays; + + if (openMicRecord.BadDays > Convert.ToInt32(devHealthReport["BadDays"])) + { + devHealthReport["BadDays"] = openMicRecord.BadDays; + } + + if (openMicRecord.LastSuccessfulConnection != null) + { + devHealthReport["LastGood"] = openMicRecord.LastSuccessfulConnection; + } + } + + foreach (SQLSearchFilter badDayFilter in badDayFilters) + { + DataTable filteredTable; + DataRow[] filteredRows = []; + // Bad Days can be =, <> (neq), <, <=, >, >=, with a number + switch (badDayFilter.Operator) + { + case "=": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) == int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<>": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) != int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) < int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<=": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) <= int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case ">": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) > int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case ">=": + filteredRows = resultTable.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) >= int.Parse(badDayFilter.SearchText)).ToArray(); + break; + default: + break; + } + if (filteredRows.Length == 0) + { + filteredTable = systemCenterResult.Clone(); + systemCenterResult = filteredTable; + } + else { - devHealthReport["LastGood"] = openMicResult["LastSuccessfulConnection"]; + filteredTable = filteredRows.CopyToDataTable(); + systemCenterResult = filteredTable; } } - return Ok(JsonConvert.SerializeObject(table)); + return Ok(JsonConvert.SerializeObject(systemCenterResult)); } [HttpGet, Route("OpenMICStatus")] From fa16df8d4b71f0d5d7f1748170005ad9db6f9f96 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 11:19:43 -0400 Subject: [PATCH 03/20] clean up endpoints --- Source/Applications/SystemCenter/Model/DeviceHealthReport.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index 5fbb44232..ea03332f4 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -25,7 +25,6 @@ using GSF.Data.Model; using GSF.Web.Model; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using openXDA.APIAuthentication; using System; using System.Collections.Generic; @@ -35,8 +34,6 @@ using System.Text; using System.Text.RegularExpressions; using System.Web.Http; -using SystemCenter.Controllers; -using System.Collections.Generic; namespace SystemCenter.Model { From a8ab0074e6a5018d498a849110b5ebb5e628daa2 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 13:51:37 -0400 Subject: [PATCH 04/20] pull information for openMICIssues from openMIC --- .../SystemCenter/Model/DeviceHealthReport.cs | 80 ++++++++++++++----- .../DeviceIssuesPage/OpenMICIssuesPage.tsx | 23 ++++-- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index ea03332f4..94de38f03 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -25,6 +25,7 @@ using GSF.Data.Model; using GSF.Web.Model; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using openXDA.APIAuthentication; using System; using System.Collections.Generic; @@ -134,9 +135,9 @@ public class DailyStatisticsRecord public int TotalUnsuccessfulConnections { get; set; } public int TotalSuccessfulConnections { get; set; } - } - public class StatusItem + + public class StatusItem { public string Status { get; set; } public string Description { get; set; } @@ -272,7 +273,7 @@ void ConfigureRequest(HttpRequestMessage request) DataTable systemCenterResult = GetSearchResults(systemCenterRequestBody); - // add empty rows to table for openMIC info + // add empty rows to table for openMIC info systemCenterResult.Columns.Add("MICStatus"); systemCenterResult.Columns.Add("MICBadDays", Type.GetType("System.Int32")); systemCenterResult.Columns.Add("LastGood"); @@ -330,12 +331,12 @@ void ConfigureRequest(HttpRequestMessage request) if (openMicRequestBody.Searches.Count() == 0) // if not filtered by openMIC, add the rest of systemCenter below { foreach (DataRow systemCenterRow in systemCenterResult.Rows) - { + { DataRow existingRow = resultTable.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("Name"), systemCenterRow["Name"])); if (existingRow != null) - { - continue; - } + { + continue; + } resultTable.ImportRow(systemCenterRow); } } @@ -393,14 +394,14 @@ void ConfigureRequest(HttpRequestMessage request) foreach (DataRow devHealthReport in systemCenterResult.Rows) { if (string.IsNullOrEmpty(devHealthReport.ConvertField("OpenMic"))) - { + { continue; - } + } DailyStatisticsRecord openMicRecord = openMicStatistics.FirstOrDefault(record => String.Equals(record.Meter, devHealthReport["OpenMic"])); if (openMicRecord is null) - { - continue; - } + { + continue; + } int totalUnsuccessfulConnections = openMicRecord.TotalUnsuccessfulConnections; devHealthReport["MICStatus"] = ""; if (totalUnsuccessfulConnections > errorLevel) @@ -466,11 +467,11 @@ void ConfigureRequest(HttpRequestMessage request) systemCenterResult = filteredTable; } else - { + { filteredTable = filteredRows.CopyToDataTable(); systemCenterResult = filteredTable; } - } + } return Ok(JsonConvert.SerializeObject(systemCenterResult)); } @@ -483,16 +484,15 @@ void ConfigureRequest(HttpRequestMessage request) request.Method = HttpMethod.Get; } - AppStatus status = new AppStatus() + AppStatus status = new AppStatus() { Status ="Success", Details = [] }; HttpResponseMessage? openMICResponse = null; - try - { + { APIQuery apiQuery = GetAPIQuery(); openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; } @@ -586,8 +586,8 @@ void ConfigureRequest(HttpRequestMessage request) request.Method = HttpMethod.Get; } - try - { + try + { APIQuery apiQuery = GetAPIQuery(); openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; } @@ -652,6 +652,48 @@ void ConfigureRequest(HttpRequestMessage request) return Ok(status); } + [HttpGet, Route("OpenMICMeterStatistics")] + public IHttpActionResult GetOpenMICMeterStatistics([FromUri] string meter) + { + DailyStatisticsRecord[] meterStatistics = []; + OpenMICDailyStatistic[] micDailyStatistics = []; + void ConfigureRequest(HttpRequestMessage request) + { + request.Method = HttpMethod.Get; + } + try + { + APIQuery apiQuery = GetAPIQuery(); + HttpResponseMessage response = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/DailyStatistics/Get/?meter={meter}").Result; + string responseContent = response.Content.ReadAsStringAsync().Result; + string trimmedResponse = responseContent.Trim('"'); + string unescapedResponse = Regex.Unescape(trimmedResponse); + meterStatistics = JsonConvert.DeserializeObject(unescapedResponse); + foreach (DailyStatisticsRecord record in meterStatistics) + { + micDailyStatistics = micDailyStatistics.Append(new() + { + ID = record.ID, + Date = record.Timestamp.ToString(), + Meter = record.Meter, + LastSuccessfulConnection = record.LastSuccessfulConnection, + LastUnsuccessfulConnection = record.LastUnsuccessfulConnection, + LastUnsuccessfulConnectionExplanation = record.LastUnsuccessfulConnectionExplanation, + TotalConnections = record.TotalConnections, + TotalSuccessfulConnections = record.TotalSuccessfulConnections, + TotalUnsuccessfulConnections = record.TotalUnsuccessfulConnections, + BadDays = record.BadDays, + Status = "" + }).ToArray(); + } + } + catch (Exception e) + { + return Ok(); + } + return Ok(JsonConvert.SerializeObject(micDailyStatistics)); + } + public static APIQuery GetAPIQuery() { using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx index 64b047a8b..6a8d85956 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx @@ -28,7 +28,6 @@ import { SystemCenter as SC } from '../global'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import _ from 'lodash'; import * as React from 'react'; -import { GenericController } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ConfigurableTable, ConfigurableColumn, Column } from '@gpa-gemstone/react-table'; import { Plot, Line } from '@gpa-gemstone/react-graph'; @@ -37,7 +36,6 @@ import moment from 'moment'; import { useAppSelector } from '../hooks'; import { SelectRoles } from '../Store/UserSettings'; -const OpenMICDailyStatisticController = new GenericController(`${homePath}api/SystemCenter/Statistics/OpenMIC`, "LastSuccessfulConnection", false); function OpenMICIssuesPage(props: { Meter: OpenXDA.Types.Meter, OpenMICAcronym: string }) { const [data, setData] = React.useState([]); @@ -51,16 +49,27 @@ function OpenMICIssuesPage(props: { Meter: OpenXDA.Types.Meter, OpenMICAcronym: const orderedData = React.useMemo(() => _.orderBy(data, [sortField], [ascending ? 'asc' : 'desc']), [data, sortField, ascending]) + function getStatistics(): JQuery.jqXHR { + return $.ajax({ + type: "Get", + url: `${homePath}api/DeviceHealthReport/OpenMICMeterStatistics/?meter=${props.OpenMICAcronym}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: false, + async: true + }); + } React.useEffect(() => { setStatus('loading') - const handle = OpenMICDailyStatisticController.PagedSearch([], undefined, undefined, 0, props.Meter.AssetKey).done(result => { - const data = JSON.parse(result.Data as unknown as string); + const handle = getStatistics().done(result => { + const data = JSON.parse(result as unknown as string); + console.log(data) setData(data); - }); - setStatus('idle') + setStatus('idle') + }).fail((d) => setStatus('error')); - return () => { + return function cleanup() { if (handle.abort != undefined) handle.abort(); } }, [props.Meter.AssetKey]); From b17962e4d30f13cd44d0db9f17c594eb39372031 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 14:08:04 -0400 Subject: [PATCH 05/20] add status to dailyStatistic --- Source/Applications/SystemCenter/Model/DeviceHealthReport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index 94de38f03..b4cb97345 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -683,7 +683,7 @@ void ConfigureRequest(HttpRequestMessage request) TotalSuccessfulConnections = record.TotalSuccessfulConnections, TotalUnsuccessfulConnections = record.TotalUnsuccessfulConnections, BadDays = record.BadDays, - Status = "" + Status = (record.TotalUnsuccessfulConnections >= 50) ? (record.TotalUnsuccessfulConnections >= 100) ? "Error" : "Warning" : "" }).ToArray(); } } From f4d287750e0ac43f4a686bc8d90f9aa39b634bde Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 14:47:16 -0400 Subject: [PATCH 06/20] set baddays to 0 if no bad days found in DeviceHealthReport query --- .../Applications/SystemCenter/Model/DeviceHealthReport.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index b4cb97345..0e320b441 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -51,9 +51,9 @@ namespace SystemCenter.Model afvtsc.Value as TSC, afvsector.Value as Sector, afvip.Value as IP, - bds.BadDays, - mimdstat.BadDays as MiMDBadDays, - xdastat.BadDays as XDABadDays, + ISNULL(bds.BadDays, 0) as BadDays, + ISNULL(mimdstat.BadDays, 0) as MiMDBadDays, + ISNULL(xdastat.BadDays, 0) as XDABadDays, mimdstat.[Status] as MiMDStatus, xdastat.[Status] as XDAStatus, dqstat.[Status] as DQStatus, @@ -289,7 +289,7 @@ void ConfigureRequest(HttpRequestMessage request) foreach (DailyStatisticsRecord record in openMicStatistics) { string openMICAcronym = record.Meter; - // if there is already a statistic from the meter, delete the other if this timestamp is newer, otherwise skip + DataRow systemCenterRow = systemCenterResult.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("OpenMIC"), record.Meter)); if (systemCenterRow is null) // could be possible due to filters { From 89cb328c2a42e9f9f825e01242c5379bd915b046 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 14:47:51 -0400 Subject: [PATCH 07/20] fix missing key --- .../Scripts/TSX/SystemCenter/DeviceHealthReport/AppStatus.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/AppStatus.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/AppStatus.tsx index 80dcc5a05..c3dfd359b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/AppStatus.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/AppStatus.tsx @@ -87,7 +87,7 @@ const AppStatus = (props: { Name: string, Endpoint: string }) => { > {appStatusData?.Details == null ? <> : appStatusData.Details.map((data, index) => ( -
{GetStatusSymbol(data.Status)} From ef126b68d78ffa9a5e9493842f5d72ed547aa428 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 11 Mar 2026 14:48:31 -0400 Subject: [PATCH 08/20] add tooltip to device health bad days --- .../DeviceHealthReport/DeviceHealthReport.tsx | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx index 994b58ee3..a99a946fd 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx @@ -32,7 +32,7 @@ import { SystemCenterSettingSlice } from '../Store/Store'; import moment from 'moment'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import AppStatus from './AppStatus' -import { ErrorBoundary } from '@gpa-gemstone/common-pages' +import { ToolTip } from '@gpa-gemstone/react-forms' const defaultSearchcols: Search.IField[] = [ { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, @@ -65,6 +65,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { const settings = useAppSelector(SystemCenterSettingSlice.Data); const settingStatus = useAppSelector(SystemCenterSettingSlice.Status); + const [hovered, setHovered] = React.useState(null); React.useEffect(() => { let handle = getMeters(); @@ -81,7 +82,6 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { React.useEffect(() => { let handle = getAdditionalFields(); - return () => { if (handle.abort != null) handle.abort(); } @@ -93,11 +93,9 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { }, [settingStatus]); - function getMeters(): JQuery.jqXHR{ setSearchState('Loading'); let searches = search.map(s => { if (defaultSearchcols.findIndex(item => item.key == s.FieldName) == -1) return { ...s, IsPivotColumn: true }; else return s; }) - return $.ajax({ type: "Post", url: `${homePath}api/DeviceHealthReport/SearchableList`, @@ -336,6 +334,38 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { Field={'BadDays'} HeaderStyle={{ width: 100 }} RowStyle={{ width: 100, textAlign: 'center' }} + Content={({ item, key, index }) => { + if (item[key] == undefined) + return ''; + return( + <> +
setHovered(index)} + onMouseLeave={() => setHovered(null)} + > + {item[key]} +
+ +
    +
  • + {`openMIC: ${item['MICBadDays']}` } +
  • +
  • + {`miMD: ${item['MiMDBadDays']}`} +
  • +
  • + {`openXDA: ${item['XDABadDays']}`} +
  • +
+
+ + ) + }} > Bad Days From 262bf9a632d561b073073c78812d57411d9dc92b Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 09:38:05 -0400 Subject: [PATCH 09/20] do not show openMIC bad days if value would be null --- .../DeviceHealthReport/DeviceHealthReport.tsx | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx index a99a946fd..af30b7cd1 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx @@ -32,7 +32,7 @@ import { SystemCenterSettingSlice } from '../Store/Store'; import moment from 'moment'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import AppStatus from './AppStatus' -import { ToolTip } from '@gpa-gemstone/react-forms' +import { ToolTip } from '@gpa-gemstone/react-forms' const defaultSearchcols: Search.IField[] = [ { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, @@ -93,7 +93,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { }, [settingStatus]); - function getMeters(): JQuery.jqXHR{ + function getMeters(): JQuery.jqXHR { setSearchState('Loading'); let searches = search.map(s => { if (defaultSearchcols.findIndex(item => item.key == s.FieldName) == -1) return { ...s, IsPivotColumn: true }; else return s; }) return $.ajax({ @@ -155,30 +155,30 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => {
CollumnList={filterableList} SetFilter={(flds) => setSearch(flds)} Direction={'left'} defaultCollumn={standardSearch} Width={'50%'} Label={'Search'} StorageID="DeviceHealthReportFilter" - ShowLoading={searchState == 'Loading'} ResultNote={searchState == 'Error' ? 'Could not complete Search' : 'Found ' + data.length + ' Meter(s)'} - GetEnum={(setOptions, field) => { - let handle = null; - if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) - return () => { }; - - handle = $.ajax({ - type: "GET", - url: `${homePath}api/ValueList/Group/${field.enum[0].Value}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); - - handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) - return () => { if (handle != null && handle.abort == null) handle.abort(); } - }} + ShowLoading={searchState == 'Loading'} ResultNote={searchState == 'Error' ? 'Could not complete Search' : 'Found ' + data.length + ' Meter(s)'} + GetEnum={(setOptions, field) => { + let handle = null; + if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) + return () => { }; + + handle = $.ajax({ + type: "GET", + url: `${homePath}api/ValueList/Group/${field.enum[0].Value}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }); + + handle.done(d => setOptions(d.map(item => ({ Value: item.Value.toString(), Label: item.Text })))) + return () => { if (handle != null && handle.abort == null) handle.abort(); } + }} >
  • Connection Status:
    - + { Name="Scada Trigger" Endpoint="ScadaTriggerStatus" /> - -
    + +
  • @@ -225,7 +225,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { Field={'Name'} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} - Content={({ item, field }) => {item[field]} } + Content={({ item, field }) => {item[field]}} > Name @@ -259,7 +259,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { return item.LocationKey }} - + > Substn @@ -268,7 +268,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { Field={'Model'} HeaderStyle={{ width: '8%' }} RowStyle={{ width: '8%' }} - Content={({ item, field }) => { + Content={({ item, field }) => { const MimdUrl = settings.find(s => s.Name == 'MiMD.Url')?.Value; if (MimdUrl != undefined) @@ -284,7 +284,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { Field={'TSC'} HeaderStyle={{ width: 50 }} RowStyle={{ width: 50 }} - Content={({ item, field }) => {item[field]} } + Content={({ item, field }) => {item[field]}} > TSC @@ -293,7 +293,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { Field={'Sector'} HeaderStyle={{ width: '5%' }} RowStyle={{ width: '5%' }} - Content={({ item, field }) => {item[field]} } + Content={({ item, field }) => {item[field]}} > Sector @@ -337,24 +337,25 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { Content={({ item, key, index }) => { if (item[key] == undefined) return ''; - return( - <> -
    setHovered(index)} - onMouseLeave={() => setHovered(null)} + return ( + <> +
    setHovered(index)} + onMouseLeave={() => setHovered(null)} > - {item[key]} -
    - + -
      +
        {item['MICBadDays'] == null ? null :
      • - {`openMIC: ${item['MICBadDays']}` } + {`openMIC: ${item['MICBadDays']}`}
      • + }
      • {`miMD: ${item['MiMDBadDays']}`}
      • @@ -362,7 +363,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { {`openXDA: ${item['XDABadDays']}`}
      - + ) }} From 5b9cb22223e4e61db9803b0da6919f153876b424 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 09:38:56 -0400 Subject: [PATCH 10/20] move badDayFilter to helper function --- .../SystemCenter/Model/DeviceHealthReport.cs | 131 +++++++----------- 1 file changed, 47 insertions(+), 84 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index 0e320b441..f6e141e86 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -342,48 +342,7 @@ void ConfigureRequest(HttpRequestMessage request) } foreach (SQLSearchFilter badDayFilter in badDayFilters) { - DataTable filteredTable; - DataRow[] filteredRows = []; - // Bad Days can be =, <> (neq), <, <=, >, >=, with a number - switch (badDayFilter.Operator) - { - case "=": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) == int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case "<>": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) != int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case "<": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) < int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case "<=": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) <= int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case ">": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) > int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case ">=": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) >= int.Parse(badDayFilter.SearchText)).ToArray(); - break; - default: - break; - } - if (filteredRows.Length == 0) - { - filteredTable = systemCenterResult.Clone(); - resultTable = filteredTable; - } - else - { - filteredTable = filteredRows.CopyToDataTable(); - resultTable = filteredTable; - } + resultTable = FilterBadDays(resultTable, badDayFilter); } return Ok(JsonConvert.SerializeObject(resultTable)); } @@ -429,48 +388,7 @@ void ConfigureRequest(HttpRequestMessage request) foreach (SQLSearchFilter badDayFilter in badDayFilters) { - DataTable filteredTable; - DataRow[] filteredRows = []; - // Bad Days can be =, <> (neq), <, <=, >, >=, with a number - switch (badDayFilter.Operator) - { - case "=": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) == int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case "<>": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) != int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case "<": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) < int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case "<=": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) <= int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case ">": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) > int.Parse(badDayFilter.SearchText)).ToArray(); - break; - case ">=": - filteredRows = resultTable.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) >= int.Parse(badDayFilter.SearchText)).ToArray(); - break; - default: - break; - } - if (filteredRows.Length == 0) - { - filteredTable = systemCenterResult.Clone(); - systemCenterResult = filteredTable; - } - else - { - filteredTable = filteredRows.CopyToDataTable(); - systemCenterResult = filteredTable; - } + systemCenterResult = FilterBadDays(systemCenterResult, badDayFilter); } return Ok(JsonConvert.SerializeObject(systemCenterResult)); } @@ -708,5 +626,50 @@ public static APIQuery GetAPIQuery() return query; } } + + public static DataTable FilterBadDays(DataTable table, SQLSearchFilter badDayFilter) + { + DataTable filteredTable; + DataRow[] filteredRows = []; + // Bad Days can be =, <> (neq), <, <=, >, >=, with a number + switch (badDayFilter.Operator) + { + case "=": + filteredRows = table.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) == int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<>": + filteredRows = table.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) != int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<": + filteredRows = table.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) < int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case "<=": + filteredRows = table.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) <= int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case ">": + filteredRows = table.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) > int.Parse(badDayFilter.SearchText)).ToArray(); + break; + case ">=": + filteredRows = table.AsEnumerable() + .Where(row => (row.Field("BadDays") ?? 0) >= int.Parse(badDayFilter.SearchText)).ToArray(); + break; + default: + break; + } + if (filteredRows.Length == 0) + { + filteredTable = table.Clone(); + } + else + { + filteredTable = filteredRows.CopyToDataTable(); + } + return filteredTable; + } } } \ No newline at end of file From 02fd237d5058b3c5f7bd983c6f25fb2b6863206c Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 11:40:33 -0400 Subject: [PATCH 11/20] clean up table combination logic --- .../SystemCenter/Model/DeviceHealthReport.cs | 504 +++++++++--------- 1 file changed, 259 insertions(+), 245 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index f6e141e86..e47473283 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -38,7 +38,7 @@ namespace SystemCenter.Model { - [CustomView(@" + [CustomView(@" SELECT Meter.ID, Meter.Name, @@ -83,33 +83,33 @@ SELECT MAX(BadDays) as BadDays FROM ( SELECT TOP 1 BadDays FROM [MiMDDailyStatistic] WHERE Meter.AssetKey = [MiMDDailyStatistic].Meter ORDER BY Cast([MiMDDailyStatistic].Date as date) DESC ) t ) as bds "), SettingsCategory("systemSettings"), ViewOnly, AllowSearch] - public class DeviceHealthReport - { - public int ID { get; set; } - public string Name { get; set; } - public string Model { get; set; } - public string TimeZone { get; set; } - public string OpenMIC { get; set; } - public int LocationID { get; set; } - public string Substation { get; set; } - public string LocationKey { get; set; } - public string TSC { get; set; } - public string Sector { get; set; } - public string IP { get; set; } - public DateTime LastGood { get; set; } // no longer gotten from table - public int BadDays { get; set; } - public int MiMDBadDays { get; set; } - public int MICBadDays { get; set; } // no longer gotten from table - public int XDABadDays { get; set; } - public string MiMDStatus { get; set; } + public class DeviceHealthReport + { + public int ID { get; set; } + public string Name { get; set; } + public string Model { get; set; } + public string TimeZone { get; set; } + public string OpenMIC { get; set; } + public int LocationID { get; set; } + public string Substation { get; set; } + public string LocationKey { get; set; } + public string TSC { get; set; } + public string Sector { get; set; } + public string IP { get; set; } + public DateTime LastGood { get; set; } // no longer gotten from table + public int BadDays { get; set; } + public int MiMDBadDays { get; set; } + public int MICBadDays { get; set; } // no longer gotten from table + public int XDABadDays { get; set; } + public string MiMDStatus { get; set; } public string MICStatus { get; set; } // no longer gotten from table - public string XDAStatus { get; set; } - public DateTime LastConfigChange { get; set; } - } + public string XDAStatus { get; set; } + public DateTime LastConfigChange { get; set; } + } - [RoutePrefix("api/DeviceHealthReport")] - public class DeviceHealthReportController : ModelController - { + [RoutePrefix("api/DeviceHealthReport")] + public class DeviceHealthReportController : ModelController + { public class DailyStatisticsRecord { [PrimaryKey(true)] @@ -138,31 +138,28 @@ public class DailyStatisticsRecord } public class StatusItem - { - public string Status { get; set; } - public string Description { get; set; } - } + { + public string Status { get; set; } + public string Description { get; set; } + } - public class AppStatus - { - public string Status { get; set; } + public class AppStatus + { + public string Status { get; set; } - public List Details { get; set; } - - } + public List Details { get; set; } + + } public override IHttpActionResult GetSearchableList([FromBody] PostData postData) { - int warningLevel = 50; - int errorLevel = 100; - PostData openMicRequestBody = new() { Searches = [], Ascending = true, OrderBy = "Timestamp" }; - + PostData systemCenterRequestBody = new() { Searches = [], @@ -170,8 +167,6 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData OrderBy = "Name" }; - bool openMICPrecedence = false; - SQLSearchFilter[] badDayFilters = []; // for each filter, add it to the request body that would use it @@ -179,10 +174,14 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { if (filter.FieldName == "LastGood") { - filter.FieldName = "LastSuccessfulConnection"; - openMicRequestBody.Searches = openMicRequestBody.Searches.Append(filter); - openMICPrecedence = true; - continue; + SQLSearchFilter newFilter = new() + { + FieldName = "LastSuccessfulConnection", + SearchText = filter.SearchText, + Operator = filter.Operator, + }; + openMicRequestBody.Searches = openMicRequestBody.Searches.Append(newFilter); + continue; } if (filter.FieldName == "MICStatus") { @@ -192,13 +191,13 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { FieldName = "TotalUnsuccessfulConnections", Operator = ">=", - SearchText = warningLevel.ToString() + SearchText = WarningLevel.ToString() }; SQLSearchFilter maxFilter = new() { FieldName = "TotalUnsuccessfulConnections", Operator = "<", - SearchText = errorLevel.ToString() + SearchText = ErrorLevel.ToString() }; openMicRequestBody.Searches = openMicRequestBody.Searches.Append(minFilter); openMicRequestBody.Searches = openMicRequestBody.Searches.Append(maxFilter); @@ -210,7 +209,7 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { FieldName = "TotalUnsuccessfulConnections", Operator = ">=", - SearchText = errorLevel.ToString() + SearchText = ErrorLevel.ToString() }; openMicRequestBody.Searches = openMicRequestBody.Searches.Append(errorFilter); } @@ -220,7 +219,7 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { FieldName = "TotalUnsuccessfulConnections", Operator = ">=", - SearchText = warningLevel.ToString() + SearchText = WarningLevel.ToString() }; openMicRequestBody.Searches = openMicRequestBody.Searches.Append(warningFilter); } @@ -228,7 +227,7 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData if (filter.FieldName == "BadDays") { badDayFilters = badDayFilters.Append(filter).ToArray(); - continue; + continue; } systemCenterRequestBody.Searches = systemCenterRequestBody.Searches.Append(filter); } @@ -238,11 +237,11 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData { openMicRequestBody.OrderBy = "LastSuccessfulConnection"; openMicRequestBody.Ascending = postData.Ascending; - openMICPrecedence = true; } else if (postData.OrderBy == "MICStatus") { - // todo SORT + openMicRequestBody.OrderBy = "TotalSuccessfulConnections"; + openMicRequestBody.Ascending = postData.Ascending; } else { @@ -250,6 +249,15 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData systemCenterRequestBody.Ascending = postData.Ascending; } + DataTable systemCenterResult = GetSearchResults(systemCenterRequestBody); + + // add empty rows to table for openMIC info + systemCenterResult.Columns.Add("MICStatus"); + systemCenterResult.Columns.Add("MICBadDays", Type.GetType("System.Int32")); + systemCenterResult.Columns.Add("LastGood"); + + // ADD A FILTER TO FILTER OPEN MIC RECORDS BY WHAT IS IN SYSTEM CENTER + DailyStatisticsRecord[] openMicStatistics; void ConfigureRequest(HttpRequestMessage request) @@ -261,170 +269,58 @@ void ConfigureRequest(HttpRequestMessage request) { APIQuery apiQuery = GetAPIQuery(); HttpResponseMessage response = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/DailyStatistics/SearchableList").Result; - string responseContent = response.Content.ReadAsStringAsync().Result; + string responseContent = response.Content.ReadAsStringAsync().Result; string trimmedResponse = responseContent.Trim('"'); string unescapedResponse = Regex.Unescape(trimmedResponse); openMicStatistics = JsonConvert.DeserializeObject(unescapedResponse); } catch (Exception e) { - return Ok(); - } - - DataTable systemCenterResult = GetSearchResults(systemCenterRequestBody); - - // add empty rows to table for openMIC info - systemCenterResult.Columns.Add("MICStatus"); - systemCenterResult.Columns.Add("MICBadDays", Type.GetType("System.Int32")); - systemCenterResult.Columns.Add("LastGood"); - - DataTable resultTable; - - - // if sorted by or filter by an openMIC field, stitch SystemCenter to existing openMIC data - if (openMICPrecedence) - { - resultTable = systemCenterResult.Clone(); - - foreach (DailyStatisticsRecord record in openMicStatistics) - { - string openMICAcronym = record.Meter; - - DataRow systemCenterRow = systemCenterResult.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("OpenMIC"), record.Meter)); - if (systemCenterRow is null) // could be possible due to filters - { - continue; - } - int totalUnsuccessfulConnections = record.TotalUnsuccessfulConnections; - systemCenterRow["MICStatus"] = ""; - if (totalUnsuccessfulConnections > errorLevel) - { - systemCenterRow["Status"] = "Error"; - - } - else if (totalUnsuccessfulConnections > warningLevel) - { - systemCenterRow["Status"] = "Warning"; - } - - systemCenterRow["MICBadDays"] = record.BadDays; - - if (record.BadDays > Convert.ToInt32(systemCenterRow["BadDays"])) - { - systemCenterRow["BadDays"] = record.BadDays; - } - - - if (!(record.LastSuccessfulConnection is null)) - { - systemCenterRow["LastGood"] = record.LastSuccessfulConnection; - } - resultTable.ImportRow(systemCenterRow); - } - if (postData.OrderBy == "LastGood") - { - DataView view = resultTable.DefaultView; - string asc = postData.Ascending ? "ASC" : "DESC"; - view.Sort = $"LastGood {asc}"; - resultTable = view.ToTable(); - } - if (openMicRequestBody.Searches.Count() == 0) // if not filtered by openMIC, add the rest of systemCenter below - { - foreach (DataRow systemCenterRow in systemCenterResult.Rows) - { - DataRow existingRow = resultTable.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("Name"), systemCenterRow["Name"])); - if (existingRow != null) - { - continue; - } - resultTable.ImportRow(systemCenterRow); - } - } - foreach (SQLSearchFilter badDayFilter in badDayFilters) - { - resultTable = FilterBadDays(resultTable, badDayFilter); - } - return Ok(JsonConvert.SerializeObject(resultTable)); - } - - resultTable = systemCenterResult; - - // stitch openMIC data to SystemCenter info - foreach (DataRow devHealthReport in systemCenterResult.Rows) - { - if (string.IsNullOrEmpty(devHealthReport.ConvertField("OpenMic"))) - { - continue; - } - DailyStatisticsRecord openMicRecord = openMicStatistics.FirstOrDefault(record => String.Equals(record.Meter, devHealthReport["OpenMic"])); - if (openMicRecord is null) - { - continue; - } - int totalUnsuccessfulConnections = openMicRecord.TotalUnsuccessfulConnections; - devHealthReport["MICStatus"] = ""; - if (totalUnsuccessfulConnections > errorLevel) - { - devHealthReport["Status"] = "Error"; - - } - else if (totalUnsuccessfulConnections > warningLevel) - { - devHealthReport["Status"] = "Warning"; - } - - devHealthReport["MICBadDays"] = openMicRecord.BadDays; - - if (openMicRecord.BadDays > Convert.ToInt32(devHealthReport["BadDays"])) - { - devHealthReport["BadDays"] = openMicRecord.BadDays; - } - - if (openMicRecord.LastSuccessfulConnection != null) - { - devHealthReport["LastGood"] = openMicRecord.LastSuccessfulConnection; - } + return Ok(systemCenterResult); // return just the system center stuff } + + DataTable resultTable = CombineData(systemCenterResult, openMicStatistics, postData); foreach (SQLSearchFilter badDayFilter in badDayFilters) { - systemCenterResult = FilterBadDays(systemCenterResult, badDayFilter); + resultTable = FilterBadDays(resultTable, badDayFilter); } - return Ok(JsonConvert.SerializeObject(systemCenterResult)); + + return Ok(JsonConvert.SerializeObject(resultTable)); } - [HttpGet, Route("OpenMICStatus")] + [HttpGet, Route("OpenMICStatus")] public IHttpActionResult GetOpenMICStatus() - { + { void ConfigureRequest(HttpRequestMessage request) { request.Method = HttpMethod.Get; } - AppStatus status = new AppStatus() - { - Status ="Success", - Details = [] - }; + AppStatus status = new AppStatus() + { + Status = "Success", + Details = [] + }; - HttpResponseMessage? openMICResponse = null; + HttpResponseMessage? openMICResponse = null; try { APIQuery apiQuery = GetAPIQuery(); openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; - } - catch - { - status.Status = "Error"; - status.Details.Add(new StatusItem() - { - Status = "Error", - Description = "Could not connect to openMIC. Check the OpenMIC.URL setting in System Center Settings." + } + catch + { + status.Status = "Error"; + status.Details.Add(new StatusItem() + { + Status = "Error", + Description = "Could not connect to openMIC. Check the OpenMIC.URL setting in System Center Settings." }); } - if (openMICResponse is null) + if (openMICResponse is null) return Ok(status); status.Details.Add(new StatusItem() @@ -434,15 +330,15 @@ void ConfigureRequest(HttpRequestMessage request) }); if (openMICResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { + { status.Status = "Error"; status.Details.Add(new StatusItem() { Status = "Error", - Description = "Could not authorize with openMIC. Check the OpenMIC.Credential and OpenMIC.Password settings in System Center Settings." - }); + Description = "Could not authorize with openMIC. Check the OpenMIC.Credential and OpenMIC.Password settings in System Center Settings." + }); - return Ok(status); + return Ok(status); } if (openMICResponse.StatusCode == System.Net.HttpStatusCode.NotFound) @@ -475,28 +371,29 @@ void ConfigureRequest(HttpRequestMessage request) Description = "Successful authentication." }); - string r = openMICResponse.Content.ReadAsStringAsync().Result; + string r = openMICResponse.Content.ReadAsStringAsync().Result; - StatusItem[] responseStatus = JsonConvert.DeserializeObject(r); + StatusItem[] responseStatus = JsonConvert.DeserializeObject(r); status.Details.AddRange(responseStatus); - if (status.Details.Any(item => item.Status == "Error")) { - status.Status = "Error"; - } + if (status.Details.Any(item => item.Status == "Error")) + { + status.Status = "Error"; + } return Ok(status); } - [HttpGet, Route("ScadaTriggerStatus")] - public IHttpActionResult GetScadaTriggerStatus() - { + [HttpGet, Route("ScadaTriggerStatus")] + public IHttpActionResult GetScadaTriggerStatus() + { AppStatus status = new AppStatus() { Status = "Success", Details = [] }; - HttpResponseMessage openMICResponse = null; + HttpResponseMessage openMICResponse = null; void ConfigureRequest(HttpRequestMessage request) @@ -507,31 +404,31 @@ void ConfigureRequest(HttpRequestMessage request) try { APIQuery apiQuery = GetAPIQuery(); - openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; - } - catch - { - status.Status = "N/A"; - status.Details.Add(new StatusItem() - { - Status = "Error", - Description = "Could not connect to openMIC." - }); - } - - if (openMICResponse is null) - { - return Ok(status); - } - - HttpResponseMessage scadaTriggerResponse = null; - try - { + openMICResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getsystemstatus/").Result; + } + catch + { + status.Status = "N/A"; + status.Details.Add(new StatusItem() + { + Status = "Error", + Description = "Could not connect to openMIC." + }); + } + + if (openMICResponse is null) + { + return Ok(status); + } + + HttpResponseMessage scadaTriggerResponse = null; + try + { APIQuery apiQuery = GetAPIQuery(); scadaTriggerResponse = apiQuery.SendWebRequestAsync(ConfigureRequest, $"api/health/getscadatriggerhealth/").Result; - } - catch - { + } + catch + { status.Status = "N/A"; status.Details.Add(new StatusItem() { @@ -540,38 +437,39 @@ void ConfigureRequest(HttpRequestMessage request) }); } - if (scadaTriggerResponse is null) - { - return Ok(status); - } + if (scadaTriggerResponse is null) + { + return Ok(status); + } - if (scadaTriggerResponse.StatusCode != System.Net.HttpStatusCode.OK) - { - status.Status = "N/A"; - status.Details.Add(new StatusItem() - { - Status = "Error", + if (scadaTriggerResponse.StatusCode != System.Net.HttpStatusCode.OK) + { + status.Status = "N/A"; + status.Details.Add(new StatusItem() + { + Status = "Error", Description = "Could not establish connection to Scada Trigger." - }); + }); - return Ok(status); + return Ok(status); } string r = scadaTriggerResponse.Content.ReadAsStringAsync().Result; StatusItem[] responseStatus = JsonConvert.DeserializeObject(r); - status.Details.AddRange(responseStatus); + status.Details.AddRange(responseStatus); - if (status.Details.Any(item => item.Status == "Error")) { + if (status.Details.Any(item => item.Status == "Error")) + { status.Status = "Error"; } - return Ok(status); - } + return Ok(status); + } [HttpGet, Route("OpenMICMeterStatistics")] - public IHttpActionResult GetOpenMICMeterStatistics([FromUri] string meter) + public IHttpActionResult GetOpenMICMeterStatistics([FromUri] string meter) { DailyStatisticsRecord[] meterStatistics = []; OpenMICDailyStatistic[] micDailyStatistics = []; @@ -601,7 +499,7 @@ void ConfigureRequest(HttpRequestMessage request) TotalSuccessfulConnections = record.TotalSuccessfulConnections, TotalUnsuccessfulConnections = record.TotalUnsuccessfulConnections, BadDays = record.BadDays, - Status = (record.TotalUnsuccessfulConnections >= 50) ? (record.TotalUnsuccessfulConnections >= 100) ? "Error" : "Warning" : "" + Status = (record.TotalUnsuccessfulConnections >= 50) ? (record.TotalUnsuccessfulConnections >= 100) ? "Error" : "Warning" : "" }).ToArray(); } } @@ -611,7 +509,7 @@ void ConfigureRequest(HttpRequestMessage request) } return Ok(JsonConvert.SerializeObject(micDailyStatistics)); } - + public static APIQuery GetAPIQuery() { using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) @@ -626,7 +524,7 @@ public static APIQuery GetAPIQuery() return query; } } - + public static DataTable FilterBadDays(DataTable table, SQLSearchFilter badDayFilter) { DataTable filteredTable; @@ -671,5 +569,121 @@ public static DataTable FilterBadDays(DataTable table, SQLSearchFilter badDayFil } return filteredTable; } + + public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] openMicData, PostData postData) + { + DataTable resultTable = systemCenterData.Clone(); + + Tuple[] dataTuples = []; + + bool openMICFiltered = (postData.Searches.Any(search => Equals(search.FieldName, "LastGood") || Equals(search.FieldName, "MICStatus"))); + + bool openMICPrecedence = (postData.OrderBy == "LastGood" || postData.OrderBy == "MICStatus"); + + if (openMICPrecedence) // if sorted or filtered by openMICField + { + foreach (DailyStatisticsRecord record in openMicData) + { + string openMICAcronym = record.Meter; + + DataRow systemCenterRow = systemCenterData.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("OpenMIC"), record.Meter)); + + if (systemCenterRow is null) // could be possible due to filters + { + continue; + } + + Tuple newTuple = Tuple.Create(systemCenterRow, record); + + dataTuples = dataTuples.Append(newTuple).ToArray(); + + } + if (!openMICFiltered) // if not filtered by openMIC, add the rest of systemCenter below + { + foreach (DataRow systemCenterRow in systemCenterData.Rows) + { + Tuple existingTuple = dataTuples.FirstOrDefault(tuple => Equals(tuple.Item1.Field("Name"), systemCenterRow["Name"])); + if (existingTuple != null) + { + continue; + } + Tuple newTuple = Tuple.Create(systemCenterRow, null); + + dataTuples = dataTuples.Append(newTuple).ToArray(); + } + } + } + else + { + foreach (DataRow row in systemCenterData.Rows) + { + if (string.IsNullOrEmpty(row.ConvertField("OpenMic"))) + { + if (openMICFiltered) { continue; } + + Tuple tuple = Tuple.Create(row, null); + dataTuples = dataTuples.Append(tuple).ToArray(); + continue; + } + + DailyStatisticsRecord openMicRecord = openMicData.FirstOrDefault(record => String.Equals(record.Meter, row["OpenMic"])); + + if (openMicRecord is null) + { + if (openMICFiltered) { continue; } + + Tuple tuple = Tuple.Create(row, null); + dataTuples = dataTuples.Append(tuple).ToArray(); + continue; + } + + Tuple newTuple = Tuple.Create(row, openMicRecord); + + dataTuples = dataTuples.Append(newTuple).ToArray(); + } + } + + foreach (var (SC, openMIC) in dataTuples) + { + if (openMIC is null) + { + resultTable.ImportRow(SC); + continue; + } + + int totalUnsuccessfulConnections = openMIC.TotalUnsuccessfulConnections; + + SC["MICStatus"] = ""; + + if (totalUnsuccessfulConnections > ErrorLevel) + { + SC["Status"] = "Error"; + + } + else if (totalUnsuccessfulConnections > WarningLevel) + { + SC["Status"] = "Warning"; + } + + SC["MICBadDays"] = openMIC.BadDays; + + if (openMIC.BadDays > Convert.ToInt32(SC["BadDays"])) + { + SC["BadDays"] = openMIC.BadDays; + } + + if (openMIC.LastSuccessfulConnection != null) + { + SC["LastGood"] = openMIC.LastSuccessfulConnection; + } + + resultTable.ImportRow(SC); + } + + return resultTable; + } + + public int WarningLevel { get; set; } = 50; + public int ErrorLevel { get; set; } = 50; } } \ No newline at end of file From 904aa41d1f447d9cf12a4ba246a0ee89e50a38b9 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 11:57:03 -0400 Subject: [PATCH 12/20] clean up tuple nonsense in combination logic --- .../SystemCenter/Model/DeviceHealthReport.cs | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index e47473283..a35307c5e 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -285,6 +285,14 @@ void ConfigureRequest(HttpRequestMessage request) { resultTable = FilterBadDays(resultTable, badDayFilter); } + + if (postData.OrderBy == "BadDays") + { + DataView dataView = resultTable.DefaultView; + string asc = postData.Ascending ? "ASC" : "DESC"; + dataView.Sort = $"BadDays {asc}"; + resultTable = dataView.ToTable(); + } return Ok(JsonConvert.SerializeObject(resultTable)); } @@ -574,8 +582,6 @@ public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] { DataTable resultTable = systemCenterData.Clone(); - Tuple[] dataTuples = []; - bool openMICFiltered = (postData.Searches.Any(search => Equals(search.FieldName, "LastGood") || Equals(search.FieldName, "MICStatus"))); bool openMICPrecedence = (postData.OrderBy == "LastGood" || postData.OrderBy == "MICStatus"); @@ -593,23 +599,20 @@ public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] continue; } - Tuple newTuple = Tuple.Create(systemCenterRow, record); - - dataTuples = dataTuples.Append(newTuple).ToArray(); + resultTable.ImportRow(CombineRow(systemCenterRow, record)); } if (!openMICFiltered) // if not filtered by openMIC, add the rest of systemCenter below { foreach (DataRow systemCenterRow in systemCenterData.Rows) { - Tuple existingTuple = dataTuples.FirstOrDefault(tuple => Equals(tuple.Item1.Field("Name"), systemCenterRow["Name"])); - if (existingTuple != null) + DataRow existingRow = resultTable.AsEnumerable() + .FirstOrDefault(resultRow => Equals(resultRow.Field("Name"), systemCenterRow["Name"])); + if (existingRow != null) { continue; } - Tuple newTuple = Tuple.Create(systemCenterRow, null); - - dataTuples = dataTuples.Append(newTuple).ToArray(); + resultTable.ImportRow(CombineRow(systemCenterRow, null)); } } } @@ -621,8 +624,7 @@ public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] { if (openMICFiltered) { continue; } - Tuple tuple = Tuple.Create(row, null); - dataTuples = dataTuples.Append(tuple).ToArray(); + resultTable.ImportRow(CombineRow(row, null)); continue; } @@ -632,55 +634,50 @@ public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] { if (openMICFiltered) { continue; } - Tuple tuple = Tuple.Create(row, null); - dataTuples = dataTuples.Append(tuple).ToArray(); + resultTable.ImportRow(CombineRow(row, null)); continue; } - - Tuple newTuple = Tuple.Create(row, openMicRecord); - - dataTuples = dataTuples.Append(newTuple).ToArray(); + resultTable.ImportRow(CombineRow(row, openMicRecord)); } } - foreach (var (SC, openMIC) in dataTuples) + return resultTable; + } + + public DataRow CombineRow(DataRow systemCenterRow, DailyStatisticsRecord? openMICRecord) + { + if (openMICRecord is null) { - if (openMIC is null) - { - resultTable.ImportRow(SC); - continue; - } + return systemCenterRow; + } - int totalUnsuccessfulConnections = openMIC.TotalUnsuccessfulConnections; + int totalUnsuccessfulConnections = openMICRecord.TotalUnsuccessfulConnections; - SC["MICStatus"] = ""; + systemCenterRow["MICStatus"] = ""; - if (totalUnsuccessfulConnections > ErrorLevel) - { - SC["Status"] = "Error"; - - } - else if (totalUnsuccessfulConnections > WarningLevel) - { - SC["Status"] = "Warning"; - } + if (totalUnsuccessfulConnections > ErrorLevel) + { + systemCenterRow["Status"] = "Error"; - SC["MICBadDays"] = openMIC.BadDays; + } + else if (totalUnsuccessfulConnections > WarningLevel) + { + systemCenterRow["Status"] = "Warning"; + } - if (openMIC.BadDays > Convert.ToInt32(SC["BadDays"])) - { - SC["BadDays"] = openMIC.BadDays; - } + systemCenterRow["MICBadDays"] = openMICRecord.BadDays; - if (openMIC.LastSuccessfulConnection != null) - { - SC["LastGood"] = openMIC.LastSuccessfulConnection; - } + if (openMICRecord.BadDays > Convert.ToInt32(systemCenterRow["BadDays"])) + { + systemCenterRow["BadDays"] = openMICRecord.BadDays; + } - resultTable.ImportRow(SC); + if (openMICRecord.LastSuccessfulConnection != null) + { + systemCenterRow["LastGood"] = openMICRecord.LastSuccessfulConnection; } - return resultTable; + return systemCenterRow; } public int WarningLevel { get; set; } = 50; From 6f8d40edbad331812a7cab7c0f204d179d96d7c6 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 12:10:48 -0400 Subject: [PATCH 13/20] serialize openMIC connection error response --- .../Applications/SystemCenter/Model/DeviceHealthReport.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index a35307c5e..e8af10d0c 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -276,7 +276,7 @@ void ConfigureRequest(HttpRequestMessage request) } catch (Exception e) { - return Ok(systemCenterResult); // return just the system center stuff + return Ok(JsonConvert.SerializeObject(systemCenterResult)); // return just the system center stuff } DataTable resultTable = CombineData(systemCenterResult, openMicStatistics, postData); @@ -592,7 +592,7 @@ public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] { string openMICAcronym = record.Meter; - DataRow systemCenterRow = systemCenterData.AsEnumerable().FirstOrDefault(r => String.Equals(r.Field("OpenMIC"), record.Meter)); + DataRow systemCenterRow = systemCenterData.AsEnumerable().FirstOrDefault(r => Equals(r.Field("OpenMIC"), record.Meter)); if (systemCenterRow is null) // could be possible due to filters { @@ -628,7 +628,7 @@ public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] continue; } - DailyStatisticsRecord openMicRecord = openMicData.FirstOrDefault(record => String.Equals(record.Meter, row["OpenMic"])); + DailyStatisticsRecord openMicRecord = openMicData.FirstOrDefault(record => Equals(record.Meter, row["OpenMic"])); if (openMicRecord is null) { From 35bed4042fdf9d1091fdc1fcd40d6433c4a401ed Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 12:30:45 -0400 Subject: [PATCH 14/20] filter openMIC query by meters in SystemCenter --- .../SystemCenter/Model/DeviceHealthReport.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index e8af10d0c..3ab20bf1d 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -25,7 +25,6 @@ using GSF.Data.Model; using GSF.Web.Model; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using openXDA.APIAuthentication; using System; using System.Collections.Generic; @@ -256,7 +255,18 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData systemCenterResult.Columns.Add("MICBadDays", Type.GetType("System.Int32")); systemCenterResult.Columns.Add("LastGood"); - // ADD A FILTER TO FILTER OPEN MIC RECORDS BY WHAT IS IN SYSTEM CENTER + // get a list of meters to search for in open mic + string[] openMICMeters = systemCenterResult.AsEnumerable() + .Select(row => row.Field("OpenMIC")).Where(openMIC => openMIC != "").ToArray(); + + SQLSearchFilter openMICMeterFilter = new() + { + SearchText = $"({String.Join(",", openMICMeters)})", + Operator = "IN", + FieldName = "Meter" + }; + + openMicRequestBody.Searches.Append(openMICMeterFilter); DailyStatisticsRecord[] openMicStatistics; From af1e03c36761f15c2d65b1d324b1b4ac957da9fc Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 15:30:11 -0400 Subject: [PATCH 15/20] clean up table join logic --- .../SystemCenter/Model/DeviceHealthReport.cs | 206 +++++++++--------- 1 file changed, 99 insertions(+), 107 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index 3ab20bf1d..d27d8bba1 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -34,6 +34,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Web.Http; +using System.Windows.Forms.DataVisualization.Charting; namespace SystemCenter.Model { @@ -95,15 +96,15 @@ public class DeviceHealthReport public string TSC { get; set; } public string Sector { get; set; } public string IP { get; set; } - public DateTime LastGood { get; set; } // no longer gotten from table + public DateTime? LastGood { get; set; } // no longer gotten from table public int BadDays { get; set; } public int MiMDBadDays { get; set; } - public int MICBadDays { get; set; } // no longer gotten from table + public int? MICBadDays { get; set; } // no longer gotten from table public int XDABadDays { get; set; } public string MiMDStatus { get; set; } public string MICStatus { get; set; } // no longer gotten from table public string XDAStatus { get; set; } - public DateTime LastConfigChange { get; set; } + public DateTime? LastConfigChange { get; set; } } [RoutePrefix("api/DeviceHealthReport")] @@ -250,14 +251,35 @@ public override IHttpActionResult GetSearchableList([FromBody] PostData postData DataTable systemCenterResult = GetSearchResults(systemCenterRequestBody); - // add empty rows to table for openMIC info - systemCenterResult.Columns.Add("MICStatus"); - systemCenterResult.Columns.Add("MICBadDays", Type.GetType("System.Int32")); - systemCenterResult.Columns.Add("LastGood"); + //turn into array. + DeviceHealthReport[] deviceHealthReports = systemCenterResult.AsEnumerable() + .Select(row => new DeviceHealthReport() + { + ID = row.Field("ID"), + Name = row.Field("Name"), + Model = row.Field("Model"), + TimeZone = row.Field("TimeZone"), + OpenMIC = row.Field("OpenMIC"), + LocationID = row.Field("LocationID"), + Substation = row.Field("Substation"), + LocationKey = row.Field("LocationKey"), + LastGood = null, + TSC = row.Field("TSC"), + Sector = row.Field("Sector"), + IP = row.Field("IP"), + BadDays = row.Field("BadDays"), + MiMDBadDays = row.Field("MiMDBadDays"), + MICStatus = null, + MICBadDays = null, + XDABadDays = row.Field("XDABadDays"), + MiMDStatus = row.Field("MiMDStatus"), + XDAStatus = row.Field("XDAStatus"), + // LastConfigChange = new DateTime(row.Field("LastConfigChange")) + }).ToArray(); // get a list of meters to search for in open mic - string[] openMICMeters = systemCenterResult.AsEnumerable() - .Select(row => row.Field("OpenMIC")).Where(openMIC => openMIC != "").ToArray(); + string[] openMICMeters = deviceHealthReports + .Select(device => device.OpenMIC).Where(openMIC => openMIC != "").ToArray(); SQLSearchFilter openMICMeterFilter = new() { @@ -286,25 +308,29 @@ void ConfigureRequest(HttpRequestMessage request) } catch (Exception e) { - return Ok(JsonConvert.SerializeObject(systemCenterResult)); // return just the system center stuff + return Ok(JsonConvert.SerializeObject(deviceHealthReports)); // return just the system center stuff } - - DataTable resultTable = CombineData(systemCenterResult, openMicStatistics, postData); + + DeviceHealthReport[] combinedReport = CombineData(deviceHealthReports, openMicStatistics, postData).ToArray(); foreach (SQLSearchFilter badDayFilter in badDayFilters) { - resultTable = FilterBadDays(resultTable, badDayFilter); + combinedReport = FilterBadDays(combinedReport, badDayFilter); } if (postData.OrderBy == "BadDays") { - DataView dataView = resultTable.DefaultView; - string asc = postData.Ascending ? "ASC" : "DESC"; - dataView.Sort = $"BadDays {asc}"; - resultTable = dataView.ToTable(); + if (postData.Ascending) + { + combinedReport = combinedReport.OrderBy(record => record.BadDays).ToArray(); + } + else + { + combinedReport = combinedReport.OrderByDescending(record => record.BadDays).ToArray(); + } } - - return Ok(JsonConvert.SerializeObject(resultTable)); + + return Ok(JsonConvert.SerializeObject(combinedReport)); } [HttpGet, Route("OpenMICStatus")] @@ -543,151 +569,117 @@ public static APIQuery GetAPIQuery() } } - public static DataTable FilterBadDays(DataTable table, SQLSearchFilter badDayFilter) + private static DeviceHealthReport[] FilterBadDays(DeviceHealthReport[] reports, SQLSearchFilter badDayFilter) { - DataTable filteredTable; - DataRow[] filteredRows = []; + DeviceHealthReport[] filteredReports = []; // Bad Days can be =, <> (neq), <, <=, >, >=, with a number switch (badDayFilter.Operator) { case "=": - filteredRows = table.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) == int.Parse(badDayFilter.SearchText)).ToArray(); + filteredReports = reports + .Where(record => record.BadDays == int.Parse(badDayFilter.SearchText)).ToArray(); break; case "<>": - filteredRows = table.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) != int.Parse(badDayFilter.SearchText)).ToArray(); + filteredReports = reports + .Where(record => record.BadDays != int.Parse(badDayFilter.SearchText)).ToArray(); break; case "<": - filteredRows = table.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) < int.Parse(badDayFilter.SearchText)).ToArray(); + filteredReports = reports + .Where(record => record.BadDays < int.Parse(badDayFilter.SearchText)).ToArray(); break; case "<=": - filteredRows = table.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) <= int.Parse(badDayFilter.SearchText)).ToArray(); + filteredReports = reports + .Where(record => record.BadDays <= int.Parse(badDayFilter.SearchText)).ToArray(); break; case ">": - filteredRows = table.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) > int.Parse(badDayFilter.SearchText)).ToArray(); + filteredReports = reports + .Where(record => record.BadDays > int.Parse(badDayFilter.SearchText)).ToArray(); break; case ">=": - filteredRows = table.AsEnumerable() - .Where(row => (row.Field("BadDays") ?? 0) >= int.Parse(badDayFilter.SearchText)).ToArray(); + filteredReports = reports + .Where(record => record.BadDays >= int.Parse(badDayFilter.SearchText)).ToArray(); break; default: break; } - if (filteredRows.Length == 0) - { - filteredTable = table.Clone(); - } - else - { - filteredTable = filteredRows.CopyToDataTable(); - } - return filteredTable; + return filteredReports; } - public DataTable CombineData(DataTable systemCenterData, DailyStatisticsRecord[] openMicData, PostData postData) + private List CombineData(DeviceHealthReport[] systemCenterData, DailyStatisticsRecord[] openMicData, PostData postData) { - DataTable resultTable = systemCenterData.Clone(); + //Turn into Divtionary using ID + List results = []; + HashSet processedID = new(); - bool openMICFiltered = (postData.Searches.Any(search => Equals(search.FieldName, "LastGood") || Equals(search.FieldName, "MICStatus"))); + bool openMICFiltered = (postData.Searches.Any(search => string.Equals(search.FieldName, "LastGood", StringComparison.OrdinalIgnoreCase) + || string.Equals(search.FieldName, "MICStatus", StringComparison.OrdinalIgnoreCase))); - bool openMICPrecedence = (postData.OrderBy == "LastGood" || postData.OrderBy == "MICStatus"); + bool openMICOrdered = (postData.OrderBy == "LastGood" || postData.OrderBy == "MICStatus"); - if (openMICPrecedence) // if sorted or filtered by openMICField + if (openMICOrdered) // if sorted or filtered by openMICField { foreach (DailyStatisticsRecord record in openMicData) { - string openMICAcronym = record.Meter; - - DataRow systemCenterRow = systemCenterData.AsEnumerable().FirstOrDefault(r => Equals(r.Field("OpenMIC"), record.Meter)); + DeviceHealthReport matchedReport = systemCenterData.FirstOrDefault(r => string.Equals(r.OpenMIC, record.Meter, StringComparison.OrdinalIgnoreCase)); - if (systemCenterRow is null) // could be possible due to filters + if (matchedReport is not null) { - continue; + results.Add(CombineReports(matchedReport, record)); + processedID.Add(matchedReport.ID); } - - resultTable.ImportRow(CombineRow(systemCenterRow, record)); - } if (!openMICFiltered) // if not filtered by openMIC, add the rest of systemCenter below - { - foreach (DataRow systemCenterRow in systemCenterData.Rows) - { - DataRow existingRow = resultTable.AsEnumerable() - .FirstOrDefault(resultRow => Equals(resultRow.Field("Name"), systemCenterRow["Name"])); - if (existingRow != null) - { - continue; - } - resultTable.ImportRow(CombineRow(systemCenterRow, null)); - } - } + results.AddRange(systemCenterData.Where(record => !processedID.Contains(record.ID))); + } else { - foreach (DataRow row in systemCenterData.Rows) + foreach (DeviceHealthReport record in systemCenterData) { - if (string.IsNullOrEmpty(row.ConvertField("OpenMic"))) - { - if (openMICFiltered) { continue; } + DailyStatisticsRecord? openMicRecord = null; + + if (!string.IsNullOrEmpty(record.OpenMIC)) + openMicRecord = openMicData.FirstOrDefault(r => string.Equals(r.Meter, record.OpenMIC, StringComparison.OrdinalIgnoreCase)); - resultTable.ImportRow(CombineRow(row, null)); - continue; - } - - DailyStatisticsRecord openMicRecord = openMicData.FirstOrDefault(record => Equals(record.Meter, row["OpenMic"])); - if (openMicRecord is null) { - if (openMICFiltered) { continue; } - - resultTable.ImportRow(CombineRow(row, null)); + if (!openMICFiltered) + results.Add(record); continue; } - resultTable.ImportRow(CombineRow(row, openMicRecord)); + + results.Add(CombineReports(record, openMicRecord)); } } - return resultTable; + return results; } - public DataRow CombineRow(DataRow systemCenterRow, DailyStatisticsRecord? openMICRecord) + private DeviceHealthReport CombineReports(DeviceHealthReport systemCenterRecord, DailyStatisticsRecord? openMICRecord) { if (openMICRecord is null) { - return systemCenterRow; + return systemCenterRecord; } - int totalUnsuccessfulConnections = openMICRecord.TotalUnsuccessfulConnections; - - systemCenterRow["MICStatus"] = ""; - - if (totalUnsuccessfulConnections > ErrorLevel) + switch (openMICRecord.TotalUnsuccessfulConnections) { - systemCenterRow["Status"] = "Error"; - - } - else if (totalUnsuccessfulConnections > WarningLevel) - { - systemCenterRow["Status"] = "Warning"; + case int n when n > ErrorLevel: + systemCenterRecord.MICStatus = "Error"; + break; + case int n when n > WarningLevel: + systemCenterRecord.MICStatus = "Warning"; + break; + default: + systemCenterRecord.MICStatus = ""; + break; } - systemCenterRow["MICBadDays"] = openMICRecord.BadDays; + systemCenterRecord.MICBadDays = Math.Max(openMICRecord.BadDays, systemCenterRecord.BadDays); - if (openMICRecord.BadDays > Convert.ToInt32(systemCenterRow["BadDays"])) - { - systemCenterRow["BadDays"] = openMICRecord.BadDays; - } - - if (openMICRecord.LastSuccessfulConnection != null) - { - systemCenterRow["LastGood"] = openMICRecord.LastSuccessfulConnection; - } + systemCenterRecord.LastGood = openMICRecord.LastSuccessfulConnection; - return systemCenterRow; + return systemCenterRecord; } public int WarningLevel { get; set; } = 50; From 45d5ff41ae14b867c93146f58f5373d71b5033e2 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 12 Mar 2026 15:42:36 -0400 Subject: [PATCH 16/20] correct error level --- Source/Applications/SystemCenter/Model/DeviceHealthReport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index d27d8bba1..a418d9fb1 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -683,6 +683,6 @@ private DeviceHealthReport CombineReports(DeviceHealthReport systemCenterRecord, } public int WarningLevel { get; set; } = 50; - public int ErrorLevel { get; set; } = 50; + public int ErrorLevel { get; set; } = 100; } } \ No newline at end of file From 19adc86cd6bb8d65ec14ffc6f0f2b19796db96f1 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 16 Mar 2026 10:55:53 -0400 Subject: [PATCH 17/20] add pagination to device health report --- .../SystemCenter/Model/DeviceHealthReport.cs | 20 ++++++++++++--- .../DeviceHealthReport/DeviceHealthReport.tsx | 25 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index a418d9fb1..f299819f1 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -110,6 +110,7 @@ public class DeviceHealthReport [RoutePrefix("api/DeviceHealthReport")] public class DeviceHealthReportController : ModelController { + public int PagingAmount { get; set; } = 3; public class DailyStatisticsRecord { [PrimaryKey(true)] @@ -151,8 +152,13 @@ public class AppStatus } - public override IHttpActionResult GetSearchableList([FromBody] PostData postData) + public override IHttpActionResult GetPagedList([FromBody] PostData postData, int page) { + PagedResults pagedReports = new() + { + RecordsPerPage = 50 + }; + PostData openMicRequestBody = new() { Searches = [], @@ -308,7 +314,11 @@ void ConfigureRequest(HttpRequestMessage request) } catch (Exception e) { - return Ok(JsonConvert.SerializeObject(deviceHealthReports)); // return just the system center stuff + pagedReports.TotalRecords = deviceHealthReports.Count(); + pagedReports.NumberOfPages = (pagedReports.TotalRecords + pagedReports.RecordsPerPage - 1) / pagedReports.RecordsPerPage; + DeviceHealthReport[] pagedRecords = deviceHealthReports.Skip(page * pagedReports.RecordsPerPage).Take(pagedReports.RecordsPerPage).ToArray(); + pagedReports.Data = JsonConvert.SerializeObject(pagedRecords); + return Ok(JsonConvert.SerializeObject(pagedReports)); // return just the system center stuff } DeviceHealthReport[] combinedReport = CombineData(deviceHealthReports, openMicStatistics, postData).ToArray(); @@ -330,7 +340,11 @@ void ConfigureRequest(HttpRequestMessage request) } } - return Ok(JsonConvert.SerializeObject(combinedReport)); + pagedReports.TotalRecords = combinedReport.Count(); + pagedReports.NumberOfPages = (pagedReports.TotalRecords + pagedReports.RecordsPerPage - 1) / pagedReports.RecordsPerPage; + DeviceHealthReport[] pageRecords = combinedReport.Skip(page * pagedReports.RecordsPerPage).Take(pagedReports.RecordsPerPage).ToArray(); + pagedReports.Data = JsonConvert.SerializeObject(pageRecords); + return Ok(JsonConvert.SerializeObject(pagedReports)); } [HttpGet, Route("OpenMICStatus")] diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx index af30b7cd1..ab3d0eeb6 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceHealthReport/DeviceHealthReport.tsx @@ -22,7 +22,7 @@ //****************************************************************************************************** import * as React from 'react'; -import { Table, Column } from '@gpa-gemstone/react-table'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import * as _ from 'lodash'; import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { SystemCenter as SCGlobal } from '../global'; @@ -34,6 +34,13 @@ import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import AppStatus from './AppStatus' import { ToolTip } from '@gpa-gemstone/react-forms' +interface IPagedResult { + Data: string, + NumberOfPages: number, + TotalRecords: number, + RecordsPerPage: number +} + const defaultSearchcols: Search.IField[] = [ { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, { label: 'Substation', key: 'LocationKey', type: 'string', isPivotField: false }, @@ -66,19 +73,23 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { const settings = useAppSelector(SystemCenterSettingSlice.Data); const settingStatus = useAppSelector(SystemCenterSettingSlice.Status); const [hovered, setHovered] = React.useState(null); + const [pagedData, setPagedData] = React.useState(null); + const [page, setPage] = React.useState(0); React.useEffect(() => { let handle = getMeters(); handle.done((dt: string) => { setSearchState('Idle'); - setData(JSON.parse(dt) as SCGlobal.DeviceHealthReport[]); + const pagedResults = JSON.parse(dt) as IPagedResult; + setPagedData(pagedResults); + setData(JSON.parse(pagedResults.Data) as SCGlobal.DeviceHealthReport[]) }).fail((d) => setSearchState('Error')); return function cleanup() { if (handle.abort != null) handle.abort(); } - }, [sortKey, ascending, search]); + }, [sortKey, ascending, search, page]); React.useEffect(() => { let handle = getAdditionalFields(); @@ -98,7 +109,7 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => { let searches = search.map(s => { if (defaultSearchcols.findIndex(item => item.key == s.FieldName) == -1) return { ...s, IsPivotColumn: true }; else return s; }) return $.ajax({ type: "Post", - url: `${homePath}api/DeviceHealthReport/SearchableList`, + url: `${homePath}api/DeviceHealthReport/PagedList/${page}`, contentType: "application/json; charset=utf-8", dataType: 'json', data: JSON.stringify({ Searches: searches, OrderBy: sortKey, Ascending: ascending }), @@ -477,6 +488,12 @@ const DeviceHealthReport: Application.Types.iByComponent = (props) => {
    +
    +
    + + {pagedData != null ? setPage(p - 1)} /> : null} +
    +
    ) } From f233217fe33165a728a814ebd8082c81f23253bc Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 16 Mar 2026 14:26:10 -0400 Subject: [PATCH 18/20] correct default rows per page --- Source/Applications/SystemCenter/Model/DeviceHealthReport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs index f299819f1..bbcce2b79 100644 --- a/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs +++ b/Source/Applications/SystemCenter/Model/DeviceHealthReport.cs @@ -110,7 +110,7 @@ public class DeviceHealthReport [RoutePrefix("api/DeviceHealthReport")] public class DeviceHealthReportController : ModelController { - public int PagingAmount { get; set; } = 3; + public int PagingAmount { get; set; } = 50; public class DailyStatisticsRecord { [PrimaryKey(true)] From c751c760ceb69df13f6952d2e6172a956f102e8f Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 16 Mar 2026 14:28:22 -0400 Subject: [PATCH 19/20] remove console.log --- .../TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx index 6a8d85956..4bdfe519b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DeviceIssuesPage/OpenMICIssuesPage.tsx @@ -64,7 +64,6 @@ function OpenMICIssuesPage(props: { Meter: OpenXDA.Types.Meter, OpenMICAcronym: setStatus('loading') const handle = getStatistics().done(result => { const data = JSON.parse(result as unknown as string); - console.log(data) setData(data); setStatus('idle') }).fail((d) => setStatus('error')); From 8688dd9cc41e49f377e088a41f46e5dc22d7fcd5 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Thu, 19 Mar 2026 09:33:48 -0400 Subject: [PATCH 20/20] remove openMicStatisticsOperation --- Source/Applications/SystemCenter/App.config | 1 - .../Applications/SystemCenter/AppDebug.config | 1 - .../OpenMICMeterStatisticOperation.cs | 233 ------------------ .../Applications/SystemCenter/ServiceHost.cs | 10 - .../SystemCenter/SystemCenter.csproj | 1 - 5 files changed, 246 deletions(-) delete mode 100644 Source/Applications/SystemCenter/ScheduledProcesses/OpenMICMeterStatisticOperation.cs diff --git a/Source/Applications/SystemCenter/App.config b/Source/Applications/SystemCenter/App.config index 2ab27e946..0d3b05c07 100644 --- a/Source/Applications/SystemCenter/App.config +++ b/Source/Applications/SystemCenter/App.config @@ -39,7 +39,6 @@ - diff --git a/Source/Applications/SystemCenter/AppDebug.config b/Source/Applications/SystemCenter/AppDebug.config index 6d067ac16..a631b9982 100644 --- a/Source/Applications/SystemCenter/AppDebug.config +++ b/Source/Applications/SystemCenter/AppDebug.config @@ -35,7 +35,6 @@ - diff --git a/Source/Applications/SystemCenter/ScheduledProcesses/OpenMICMeterStatisticOperation.cs b/Source/Applications/SystemCenter/ScheduledProcesses/OpenMICMeterStatisticOperation.cs deleted file mode 100644 index 5da230186..000000000 --- a/Source/Applications/SystemCenter/ScheduledProcesses/OpenMICMeterStatisticOperation.cs +++ /dev/null @@ -1,233 +0,0 @@ -//****************************************************************************************************** -// OpenMICMeterStatisticOperation.cs - Gbtc -// -// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. -// -// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See -// the NOTICE file distributed with this work for additional information regarding copyright ownership. -// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this -// file except in compliance with the License. You may obtain a copy of the License at: -// -// http://opensource.org/licenses/MIT -// -// Unless agreed to in writing, the subject software distributed under the License is distributed on an -// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the -// License for the specific language governing permissions and limitations. -// -// Code Modification History: -// ---------------------------------------------------------------------------------------------------- -// 07/09/2021 - Billy Ernest -// Generated original version of source code. -// -//****************************************************************************************************** - -using GSF.Configuration; -using GSF.Data; -using GSF.Data.Model; -using GSF.Identity; -using log4net; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using openXDA.Model; -using System; -using System.Collections.Generic; -using System.Data; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Mail; -using System.Security; -using System.Text; -using System.Threading.Tasks; -using SystemCenter.Controllers; -using SystemCenter.Model; -using SystemCenter.Model.Security; -using Setting = SystemCenter.Model.Setting; - -namespace SystemCenter -{ - public class OpenMICMeterStatisticOperation - { - - #region [ Static ] - private static readonly ILog Log = LogManager.GetLogger(typeof(OpenMICMeterStatisticOperation)); - private static bool Running { get; set; } = false; - - #endregion - - #region [ Member ] - #endregion - - #region [ Constructor ] - public OpenMICMeterStatisticOperation() { - } - - #endregion - - #region [ Properties ] - #endregion - - #region [ Methods ] - public void GetStatistics() - { - if (Running) - { - Log.Info("OpenMIC Statistic operation already running..."); - return; - } - - try - { - Log.Info("Beginning OpenMIC Statistic operation"); - - Running = true; - IEnumerable devices = GetOpenMICMeters(); - if(devices == null) - Log.Info("Null devices record recieved from OpenMIC."); - else if (!devices.Any()) - Log.Info("Empty devices record recieved from OpenMIC."); - - foreach (string device in devices) - { - try - { - Log.Info($"Querying {device} for the OpenMIC Statistic operation"); - - JObject statistic = GetOpenMICStatistic(device); - if (statistic == null) { - Log.Info($"No statistics from openMIC for {device}"); - continue; - } - - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - { - AdditionalField field = new TableOperations(connection).QueryRecordWhere("ParentTable = 'Meter' AND FieldName = 'OpenMICAcronym'"); - if (field == null) - { - Log.Info($"No Field exists for OpenMICAcronym"); - continue; - } - - AdditionalFieldValue value = new TableOperations(connection).QueryRecordWhere("AdditionalFieldID = {0} AND Value = {1}", field.ID, device); - if (value == null) - { - Log.Info($"No additional value exists for {device}"); - continue; - } - - Meter meter = new TableOperations(connection).QueryRecordWhere("ID = {0}", value.ParentTableID); - if (meter == null) - { - Log.Info($"No meter exists for {device}"); - continue; - } - - int warningLevel = int.Parse(new TableOperations(connection).QueryRecordWhere("Name = 'OpenMIC.WarningLevel'")?.Value ?? "50"); - int errorLevel = int.Parse(new TableOperations(connection).QueryRecordWhere("Name = 'OpenMIC.ErrorLevel'")?.Value ?? "100"); - - openXDA.Model.Setting defaultTimeZone = new TableOperations(connection).QueryRecordWhere("Name = 'System.DefaultMeterTimeZone'"); - if (defaultTimeZone == null) - { - Log.Info($"No setting exists for default time zone, using UTC"); - defaultTimeZone = new openXDA.Model.Setting() { Value = "UTC" }; - } - - if (meter.TimeZone == null) meter.TimeZone = defaultTimeZone.Value; - - string date = TimeZoneInfo.ConvertTimeFromUtc(statistic["EndTime"]?.ToObject() ?? DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(meter.TimeZone)).ToString("MM/dd/yyyy"); - if (date == "01/01/0001") { - date = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(meter.TimeZone)).ToString("MM/dd/yyyy"); - } - - OpenMICDailyStatistic stat = new TableOperations(connection).QueryRecordWhere("Meter = {0} AND Date = {1}", meter.AssetKey, date); - - if (stat == null) - { - stat = new OpenMICDailyStatistic(); - stat.Meter = meter.AssetKey; - stat.Date = date; - stat.BadDays = new TableOperations(connection).QueryRecords("[DATE] DESC", new RecordRestriction("Meter = {0}", meter.AssetKey)).FirstOrDefault()?.BadDays ?? 0; - } - - DateTime? lastSuccess = statistic["LastSuccessfulConnection"].Value(); - if (lastSuccess != null) - lastSuccess = TimeZoneInfo.ConvertTimeFromUtc((DateTime)lastSuccess, TimeZoneInfo.FindSystemTimeZoneById(meter.TimeZone)); - - stat.LastSuccessfulConnection = lastSuccess; - - DateTime? lastUnsuccess = statistic["LastUnsuccessfulConnection"].Value(); - if (lastUnsuccess != null) - lastUnsuccess = TimeZoneInfo.ConvertTimeFromUtc((DateTime)lastUnsuccess, TimeZoneInfo.FindSystemTimeZoneById(meter.TimeZone)); - - stat.LastUnsuccessfulConnection = lastUnsuccess; - stat.LastUnsuccessfulConnectionExplanation = statistic["LastUnsuccessfulConnectionExplanation"]?.ToString(); - stat.TotalSuccessfulConnections = statistic["TotalSuccessfulConnections"]?.ToObject() ?? 0; - stat.TotalUnsuccessfulConnections = statistic["TotalUnsuccessfulConnections"]?.ToObject() ?? 0; - stat.TotalConnections = stat.TotalSuccessfulConnections + stat.TotalUnsuccessfulConnections; - - if (stat.Status == "Error") { } // do nothing if alreaedy an error for the day - else if (stat.TotalUnsuccessfulConnections > errorLevel) - { - stat.Status = "Error"; - stat.BadDays++; - - } - else if (stat.Status == "Warning") { } // do nothing else if already a warning for the day - else if (stat.TotalUnsuccessfulConnections > warningLevel) - { - stat.Status = "Warning"; - } - - Log.Info($"Updating statistic record for {device} - Date: {stat.Date} / ID: {stat.ID} / Last Successful Connection: {stat.LastSuccessfulConnection} / Daily Connections: {stat.TotalConnections}"); - new TableOperations(connection).AddNewOrUpdateRecord(stat); - Log.Info($"Loaded record for {device} with no exceptions"); - } - - } - catch (Exception ex) - { - Log.Error(ex.Message + "\n" + ex.StackTrace, ex); - } - } - - } - catch (Exception ex) { - Log.Error(ex.Message + "\n" + ex.StackTrace, ex); - - } - finally - { - Running = false; - - } - } - - private IEnumerable GetOpenMICMeters() { - using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) - { - DataTable table = connection.RetrieveData(@" - SELECT AdditionalFieldValue.Value - FROM - AdditionalFieldValue JOIN - AdditionalField ON AdditionalFieldValue.AdditionalFieldID = AdditionalField.ID - WHERE - AdditionalField.ParentTable = 'Meter' AND - AdditionalField.FieldName = 'OpenMICAcronym' - - "); - - return table.Select().Select(x => x["Value"].ToString()); - } - //return ControllerHelpers.Get>("OpenMIC", "api/Operations/Meters"); - } - - private JObject GetOpenMICStatistic(string meter) - { - return JObject.Parse(ControllerHelpers.Get("OpenMIC", $"api/Operations/Statistics/{meter}")); - } - - #endregion - } -} \ No newline at end of file diff --git a/Source/Applications/SystemCenter/ServiceHost.cs b/Source/Applications/SystemCenter/ServiceHost.cs index 18e7280f3..eaa561da2 100644 --- a/Source/Applications/SystemCenter/ServiceHost.cs +++ b/Source/Applications/SystemCenter/ServiceHost.cs @@ -158,8 +158,6 @@ public ServiceHost() systemSettings.Add("CompanyAcronym", "GPA", "The acronym representing the company who owns this instance of the SystemCenter."); systemSettings.Add("UserAccountMetaDataUpdater", (systemSettings["CompanyAcronym"].Value == "TVA" ? "0 0 * * *" : "never"), "Default frequency to run the user account meta-data updater"); - systemSettings.Add("OpenMICStatisticOperation", (systemSettings["CompanyAcronym"].Value == "TVA" ? "* * * * *" : "never"), "Default frequency to run openmic statistics operation"); - // Attempt to set default culture string defaultCulture = systemSettings["DefaultCulture"].ValueAs("en-US"); CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(defaultCulture); // Defaults for date formatting, etc. @@ -263,8 +261,6 @@ private void ServiceHelper_ServiceStarted(object sender, EventArgs e) if (systemSettings["UserAccountMetaDataUpdater"].Value != "never") m_serviceHelper.AddScheduledProcess(UserAccountMetaDataUpdaterHandler, "UserAccountMetaDataUpdater", systemSettings["UserAccountMetaDataUpdater"].Value); - if (systemSettings["OpenMICStatisticOperation"].Value != "never") - m_serviceHelper.AddScheduledProcess(OpenMICStatisticOperationHandler, "OpenMICStatisticOperation", systemSettings["OpenMICStatisticOperation"].Value); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("ReloadSystemSettings", "Reloads system settings from the database", ReloadSystemSettingsRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("EngineStatus", "Displays status information about the XDA engine", EngineStatusHandler)); @@ -625,12 +621,6 @@ private void UserAccountMetaDataUpdaterHandler(string s, object[] args) { metaDataUpdater.Update(); } - private void OpenMICStatisticOperationHandler(string s, object[] args) - { - OpenMICMeterStatisticOperation operation = new OpenMICMeterStatisticOperation(); - operation.GetStatistics(); - } - // Reloads system settings from the database. private void ReloadSystemSettingsRequestHandler(ClientRequestInfo requestInfo) { diff --git a/Source/Applications/SystemCenter/SystemCenter.csproj b/Source/Applications/SystemCenter/SystemCenter.csproj index 1b43f3537..23bbdfc0c 100644 --- a/Source/Applications/SystemCenter/SystemCenter.csproj +++ b/Source/Applications/SystemCenter/SystemCenter.csproj @@ -302,7 +302,6 @@ -