diff --git a/OrcanodeMonitor/Core/Fetcher.cs b/OrcanodeMonitor/Core/Fetcher.cs index 1477511..0eec9ed 100644 --- a/OrcanodeMonitor/Core/Fetcher.cs +++ b/OrcanodeMonitor/Core/Fetcher.cs @@ -1042,6 +1042,18 @@ public static List GetEvents(OrcanodeMonitorContext context, int return orcanodeEvents; } + public static List GetRecentEventsForNode(OrcanodeMonitorContext context, string id, DateTime since) + { + List events = context.OrcanodeEvents.Where(e => e.OrcanodeId == id).OrderByDescending(e => e.DateTimeUtc).ToList(); + List orcanodeEvents = events.Where(e => e.DateTimeUtc >= since).ToList(); + OrcanodeEvent? olderEvent = events.Where(e => (e.DateTimeUtc < since)).FirstOrDefault(); + if (olderEvent != null) + { + return orcanodeEvents.Append(olderEvent).ToList(); + } + return orcanodeEvents; + } + private static void AddOrcanodeEvent(OrcanodeMonitorContext context, Orcanode node, string type, string value) { var orcanodeEvent = new OrcanodeEvent(node, type, value, DateTime.UtcNow); diff --git a/OrcanodeMonitor/Models/Orcanode.cs b/OrcanodeMonitor/Models/Orcanode.cs index 63955ce..f7b31ec 100644 --- a/OrcanodeMonitor/Models/Orcanode.cs +++ b/OrcanodeMonitor/Models/Orcanode.cs @@ -392,6 +392,82 @@ public string OrcasoundOnlineStatusString { public OrcanodeIftttDTO ToIftttDTO() => new OrcanodeIftttDTO(ID, DisplayName); + /// + /// Calculates the uptime percentage for a node based on its events since a specified date. + /// + /// The ID of the node to calculate uptime for + /// List of node events + /// The start date for uptime calculation + /// Uptime percentage as an integer between 0 and 100 + /// Thrown when orcanodeId is null or empty + public static int GetUptimePercentage(string orcanodeId, List events, DateTime since) + { + if (string.IsNullOrEmpty(orcanodeId)) + { + throw new ArgumentException("Node ID cannot be null or empty", nameof(orcanodeId)); + } + if (since > DateTime.UtcNow) + { + throw new ArgumentException("Start date cannot be in the future", nameof(since)); + } + if (events == null) + { + return 0; + } + + TimeSpan up = TimeSpan.Zero; + TimeSpan down = TimeSpan.Zero; + DateTime start = since; + string lastValue = string.Empty; + + // Get events sorted by date to ensure correct chronological processing. + var nodeEvents = events + .Where(e => e.OrcanodeId == orcanodeId) + .OrderBy(e => e.DateTimeUtc) + .ToList(); + + // Compute uptime percentage by looking at OrcanodeEvents over the past week. + foreach (OrcanodeEvent e in nodeEvents) + { + if (e.DateTimeUtc <= since) + { + // Event is too old. + lastValue = e.Value; + continue; + } + DateTime current = e.DateTimeUtc; + if (lastValue == OnlineString) + { + up += (current - start); + } + else + { + down += (current - start); + } + start = current; + lastValue = e.Value; + } + + // Account for the reminder of the time until now. + DateTime now = DateTime.UtcNow; + if (lastValue == OnlineString) + { + up += now - start; + } + else + { + down += now - start; + } + + TimeSpan totalTime = up + down; + if (totalTime == TimeSpan.Zero) + { + return 0; + } + int percentage = (int)((100.0 * up) / totalTime + 0.5); + return percentage; + } + /// /// Derive a human-readable display name from a Dataplicity node name. /// diff --git a/OrcanodeMonitor/Models/OrcanodeEvent.cs b/OrcanodeMonitor/Models/OrcanodeEvent.cs index 49a40c7..ebbca2b 100644 --- a/OrcanodeMonitor/Models/OrcanodeEvent.cs +++ b/OrcanodeMonitor/Models/OrcanodeEvent.cs @@ -63,6 +63,13 @@ public class OrcanodeEvent { public OrcanodeEvent() { + Slug = string.Empty; + Type = string.Empty; + Value = string.Empty; + OrcanodeId = string.Empty; + ID = string.Empty; + DateTimeUtc = DateTime.UtcNow; + Year = DateTimeUtc.Year; } public OrcanodeEvent(Orcanode node, string type, string value, DateTime timestamp) @@ -97,7 +104,7 @@ public OrcanodeEvent(Orcanode node, string type, string value, DateTime timestam public string OrcanodeId { get; set; } // Navigation property that uses OrcanodeId. - public virtual Orcanode Orcanode { get; set; } + public virtual Orcanode? Orcanode { get; set; } public DateTime DateTimeUtc { get; set; } @@ -141,7 +148,7 @@ public int DerivedSeverity public override string ToString() { - return string.Format("{0} {1} => {2} at {3}", Slug, Type, Value, Fetcher.UtcToLocalDateTime(DateTimeUtc)); + return string.Format("{0} {1} => {2} at {3}", NodeName, Type, Value, Fetcher.UtcToLocalDateTime(DateTimeUtc)); } #endregion methods diff --git a/OrcanodeMonitor/Pages/DataplicityNode.cshtml.cs b/OrcanodeMonitor/Pages/DataplicityNode.cshtml.cs index 0be86b3..8767f8b 100644 --- a/OrcanodeMonitor/Pages/DataplicityNode.cshtml.cs +++ b/OrcanodeMonitor/Pages/DataplicityNode.cshtml.cs @@ -22,6 +22,8 @@ public DataplicityNodeModel(OrcanodeMonitorContext context, ILogger { _databaseContext = context; _logger = logger; + _serial = string.Empty; + _jsonData = string.Empty; } public string LastChecked diff --git a/OrcanodeMonitor/Pages/Index.cshtml b/OrcanodeMonitor/Pages/Index.cshtml index de60fc3..3b559a6 100644 --- a/OrcanodeMonitor/Pages/Index.cshtml +++ b/OrcanodeMonitor/Pages/Index.cshtml @@ -71,8 +71,11 @@ } - - @Model.GetUptimePercentage(item)% + + + @Model.GetUptimePercentage(item)% + @if (item.OrcasoundStatus == Models.OrcanodeOnlineStatus.Absent) { @@ -175,20 +178,20 @@

Recent State Events

- - - - - @foreach (Models.OrcanodeEvent item in Model.RecentEvents) - { + - - + + - } + + + @foreach (Models.OrcanodeEvent item in Model.RecentEvents) + { + + + + + } +
TimestampEvent
- @item.DateTimeLocal - - @item.Description - TimestampEvent
@item.DateTimeLocal.ToString("g")@item.Description
diff --git a/OrcanodeMonitor/Pages/Index.cshtml.cs b/OrcanodeMonitor/Pages/Index.cshtml.cs index 08f7f34..d895f4a 100644 --- a/OrcanodeMonitor/Pages/Index.cshtml.cs +++ b/OrcanodeMonitor/Pages/Index.cshtml.cs @@ -19,12 +19,13 @@ public class IndexModel : PageModel public List Nodes => _nodes; private const int _maxEventCountToDisplay = 20; public List RecentEvents => Fetcher.GetEvents(_databaseContext, _maxEventCountToDisplay); - private TimeSpan _uptimeEvaluationPeriod = TimeSpan.FromDays(7); // 1 week. public IndexModel(OrcanodeMonitorContext context, ILogger logger) { _databaseContext = context; _logger = logger; + _events = new List(); + _nodes = new List(); } public string LastChecked { @@ -94,60 +95,9 @@ private string GetTextColor(OrcanodeOnlineStatus status) public string NodeOrcasoundTextColor(Orcanode node) => GetTextColor(node.OrcasoundStatus); - public int GetUptimePercentage(Orcanode node) - { - if (_events == null) - { - return 0; - } - - TimeSpan up = TimeSpan.Zero; - TimeSpan down = TimeSpan.Zero; - DateTime start = DateTime.UtcNow - _uptimeEvaluationPeriod; - string lastValue = string.Empty; - - // Get events sorted by date to ensure correct chronological processing - var nodeEvents = _events - .Where(e => e.Orcanode == node) - .OrderBy(e => e.DateTimeUtc) - .ToList(); + private DateTime SinceTime => DateTime.UtcNow.AddDays(-7); - // Compute uptime percentage by looking at OrcanodeEvents over the past week. - foreach (OrcanodeEvent e in nodeEvents) - { - if (DateTime.UtcNow - e.DateTimeUtc >= _uptimeEvaluationPeriod) { - // More than a week old. - lastValue = e.Value; - continue; - } - DateTime current = e.DateTimeUtc; - if (lastValue == Orcanode.OnlineString) - { - up += (current - start); - } - else - { - down += (current - start); - } - start = current; - lastValue = e.Value; - } - if (lastValue == Orcanode.OnlineString) - { - up += DateTime.UtcNow - start; - } else - { - down += DateTime.UtcNow - start; - } - - TimeSpan totalTime = up + down; - if (totalTime == TimeSpan.Zero) - { - return 0; - } - int percentage = (int)((100.0 * up) / totalTime + 0.5); - return percentage; - } + public int GetUptimePercentage(Orcanode node) => Orcanode.GetUptimePercentage(node.ID, _events, SinceTime); public string NodeUptimePercentageBackgroundColor(Orcanode node) { diff --git a/OrcanodeMonitor/Pages/NodeEvents.cshtml b/OrcanodeMonitor/Pages/NodeEvents.cshtml new file mode 100644 index 0000000..480a72d --- /dev/null +++ b/OrcanodeMonitor/Pages/NodeEvents.cshtml @@ -0,0 +1,51 @@ +@page "{id}" +@model OrcanodeMonitor.Pages.NodeEventsModel +@{ + ViewData["Title"] = "Node Events"; +} + +
+

Node Events

+
+ @Html.AntiForgeryToken() + +
+ + +
+
+

+ Uptime percentage: @Model.UptimePercentage% +

+ + + + + + + + + @if (!Model.RecentEvents.Any()) + { + + + + } + @foreach (Models.OrcanodeEvent item in Model.RecentEvents) + { + + + + + } + +
TimestampEvent
No events found for this time period.
@item.DateTimeLocal.ToString("g")@item.Description
+
diff --git a/OrcanodeMonitor/Pages/NodeEvents.cshtml.cs b/OrcanodeMonitor/Pages/NodeEvents.cshtml.cs new file mode 100644 index 0000000..98637d6 --- /dev/null +++ b/OrcanodeMonitor/Pages/NodeEvents.cshtml.cs @@ -0,0 +1,64 @@ +// Copyright (c) Orcanode Monitor contributors +// SPDX-License-Identifier: MIT +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using OrcanodeMonitor.Core; +using OrcanodeMonitor.Data; +using OrcanodeMonitor.Models; + +namespace OrcanodeMonitor.Pages +{ + public class NodeEventsModel : PageModel + { + private readonly OrcanodeMonitorContext _databaseContext; + private readonly ILogger _logger; + private string _nodeId; + public string Id => _nodeId; + [BindProperty] + public string Selected { get; set; } = "week"; // Default to 'week' + private DateTime SinceTime => (Selected == "week") ? DateTime.UtcNow.AddDays(-7) : DateTime.UtcNow.AddMonths(-1); + private List _events; + public List RecentEvents => _events; + public int UptimePercentage => Orcanode.GetUptimePercentage(_nodeId, _events, SinceTime); + + public NodeEventsModel(OrcanodeMonitorContext context, ILogger logger) + { + _databaseContext = context; + _logger = logger; + _nodeId = string.Empty; + _events = new List(); + } + + private void FetchEvents() + { + try + { + _events = Fetcher.GetRecentEventsForNode(_databaseContext, _nodeId, SinceTime); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to fetch events for node {NodeId}", _nodeId); + _events = new List(); + } + } + + public void OnGet(string id) + { + _nodeId = id; + FetchEvents(); + } + + public IActionResult OnPost(string selected, string id) + { + if (selected != "week" && selected != "month") + { + _logger.LogWarning("Invalid time range selected: {selected}", selected); + return BadRequest("Invalid time range"); + } + Selected = selected; + _nodeId = id; + FetchEvents(); + return Page(); + } + } +} diff --git a/OrcanodeMonitor/Pages/NodeEvents.cshtml.css b/OrcanodeMonitor/Pages/NodeEvents.cshtml.css new file mode 100644 index 0000000..c54bb1f --- /dev/null +++ b/OrcanodeMonitor/Pages/NodeEvents.cshtml.css @@ -0,0 +1,20 @@ +table, th, td { + border: 1px solid black; + border-collapse: collapse; +} + +th, td { + padding: 5px; +} + +h1 { + font-size: 2rem; +} + +.selected { + background-color: blue; + color: white; +} +.unselected { + border-color: blue; +} \ No newline at end of file