Problema originale: La stessa macchina appariva più volte nella dashboard perché venivano mostrati i report invece dei dispositivi unici.
Soluzione: Implementazione di device deduplication con tracking dello storico report.
La struttura del database era già corretta con:
- Tabella
Devicesseparata con ID univoco - Relazione 1-to-many:
Device?Reports - Index su
MachineNameper performance - Campi
FirstSeenUtceLastSeenUtcper tracking temporale
Il codice in EfCoreReportStore.SaveAsync() già implementa correttamente:
// Cerca device esistente per MachineName + DomainName
var device = await _dbContext.Devices
.FirstOrDefaultAsync(d =>
d.MachineName == report.Device.MachineName &&
d.DomainName == report.Device.DomainName,
cancellationToken);
if (device == null)
{
// Crea nuovo device
device = new DeviceEntity { ... };
_dbContext.Devices.Add(device);
}
else
{
// Aggiorna device esistente
device.LastSeenUtc = utcNow;
device.FirmwareVersion = report.Device.FirmwareVersion;
// ...
}
// Crea SEMPRE un nuovo report collegato al device
var reportEntity = new SecureBootReportEntity
{
Device = device, // ? Relazione con device esistente o nuovo
// ...
};Comportamento:
- ? Device viene creato una sola volta (identificato da
MachineName+DomainName) - ? Ogni invio crea un nuovo report collegato al device
- ?
LastSeenUtcviene aggiornato ogni volta - ? Mantiene storico completo dei report per dispositivo
Creato nuovo controller /api/Devices con endpoint:
Restituisce lista di dispositivi unici con summary:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"machineName": "W11DEV",
"domainName": "MSINTUNE.LAB",
"fleetId": "mslabs",
"manufacturer": "Microsoft Corporation",
"model": "Virtual Machine",
"firstSeenUtc": "2025-01-15T10:00:00Z",
"lastSeenUtc": "2025-01-15T14:30:00Z",
"reportCount": 5,
"latestDeploymentState": "Deployed",
"latestReportDate": "2025-01-15T14:30:00Z"
}
]Restituisce dettagli device con ultimi 10 report:
{
"id": "550e8400-...",
"machineName": "W11DEV",
"domainName": "MSINTUNE.LAB",
"userPrincipalName": "admin@msintune.lab",
"fleetId": "mslabs",
"manufacturer": "Microsoft Corporation",
"model": "Virtual Machine",
"firmwareVersion": "Hyper-V UEFI Release v4.1",
"tagsJson": "{\"environment\":\"dev\",\"fleet\":\"mslabs\"}",
"firstSeenUtc": "2025-01-15T10:00:00Z",
"lastSeenUtc": "2025-01-15T14:30:00Z",
"recentReports": [
{
"reportId": "abc123...",
"createdAtUtc": "2025-01-15T14:30:00Z",
"deploymentState": "Deployed",
"clientVersion": "1.0.0"
},
{
"reportId": "def456...",
"createdAtUtc": "2025-01-15T13:00:00Z",
"deploymentState": "Pending",
"clientVersion": "1.0.0"
}
]
}Restituisce storico report per un device specifico.
- Prima: W11DEV appare 3 volte (una per ogni report)
- Dopo: W11DEV appare 1 volta con "Report Count: 3"
- First Seen: Quando il device ha inviato il primo report
- Last Seen: Quando ha inviato l'ultimo report
- Utile per identificare device inattivi
Confrontando i report storici si può vedere quando un device ha cambiato stato:
- Report 1 (10:00):
DeploymentState: "Pending" - Report 2 (11:00):
DeploymentState: "Deployed"? CAMBIAMENTO RILEVATO - Report 3 (12:00):
DeploymentState: "Deployed"
- Meno righe da visualizzare nella dashboard
- Index su
MachineNameper query veloci - Caricamento più rapido
Modifica ISecureBootApiClient per aggiungere:
Task<IReadOnlyList<DeviceSummary>> GetDevicesAsync(CancellationToken cancellationToken = default);
Task<DeviceDetail?> GetDeviceAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ReportHistoryItem>> GetDeviceReportsAsync(Guid deviceId, int limit = 50, CancellationToken cancellationToken = default);Mostra tutti i dispositivi unici con:
- Machine Name
- Domain
- Last Seen
- Report Count
- Latest Deployment State
- Bottone "View Details" ?
/Devices/Details/{id} - Bottone "View Reports" ?
/Devices/{id}/Reports
Mostra informazioni complete del device:
- Machine Name, Domain, UPN
- Manufacturer, Model, Firmware Version
- Fleet ID, Tags
- First Seen / Last Seen
- Tabella ultimi 10 report con link a
/Reports/Details/{reportId}
Mostra tutti i report per un device specifico:
- Timeline dei report
- Evidenzia cambiamenti di stato
- Link a dettagli completi di ogni report
Modifica /Pages/Index.cshtml per:
- Mostrare conteggio dispositivi unici invece di report
- Aggiungere card "Active Devices" (visti nelle ultime 24h)
- Aggiungere card "Inactive Devices" (non visti da >7 giorni)
- Tabella "Recent Activity" con ultimi device visti
Crea query per rilevare cambiamenti di stato:
// API endpoint: GET /api/Devices/{id}/state-changes
public async Task<IReadOnlyCollection<StateChangeEvent>> GetStateChangesAsync(Guid deviceId)
{
var reports = await _dbContext.Reports
.Where(r => r.DeviceId == deviceId)
.OrderBy(r => r.CreatedAtUtc)
.Select(r => new { r.CreatedAtUtc, r.DeploymentState })
.ToListAsync();
var changes = new List<StateChangeEvent>();
string? previousState = null;
foreach (var report in reports)
{
if (report.DeploymentState != previousState && previousState != null)
{
changes.Add(new StateChangeEvent(
report.CreatedAtUtc,
previousState,
report.DeploymentState ?? "Unknown"));
}
previousState = report.DeploymentState;
}
return changes;
}Crea background job che controlla device inattivi:
// Trova device non visti da >7 giorni
var inactiveDevices = await _dbContext.Devices
.Where(d => d.LastSeenUtc < DateTimeOffset.UtcNow.AddDays(-7))
.ToListAsync();
foreach (var device in inactiveDevices)
{
_logger.LogWarning("Device {MachineName} inactive for {Days} days. Last seen: {LastSeen}",
device.MachineName,
(DateTimeOffset.UtcNow - device.LastSeenUtc).TotalDays,
device.LastSeenUtc);
}Azione: Invia 3 report dalla stessa macchina (W11DEV)
Risultato Atteso:
-- 1 device
SELECT * FROM Devices WHERE MachineName = 'W11DEV';
-- Id: 550e8400-..., MachineName: W11DEV, ReportCount: 3
-- 3 report collegati allo stesso device
SELECT * FROM SecureBootReports WHERE DeviceId = '550e8400-...';
-- Report1: CreatedAtUtc = 2025-01-15 10:00:00
-- Report2: CreatedAtUtc = 2025-01-15 11:00:00
-- Report3: CreatedAtUtc = 2025-01-15 12:00:00Verifica Web:
GET /api/Devices
? Risposta: 1 device con reportCount: 3
GET /api/Devices/550e8400-.../reports
? Risposta: 3 report
Azione: Invia report da W11DEV alle 10:00, poi alle 14:00
Risultato Atteso:
SELECT FirstSeenUtc, LastSeenUtc FROM Devices WHERE MachineName = 'W11DEV';
-- FirstSeenUtc: 2025-01-15 10:00:00 (invariato)
-- LastSeenUtc: 2025-01-15 14:00:00 (aggiornato)Azione: Invia 3 report con deployment state diversi
Risultato Atteso:
Report 1 (10:00): DeploymentState = "Pending"
Report 2 (11:00): DeploymentState = "Deployed" ? CAMBIO STATO
Report 3 (12:00): DeploymentState = "Deployed"
Query state changes:
GET /api/Devices/550e8400-.../state-changes
? [{
"timestamp": "2025-01-15T11:00:00Z",
"fromState": "Pending",
"toState": "Deployed"
}]
-- Conta dispositivi unici
SELECT COUNT(*) AS TotalDevices FROM Devices;
-- Dispositivi con più report
SELECT
MachineName,
COUNT(*) AS ReportCount,
MIN(CreatedAtUtc) AS FirstSeen,
MAX(LastSeenUtc) AS LastSeen
FROM Devices d
INNER JOIN SecureBootReports r ON d.Id = r.DeviceId
GROUP BY MachineName
ORDER BY ReportCount DESC;
-- Dispositivi inattivi (non visti da >7 giorni)
SELECT MachineName, LastSeenUtc,
DATEDIFF(DAY, LastSeenUtc, GETUTCDATE()) AS DaysInactive
FROM Devices
WHERE LastSeenUtc < DATEADD(DAY, -7, GETUTCDATE())
ORDER BY LastSeenUtc ASC;
-- Cambiamenti di stato per un device
SELECT
CreatedAtUtc,
DeploymentState,
LAG(DeploymentState) OVER (ORDER BY CreatedAtUtc) AS PreviousState
FROM SecureBootReports
WHERE DeviceId = '550e8400-...'
ORDER BY CreatedAtUtc;Dashboard PRIMA:
?? Report Recenti (3 righe)
- W11DEV | MSINTUNE.LAB | Deployed | 2025-01-15 12:00:00 | [Dettagli]
- W11DEV | MSINTUNE.LAB | Deployed | 2025-01-15 11:00:00 | [Dettagli]
- W11DEV | MSINTUNE.LAB | Pending | 2025-01-15 10:00:00 | [Dettagli]
Dashboard DOPO:
?? Dispositivi (1 riga)
- W11DEV | MSINTUNE.LAB | 3 reports | Last Seen: 2 min ago | Deployed | [Dettagli] [Reports]
?? Statistics:
- Total Devices: 1
- Active (24h): 1
- Inactive (>7d): 0
- Database structure (già esistente)
- EfCoreReportStore deduplication logic (già implementata)
- API DevicesController creato
- Web client
ISecureBootApiClientaggiornato - Razor Pages
/Devices/Indexcreata - Razor Pages
/Devices/Detailscreata - Razor Pages
/Devices/{id}/Reportscreata - Dashboard principale aggiornata per mostrare devices
- State change detection implementata
- Alert per device inattivi
- Test end-to-end
Testa il nuovo API endpoint:
# Avvia l'API
cd SecureBootDashboard.Api
dotnet run
# In un altro terminale, testa l'endpoint
Invoke-RestMethod -Uri "https://localhost:5001/api/Devices" -Method GetOutput atteso:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"machineName": "W11DEV",
"domainName": "MSINTUNE.LAB",
"fleetId": "mslabs",
"reportCount": 3,
"latestDeploymentState": "Deployed",
"lastSeenUtc": "2025-01-15T14:30:00Z"
}
]Se funziona, procedi con l'aggiornamento del web client! ??