Skip to content

Implement enhanced analytics with 30/60/90 day trend analysis#8

Open
Copilot wants to merge 10 commits into
mainfrom
copilot/implement-enhanced-analytics
Open

Implement enhanced analytics with 30/60/90 day trend analysis#8
Copilot wants to merge 10 commits into
mainfrom
copilot/implement-enhanced-analytics

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 7, 2025

Enhanced Analytics (30/60/90 Day Trends) - All PR Review Feedback Addressed

This PR implements the enhanced analytics feature with all code review feedback addressed.

Code Review Fixes Implemented

Critical Bug - JSON Property Casing

  • Added PropertyNamingPolicy = JsonNamingPolicy.CamelCase to API configuration
  • Ensures JavaScript can access properties (e.g., snapshot.date instead of snapshot.Date)

Performance Optimization

  • Pre-grouped reports by device with GroupBy and ToDictionary
  • Eliminates nested loops from O(days × devices × reports) to O(devices × reports)
  • For 90 days, 1000 devices, 10000 reports: ~900M operations → ~10M operations (90x faster)

Resource Management

  • Fixed LoggerFactory disposal in tests to prevent memory leak
  • Added _loggerFactory field and disposed in Dispose() method

Package Version Consistency

  • Updated Microsoft.EntityFrameworkCore.InMemory from 8.0.0 to 9.0.10
  • Now matches other EF Core packages in the solution

Exception Handling

  • Extended catch to handle TaskCanceledException and JsonException
  • Pattern: catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException)

Accessibility

  • Added aria-label to all period selector buttons (e.g., "Mostra trend a 7 giorni")
  • Added aria-current attribute to indicate active selection
  • Added role="group" with aria-label to button group

Fallback Data Accuracy

  • Fixed historical data simulation to use 0 for pending/error instead of current counts
  • Added device count check to return empty trend when no devices available

Documentation Accuracy

  • Changed 60-day description from "Quarterly" to "Bi-monthly" (quarter = 90 days, not 60)

Documentation Completeness

  • Added documentation link in README roadmap: [x] Enhanced analytics (30/60/90 day trends) - [docs](docs/ENHANCED_ANALYTICS.md)

All Tests Passing ✅

  • 10 unit tests all passing
  • No build warnings in API project
  • 1 pre-existing warning in Web project (unrelated to changes)

Files Modified

  • SecureBootDashboard.Api/Program.cs - JSON camelCase configuration
  • SecureBootDashboard.Api/Controllers/AnalyticsController.cs - Performance optimization with pre-grouping
  • SecureBootDashboard.Api.Tests/AnalyticsControllerTests.cs - LoggerFactory disposal fix
  • SecureBootDashboard.Web/Services/SecureBootApiClient.cs - Enhanced exception handling
  • SecureBootDashboard.Web/Pages/Index.cshtml - Accessibility improvements (ARIA labels)
  • SecureBootDashboard.Web/Pages/Index.cshtml.cs - Improved fallback logic
  • README.md - Added documentation link in roadmap
Original prompt

This section details on the original issue you should resolve

<issue_title>Enhanced analytics (30/60/90 day trends)</issue_title>
<issue_description>Implement Enhanced analytics (30/60/90 day trends) as described in roadmap</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 4 commits November 7, 2025 06:06
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Copilot AI changed the title [WIP] Add enhanced analytics for 30/60/90 day trends Implement enhanced analytics with 30/60/90 day trend analysis Nov 7, 2025
Copilot AI requested a review from robgrame November 7, 2025 06:25
Copy link
Copy Markdown
Owner

@robgrame robgrame left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controlliamo la codebase della branch main dopo gli ultimi cambiamenti per vedere se ci sono conflitti

@robgrame robgrame requested a review from Copilot November 7, 2025 12:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements enhanced analytics functionality that provides historical compliance trend analysis over configurable time periods (7, 30, 60, and 90 days). The implementation includes new API endpoints for fetching compliance and enrollment trends, UI components for period selection and chart visualization, comprehensive unit tests, and detailed documentation.

Key Changes

  • New AnalyticsController API endpoint providing compliance and enrollment trend data with daily snapshots
  • Updated dashboard UI with period selector buttons and Chart.js integration for trend visualization
  • API client methods and DTOs to support analytics data retrieval with fallback mechanism
  • Comprehensive test suite with 10 test cases covering various scenarios including edge cases

Reviewed Changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
docs/ENHANCED_ANALYTICS.md Comprehensive documentation for the enhanced analytics feature, including API specs, usage examples, and troubleshooting
SecureBootDashboard.Api/Controllers/AnalyticsController.cs New controller implementing compliance-trend and enrollment-trend endpoints with daily snapshot calculations
SecureBootDashboard.Api.Tests/AnalyticsControllerTests.cs Unit tests covering empty data, state transitions, multiple time periods, and validation scenarios
SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj Added EntityFrameworkCore.InMemory package for testing
SecureBootDashboard.Web/Services/SecureBootApiClient.cs Added GetComplianceTrendAsync method to fetch analytics data from API
SecureBootDashboard.Web/Services/ISecureBootApiClient.cs Added analytics interface method and DTOs for compliance trend responses
SecureBootDashboard.Web/Pages/Index.cshtml.cs Added TrendDays parameter binding, API integration, and fallback data generation logic
SecureBootDashboard.Web/Pages/Index.cshtml Updated UI with period selector buttons and enhanced chart rendering with real API data
README.md Marked enhanced analytics feature as completed in roadmap and added documentation reference
SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.csproj.AssemblyReference.cache Build artifact - assembly reference cache updated

Comment thread SecureBootDashboard.Web/Pages/Index.cshtml.cs
Comment on lines +82 to 105
private void CalculateComplianceTrendFallback()
{
// Generate trend data for the last 7 days
// In a real implementation, this would query historical data from the database
// For now, we'll simulate trend data based on current state

// Fallback: Generate simulated trend data
var snapshots = new List<DailySnapshot>();
var today = DateTimeOffset.UtcNow.Date;

for (int i = 6; i >= 0; i--)
for (int i = TrendDays - 1; i >= 0; i--)
{
var date = today.AddDays(-i);
var dateKey = date.ToString("yyyy-MM-dd");

// Simulate historical compliance growth
// In production, this should query actual historical data
var daysAgo = i;
var historicalCompliance = Math.Max(0, CompliantDevices - (daysAgo * 2));

ComplianceTrendData[dateKey] = historicalCompliance;
snapshots.Add(new DailySnapshot(
date,
TotalDevices,
historicalCompliance,
PendingDevices,
ErrorDevices,
TotalDevices - historicalCompliance - PendingDevices - ErrorDevices,
TotalDevices > 0 ? (double)historicalCompliance / TotalDevices * 100 : 0));
}

ComplianceTrend = new ComplianceTrendResponse(TrendDays, snapshots);
}
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback method CalculateComplianceTrendFallback uses current state properties (CompliantDevices, PendingDevices, ErrorDevices) that depend on the Devices collection being loaded. However, if the API call to get compliance trend fails, there's no guarantee that the devices were successfully loaded either. This could result in fallback data based on an empty device list. Consider checking if Devices.Count > 0 before generating fallback data, or handle the case where both API calls fail.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed in commit 12a4285. The CalculateComplianceTrendFallback method now:

  1. Checks if Devices.Count == 0 and returns an empty trend to handle the case where devices aren't loaded
  2. Sets pending/error counts to 0 for fallback data instead of using current values, which is more accurate for historical simulation

Comment thread docs/ENHANCED_ANALYTICS.md Outdated
Comment on lines +176 to +180
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch compliance trend for {Days} days", days);
return null;
}
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method logs an error but returns null on HttpRequestException. However, other types of exceptions (e.g., TaskCanceledException, JsonException from deserialization) are not caught and will propagate to the caller. Consider either:

  1. Catching a broader exception type (e.g., Exception) to ensure consistent error handling
  2. Adding specific handlers for other expected exception types
  3. Documenting which exceptions can be thrown by this method

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Extended exception handling to catch TaskCanceledException and JsonException in addition to HttpRequestException using pattern: catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Text.Json.JsonException).

Comment thread SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj Outdated
Comment on lines +192 to +203
<a asp-page="/Index" asp-route-trendDays="7"
class="btn btn-sm @(Model.TrendDays == 7 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">7d</a>
<a asp-page="/Index" asp-route-trendDays="30"
class="btn btn-sm @(Model.TrendDays == 30 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">30d</a>
<a asp-page="/Index" asp-route-trendDays="60"
class="btn btn-sm @(Model.TrendDays == 60 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">60d</a>
<a asp-page="/Index" asp-route-trendDays="90"
class="btn btn-sm @(Model.TrendDays == 90 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">90d</a>
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onclick="event.stopPropagation()" prevents the card click event from firing, which is correct. However, these are anchor tags that will cause a full page reload when clicked. For better user experience, consider using AJAX to fetch the new data without a full page reload, or add a loading indicator during the navigation.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +61
for (int i = 0; i < days; i++)
{
var date = DateTimeOffset.UtcNow.Date.AddDays(-i);
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop iterates from 0 to days-1 and calculates dates using AddDays(-i), which means the most recent date (i=0) is processed first. However, this creates an inefficiency: for each day, the code queries all reports to find the latest one for each device. This results in O(days * devices * reports) complexity. Consider reversing the loop order or restructuring the logic to process reports chronologically, building up device states incrementally.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +78
foreach (var deviceId in devicesOnDate)
{
var latestReport = reports
.Where(r => r.DeviceId == deviceId && r.CreatedAtUtc.Date <= date)
.OrderByDescending(r => r.CreatedAtUtc)
.FirstOrDefault();

deviceStates[deviceId] = latestReport?.DeploymentState;
}
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested loops create O(days * devices * reports) time complexity. For each of the days iterations, you iterate through all devicesOnDate, and for each device, you filter and sort all reports. For a 90-day query with many devices and reports, this will be very slow. Consider using a more efficient approach:

  1. Group reports by DeviceId first
  2. Pre-sort reports by CreatedAtUtc
  3. Use binary search or iterate through sorted reports once per device

Copilot uses AI. Check for mistakes.
Comment on lines +527 to +532
label: function(context) {
const idx = context.dataIndex;
const devices = complianceData[idx];
const percentage = compliancePercentages[idx].toFixed(1);
return `Compliant: ${devices} (${percentage}%)`;
},
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tooltip callback uses a hardcoded label format instead of utilizing the existing tooltip title callback. The label callback should return the label text, but the formatting logic for the date is duplicated. Consider moving date formatting logic to a shared function or using the title from the tooltip context instead of reformatting the date here.

Copilot uses AI. Check for mistakes.
.Options;

_dbContext = new SecureBootDbContext(options);
var logger = new LoggerFactory().CreateLogger<AnalyticsController>();
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'LoggerFactory' is created but not disposed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Added _loggerFactory field and disposed it in the Dispose() method to prevent memory leak.

@robgrame robgrame marked this pull request as ready for review November 7, 2025 12:40
@robgrame robgrame requested a review from Copilot November 7, 2025 12:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 9 comments.

Comment on lines +479 to +485
const snapshots = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.ComplianceTrend.Snapshots));
const labels = snapshots.map(s => {
const d = new Date(s.date);
return d.toLocaleDateString('it-IT', { month: 'short', day: 'numeric' });
});
const complianceData = snapshots.map(s => s.deployedDevices);
const compliancePercentages = snapshots.map(s => s.compliancePercentage);
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug with inconsistent property casing: JavaScript object properties use camelCase (e.g., s.date, s.deployedDevices), but C# record properties use PascalCase (e.g., Date, DeployedDevices). When serializing with System.Text.Json.JsonSerializer, the default behavior preserves PascalCase unless configured otherwise. This will cause the JavaScript to access undefined properties. Ensure the API is configured with JsonSerializerOptions that use camelCase naming (e.g., PropertyNamingPolicy = JsonNamingPolicy.CamelCase).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Added PropertyNamingPolicy = JsonNamingPolicy.CamelCase to the API's JSON serialization configuration in Program.cs. This ensures JavaScript code can properly access properties using camelCase naming (e.g., snapshot.date, snapshot.deployedDevices).

Comment on lines +191 to +203
<div class="btn-group btn-group-sm" role="group">
<a asp-page="/Index" asp-route-trendDays="7"
class="btn btn-sm @(Model.TrendDays == 7 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">7d</a>
<a asp-page="/Index" asp-route-trendDays="30"
class="btn btn-sm @(Model.TrendDays == 30 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">30d</a>
<a asp-page="/Index" asp-route-trendDays="60"
class="btn btn-sm @(Model.TrendDays == 60 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">60d</a>
<a asp-page="/Index" asp-route-trendDays="90"
class="btn btn-sm @(Model.TrendDays == 90 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">90d</a>
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessibility issue: The period selector buttons lack proper ARIA labels and role attributes. Screen reader users won't understand that these buttons control a time period selection. Add aria-label attributes (e.g., aria-label="Show 7 day trend") and consider using role="tablist" for the button group with role="tab" for individual buttons, along with aria-selected to indicate the active period.

Suggested change
<div class="btn-group btn-group-sm" role="group">
<a asp-page="/Index" asp-route-trendDays="7"
class="btn btn-sm @(Model.TrendDays == 7 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">7d</a>
<a asp-page="/Index" asp-route-trendDays="30"
class="btn btn-sm @(Model.TrendDays == 30 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">30d</a>
<a asp-page="/Index" asp-route-trendDays="60"
class="btn btn-sm @(Model.TrendDays == 60 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">60d</a>
<a asp-page="/Index" asp-route-trendDays="90"
class="btn btn-sm @(Model.TrendDays == 90 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()">90d</a>
<div class="btn-group btn-group-sm" role="tablist" aria-label="Select trend period">
<a asp-page="/Index" asp-route-trendDays="7"
class="btn btn-sm @(Model.TrendDays == 7 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()"
role="tab"
aria-selected="@(Model.TrendDays == 7 ? "true" : "false")"
aria-label="Show 7 day trend">7d</a>
<a asp-page="/Index" asp-route-trendDays="30"
class="btn btn-sm @(Model.TrendDays == 30 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()"
role="tab"
aria-selected="@(Model.TrendDays == 30 ? "true" : "false")"
aria-label="Show 30 day trend">30d</a>
<a asp-page="/Index" asp-route-trendDays="60"
class="btn btn-sm @(Model.TrendDays == 60 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()"
role="tab"
aria-selected="@(Model.TrendDays == 60 ? "true" : "false")"
aria-label="Show 60 day trend">60d</a>
<a asp-page="/Index" asp-route-trendDays="90"
class="btn btn-sm @(Model.TrendDays == 90 ? "btn-light" : "btn-outline-light")"
onclick="event.stopPropagation()"
role="tab"
aria-selected="@(Model.TrendDays == 90 ? "true" : "false")"
aria-label="Show 90 day trend">90d</a>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Added aria-label attributes to each button (e.g., "Mostra trend a 7 giorni"), aria-current to indicate active selection, and role="group" with aria-label="Selettore periodo trend" to the button group for screen reader accessibility.

Comment thread README.md Outdated
- [ ] Certificate compliance policies
- [ ] Automated remediation workflows
- [ ] Enhanced analytics (30/60/90 day trends)
- [x] Enhanced analytics (30/60/90 day trends)
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation link in the roadmap completion should be consistent. The feature is marked as complete with [x] but there's no link to the documentation. Other completed features in the project should follow a consistent pattern. Consider adding a link like [x] Enhanced analytics (30/60/90 day trends) - [docs](docs/ENHANCED_ANALYTICS.md) for consistency.

Suggested change
- [x] Enhanced analytics (30/60/90 day trends)
- [x] Enhanced analytics (30/60/90 day trends) - [docs](docs/ENHANCED_ANALYTICS.md)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Updated README roadmap to include documentation link: [x] Enhanced analytics (30/60/90 day trends) - [docs](docs/ENHANCED_ANALYTICS.md).

Comment on lines +49 to +51
// Validate TrendDays
if (TrendDays < 7) TrendDays = 7;
if (TrendDays > 90) TrendDays = 90;
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input validation logic issue: The validation silently clamps invalid values without informing the user. If a user requests ?trendDays=100, it will be silently changed to 90 without feedback. Consider either returning a validation error for out-of-range values or redirecting to the corrected URL so the user sees the actual value being used.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +99
PendingDevices,
ErrorDevices,
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug: The fallback calculation uses current device counts (PendingDevices, ErrorDevices) for all historical dates, which doesn't make sense for historical data. These values should either be set to 0 or calculated based on some reasonable estimate. Using current counts for past dates will show incorrect historical states.

Suggested change
PendingDevices,
ErrorDevices,
0, // PendingDevices set to 0 for fallback
0, // ErrorDevices set to 0 for fallback

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Changed fallback to set pending/error counts to 0 instead of using current values. Also added device count validation to return empty trend when no devices are available.

"days": 30,
"snapshots": [
{
"date": "2025-10-08T00:00:00Z",
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent date format in documentation: The example shows dates in ISO 8601 format with time zone ("2025-10-08T00:00:00Z"), but the actual API implementation uses DateTimeOffset.UtcNow.Date which creates a date at midnight. Ensure the documentation example matches the actual API response format. Consider showing a more realistic recent date instead of a future date (2025-10-08 is in the past relative to November 2025).

Suggested change
"date": "2025-10-08T00:00:00Z",
"date": "2024-06-01T00:00:00Z",

Copilot uses AI. Check for mistakes.
Comment on lines +527 to 554
label: function(context) {
const idx = context.dataIndex;
const devices = complianceData[idx];
const percentage = compliancePercentages[idx].toFixed(1);
return `Compliant: ${devices} (${percentage}%)`;
},
title: function(context) {
const date = Object.keys(trendData)[context[0].dataIndex];
return new Date(date).toLocaleDateString('it-IT', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
});
@if (Model.ComplianceTrend != null && Model.ComplianceTrend.Snapshots.Count > 0)
{
<text>
const idx = context[0].dataIndex;
const snapshot = snapshots[idx];
const date = new Date(snapshot.date);
return date.toLocaleDateString('it-IT', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
});
</text>
}
else
{
<text>
return context[0].label;
</text>
}
}
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability issue: The tooltip callbacks have duplicated logic with nested Razor conditionals that make the code hard to read and maintain. The @if blocks inside JavaScript callbacks are complex to debug. Consider moving this logic to a separate JavaScript function or simplifying by always using the snapshot data structure in the fallback case as well.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +85
var startDate = DateTimeOffset.UtcNow.Date.AddDays(-days);

// Get all reports within the date range
var reports = await _dbContext.Reports
.AsNoTracking()
.Where(r => r.CreatedAtUtc >= startDate)
.Select(r => new { r.CreatedAtUtc, r.DeploymentState, r.DeviceId })
.ToListAsync(cancellationToken);

// Get all devices to calculate total device count per day
var allDevices = await _dbContext.Devices
.AsNoTracking()
.Select(d => new { d.Id, d.CreatedAtUtc })
.ToListAsync(cancellationToken);

// Calculate daily snapshots
var dailySnapshots = new Dictionary<string, DailySnapshot>();

for (int i = 0; i < days; i++)
{
var date = DateTimeOffset.UtcNow.Date.AddDays(-i);
var dateKey = date.ToString("yyyy-MM-dd");

// Devices that existed on this date
var devicesOnDate = allDevices.Where(d => d.CreatedAtUtc.Date <= date).Select(d => d.Id).ToHashSet();

// For each device, find its most recent deployment state as of this date
var deviceStates = new Dictionary<Guid, string?>();

foreach (var deviceId in devicesOnDate)
{
var latestReport = reports
.Where(r => r.DeviceId == deviceId && r.CreatedAtUtc.Date <= date)
.OrderByDescending(r => r.CreatedAtUtc)
.FirstOrDefault();

deviceStates[deviceId] = latestReport?.DeploymentState;
}

var totalDevices = devicesOnDate.Count;
var deployedDevices = deviceStates.Count(kvp => kvp.Value == "Deployed");
var pendingDevices = deviceStates.Count(kvp => kvp.Value == "Pending");
var errorDevices = deviceStates.Count(kvp => kvp.Value == "Error");
var unknownDevices = totalDevices - deployedDevices - pendingDevices - errorDevices;

Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential performance issue: This implementation loads all reports and all devices into memory before processing. For large datasets (thousands of devices with millions of reports), this could cause memory issues and slow performance. Consider implementing pagination or using database-side aggregation with grouping queries to reduce memory footprint.

Suggested change
var startDate = DateTimeOffset.UtcNow.Date.AddDays(-days);
// Get all reports within the date range
var reports = await _dbContext.Reports
.AsNoTracking()
.Where(r => r.CreatedAtUtc >= startDate)
.Select(r => new { r.CreatedAtUtc, r.DeploymentState, r.DeviceId })
.ToListAsync(cancellationToken);
// Get all devices to calculate total device count per day
var allDevices = await _dbContext.Devices
.AsNoTracking()
.Select(d => new { d.Id, d.CreatedAtUtc })
.ToListAsync(cancellationToken);
// Calculate daily snapshots
var dailySnapshots = new Dictionary<string, DailySnapshot>();
for (int i = 0; i < days; i++)
{
var date = DateTimeOffset.UtcNow.Date.AddDays(-i);
var dateKey = date.ToString("yyyy-MM-dd");
// Devices that existed on this date
var devicesOnDate = allDevices.Where(d => d.CreatedAtUtc.Date <= date).Select(d => d.Id).ToHashSet();
// For each device, find its most recent deployment state as of this date
var deviceStates = new Dictionary<Guid, string?>();
foreach (var deviceId in devicesOnDate)
{
var latestReport = reports
.Where(r => r.DeviceId == deviceId && r.CreatedAtUtc.Date <= date)
.OrderByDescending(r => r.CreatedAtUtc)
.FirstOrDefault();
deviceStates[deviceId] = latestReport?.DeploymentState;
}
var totalDevices = devicesOnDate.Count;
var deployedDevices = deviceStates.Count(kvp => kvp.Value == "Deployed");
var pendingDevices = deviceStates.Count(kvp => kvp.Value == "Pending");
var errorDevices = deviceStates.Count(kvp => kvp.Value == "Error");
var unknownDevices = totalDevices - deployedDevices - pendingDevices - errorDevices;
var startDate = DateTimeOffset.UtcNow.Date.AddDays(-days + 1);
// Precompute the list of days (from oldest to newest)
var daysList = Enumerable.Range(0, days)
.Select(i => DateTimeOffset.UtcNow.Date.AddDays(-i))
.OrderBy(d => d)
.ToList();
var dailySnapshots = new Dictionary<string, DailySnapshot>();
foreach (var date in daysList)
{
var dateKey = date.ToString("yyyy-MM-dd");
// Get devices that existed as of this date
var devicesOnDateQuery = _dbContext.Devices
.AsNoTracking()
.Where(d => d.CreatedAtUtc.Date <= date.Date)
.Select(d => d.Id);
var devicesOnDate = await devicesOnDateQuery.ToListAsync(cancellationToken);
// For all these devices, get their latest report as of this date
var latestReportsQuery =
from r in _dbContext.Reports.AsNoTracking()
where r.CreatedAtUtc <= date && devicesOnDate.Contains(r.DeviceId)
group r by r.DeviceId into g
select new
{
DeviceId = g.Key,
LatestDeploymentState = g.OrderByDescending(x => x.CreatedAtUtc).Select(x => x.DeploymentState).FirstOrDefault()
};
var deviceStates = await latestReportsQuery.ToListAsync(cancellationToken);
var deviceStatesDict = deviceStates.ToDictionary(x => x.DeviceId, x => x.LatestDeploymentState);
var totalDevices = devicesOnDate.Count;
var deployedDevices = deviceStatesDict.Count(kvp => kvp.Value == "Deployed");
var pendingDevices = deviceStatesDict.Count(kvp => kvp.Value == "Pending");
var errorDevices = deviceStatesDict.Count(kvp => kvp.Value == "Error");
var unknownDevices = totalDevices - deployedDevices - pendingDevices - errorDevices;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Optimized by pre-grouping reports by device using GroupBy and ToDictionary, then using FirstOrDefault on sorted lists. This eliminates the O(days × devices × reports) complexity, reducing operations from ~900M to ~10M for typical scenarios (90x faster).

Comment on lines +58 to +85

for (int i = 0; i < days; i++)
{
var date = DateTimeOffset.UtcNow.Date.AddDays(-i);
var dateKey = date.ToString("yyyy-MM-dd");

// Devices that existed on this date
var devicesOnDate = allDevices.Where(d => d.CreatedAtUtc.Date <= date).Select(d => d.Id).ToHashSet();

// For each device, find its most recent deployment state as of this date
var deviceStates = new Dictionary<Guid, string?>();

foreach (var deviceId in devicesOnDate)
{
var latestReport = reports
.Where(r => r.DeviceId == deviceId && r.CreatedAtUtc.Date <= date)
.OrderByDescending(r => r.CreatedAtUtc)
.FirstOrDefault();

deviceStates[deviceId] = latestReport?.DeploymentState;
}

var totalDevices = devicesOnDate.Count;
var deployedDevices = deviceStates.Count(kvp => kvp.Value == "Deployed");
var pendingDevices = deviceStates.Count(kvp => kvp.Value == "Pending");
var errorDevices = deviceStates.Count(kvp => kvp.Value == "Error");
var unknownDevices = totalDevices - deployedDevices - pendingDevices - errorDevices;

Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient nested loop pattern (N+1 problem): For each of the days iterations, you're iterating through devicesOnDate, and for each device, you're performing a LINQ query on reports. This results in O(days × devices × reports) complexity. For 90 days with 1000 devices and 10000 reports, this could be millions of iterations. Consider pre-grouping reports by device and date to avoid repeated filtering.

Suggested change
for (int i = 0; i < days; i++)
{
var date = DateTimeOffset.UtcNow.Date.AddDays(-i);
var dateKey = date.ToString("yyyy-MM-dd");
// Devices that existed on this date
var devicesOnDate = allDevices.Where(d => d.CreatedAtUtc.Date <= date).Select(d => d.Id).ToHashSet();
// For each device, find its most recent deployment state as of this date
var deviceStates = new Dictionary<Guid, string?>();
foreach (var deviceId in devicesOnDate)
{
var latestReport = reports
.Where(r => r.DeviceId == deviceId && r.CreatedAtUtc.Date <= date)
.OrderByDescending(r => r.CreatedAtUtc)
.FirstOrDefault();
deviceStates[deviceId] = latestReport?.DeploymentState;
}
var totalDevices = devicesOnDate.Count;
var deployedDevices = deviceStates.Count(kvp => kvp.Value == "Deployed");
var pendingDevices = deviceStates.Count(kvp => kvp.Value == "Pending");
var errorDevices = deviceStates.Count(kvp => kvp.Value == "Error");
var unknownDevices = totalDevices - deployedDevices - pendingDevices - errorDevices;
// Pre-group reports by device, sorted descending by CreatedAtUtc
var reportsByDevice = reports
.GroupBy(r => r.DeviceId)
.ToDictionary(
g => g.Key,
g => g.OrderByDescending(r => r.CreatedAtUtc).ToList()
);
// For each device, build a per-day latest report lookup
// Dictionary: [deviceId][dateKey] = latestReport
var deviceDayLatestReport = new Dictionary<Guid, Dictionary<string, string?>>();
// Precompute all date keys (from most recent to oldest)
var dateList = Enumerable.Range(0, days)
.Select(i => DateTimeOffset.UtcNow.Date.AddDays(-i))
.ToList();
var dateKeys = dateList.Select(d => d.ToString("yyyy-MM-dd")).ToList();
foreach (var device in allDevices)
{
var deviceId = device.Id;
var createdAt = device.CreatedAtUtc.Date;
var perDay = new Dictionary<string, string?>();
// Only consider dates on/after device creation
var validDates = dateList.Where(d => createdAt <= d).ToList();
var validDateKeys = validDates.Select(d => d.ToString("yyyy-MM-dd")).ToList();
if (reportsByDevice.TryGetValue(deviceId, out var deviceReports))
{
int reportIdx = 0;
// For each date, find the latest report as of that date
foreach (var (date, dateKey) in validDates.Zip(validDateKeys, (d, k) => (d, k)))
{
// Find the first report with CreatedAtUtc.Date <= date
while (reportIdx < deviceReports.Count && deviceReports[reportIdx].CreatedAtUtc.Date > date)
{
reportIdx++;
}
var latestReport = (reportIdx < deviceReports.Count && deviceReports[reportIdx].CreatedAtUtc.Date <= date)
? deviceReports[reportIdx]
: null;
perDay[dateKey] = latestReport?.DeploymentState;
}
}
else
{
// No reports for this device
foreach (var dateKey in validDateKeys)
{
perDay[dateKey] = null;
}
}
deviceDayLatestReport[deviceId] = perDay;
}
// Now, for each day, build the snapshot using the precomputed data
for (int i = 0; i < days; i++)
{
var date = dateList[i];
var dateKey = dateKeys[i];
// Devices that existed on this date
var devicesOnDate = allDevices.Where(d => d.CreatedAtUtc.Date <= date).Select(d => d.Id).ToHashSet();
// For each device, get its most recent deployment state as of this date
var deviceStates = new Dictionary<Guid, string?>();
foreach (var deviceId in devicesOnDate)
{
if (deviceDayLatestReport.TryGetValue(deviceId, out var perDay) && perDay.TryGetValue(dateKey, out var state))
{
deviceStates[deviceId] = state;
}
else
{
deviceStates[deviceId] = null;
}
}
var totalDevices = devicesOnDate.Count;
var deployedDevices = deviceStates.Count(kvp => kvp.Value == "Deployed");
var pendingDevices = deviceStates.Count(kvp => kvp.Value == "Pending");
var errorDevices = deviceStates.Count(kvp => kvp.Value == "Error");
var unknownDevices = totalDevices - deployedDevices - pendingDevices - errorDevices;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 12a4285. Pre-grouped reports by DeviceId with reportsByDevice.TryGetValue() lookup. The performance improvement eliminates the N+1 pattern, reducing from O(days × devices × reports) to O(devices × reports).

@robgrame robgrame requested a review from Copilot November 7, 2025 13:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

@robgrame robgrame requested a review from Copilot November 7, 2025 14:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Comment thread SecureBootDashboard.Api/Controllers/AnalyticsController.cs
Comment thread docs/ENHANCED_ANALYTICS.md Outdated
The dashboard now supports viewing compliance trends over multiple time periods:
- **7 days** - Short-term compliance tracking
- **30 days** - Monthly compliance analysis
- **60 days** - Quarterly compliance trends
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected description: 60 days represents approximately 2 months, not a quarter. A quarter is typically 90 days or 3 months.

Suggested change
- **60 days** - Quarterly compliance trends
- **60 days** - Bi-monthly compliance trends

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in commit 4715170 (applied before my changes). Documentation now correctly describes 60 days as "Bi-monthly compliance trends" instead of "Quarterly".

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@robgrame
Copy link
Copy Markdown
Owner

robgrame commented Nov 7, 2025

@copilot apply changes based on the comments in this thread

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@robgrame
Copy link
Copy Markdown
Owner

robgrame commented Nov 7, 2025

@copilot apply changes based on the comments in this thread

robgrame and others added 3 commits November 7, 2025 15:32
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…proj

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ty, quality improvements

Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Copilot AI requested a review from robgrame November 7, 2025 14:47
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Nov 7, 2025

@copilot apply changes based on the comments in this thread

All feedback from review thread 3433704283 has been addressed in commit 12a4285:

✅ JSON property casing fixed (camelCase serialization)
✅ Performance optimization (pre-grouped reports)
✅ LoggerFactory disposal fixed
✅ Exception handling enhanced (TaskCanceledException, JsonException)
✅ Accessibility improvements (ARIA labels)
✅ Fallback data accuracy (0 for historical counts, device check)
✅ Documentation corrections and links

All 10 unit tests passing. No new issues introduced.

@robgrame robgrame requested a review from Copilot November 7, 2025 22:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Comment thread SecureBootDashboard.Web/Pages/Index.cshtml
Comment thread SecureBootDashboard.Api/Controllers/AnalyticsController.cs
Comment thread SecureBootDashboard.Web/Pages/Index.cshtml.cs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhanced analytics (30/60/90 day trends)

3 participants