Add device details view with report merging and linking functionality

This commit is contained in:
2025-09-13 02:33:31 +01:00
parent 356ff26944
commit 64675a73e8
4 changed files with 836 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Aaru.Server/Aaru.Documentation" vcs="Git" />
</component>
</project>

View File

@@ -4,7 +4,7 @@ namespace Aaru.Server.Database.Models;
public class DeviceDetails
{
public Device Report { get; set; }
public Device? Report { get; set; }
public List<int> SameAll { get; set; }
public List<int> SameButManufacturer { get; set; }
public List<int> ReportAll { get; set; }

View File

@@ -0,0 +1,483 @@
@page "/admin/devices/{id:int}"
@attribute [Authorize]
@layout AdminLayout
@inject Microsoft.EntityFrameworkCore.IDbContextFactory<DbContext> DbContextFactory
<PageTitle>Device reports</PageTitle>
@if(!_initialized)
{
<div class="stats-section">
<h1 style="color: red; align-content: center; padding: 2rem">Loading...</h1>
</div>
return;
}
@if(_notFound)
{
<div class="stats-section">
<h1 style="color: red; align-content: center; padding: 2rem">Device report not found</h1>
</div>
return;
}
<section class="stats-section">
<div>
<h4>Device report</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.AddedWhen))
</dt>
<dd class="col-sm-10">
@model.Report?.AddedWhen
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.ModifiedWhen))
</dt>
<dd class="col-sm-10">
@model.Report?.ModifiedWhen
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.Manufacturer))
</dt>
<dd class="col-sm-10">
@model.Report?.Manufacturer
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.Model))
</dt>
<dd class="col-sm-10">
@model.Report?.Model
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.Revision))
</dt>
<dd class="col-sm-10">
@model.Report?.Revision
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.CompactFlash))
</dt>
<dd class="col-sm-10">
@model.Report?.CompactFlash
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.OptimalMultipleSectorsRead))
</dt>
<dd class="col-sm-10">
@model.Report?.OptimalMultipleSectorsRead
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.CanReadGdRomUsingSwapDisc))
</dt>
<dd class="col-sm-10">
@model.Report?.CanReadGdRomUsingSwapDisc
</dd>
<dt class="col-sm-2">
@DisplayNameHelper.GetDisplayName(typeof(Device), nameof(Device.Type))
</dt>
<dd class="col-sm-10">
@model.Report?.Type
</dd>
</dl>
</div>
<div>
<a href="/admin/devices/edit/@model.Report?.Id" class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit
</a>
<a href="/admin/devices" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to List
</a>
</div>
@if(model.ReadCapabilitiesId != 0)
{
<div>
<a href="/admin/tested-media/@model.ReadCapabilitiesId" target="_blank">Read capabilities</a>
</div>
}
@if(model.Report.ATA != null)
{
<div>
<a href="/admin/ata/@model.Report.ATA.Id" target="_blank">ATA report</a>
</div>
}
@if(model.Report.ATAPI != null)
{
<div>
<a href="/admin/ata/@model.Report.ATAPI.Id" target="_blank">ATAPI report</a>
</div>
}
@if(model.Report.SCSI != null)
{
<div>
<a href="/admin/scsi/@model.Report.SCSI.Id" target="_blank">SCSI report</a>
</div>
}
@if(model.Report.MultiMediaCard != null)
{
<div>
<a href="/admin/mmc-sd/@model.Report.MultiMediaCard.Id" target="_blank">MultiMediaCard report</a>
</div>
}
@if(model.Report.SecureDigital != null)
{
<div>
<a href="/admin/mmc-sd/@model.Report.SecureDigital.Id" target="_blank">SecureDigital report</a>
</div>
}
@if(model.Report.USB != null)
{
<div>
<a href="/admin/usb/devices/@model.Report.USB.Id" target="_blank">USB report</a>
</div>
}
@if(model.Report.GdRomSwapDiscCapabilitiesId != null)
{
<div>
<a href="/admin/gdrom/@model.Report.GdRomSwapDiscCapabilitiesId" target="_blank">GD-ROM swap-trick
capabilities
report</a>
</div>
}
@if(model.Report.FireWire != null)
{
<div>
Has a FireWire report.
</div>
}
@if(model.Report.PCMCIA != null)
{
<div>
Has a PCMCIA report.
</div>
}
@if(model.SameAll.Count > 0)
{
<div>
<h4>Other device reports with same manufacturer, model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in model.SameAll)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/devices/@item" class="btn btn-primary" target="_blank">Details</a>
<a @onclick="() => Merge(model.Report.Id, item)" class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.SameButManufacturer.Count > 0)
{
<div>
<h4>Other device reports with same model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in model.SameButManufacturer)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/devices/@item" class="btn btn-primary" target="_blank">Details</a>
<a @onclick="() => Merge(model.Report.Id, item)" class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.ReportAll.Count > 0)
{
<div>
<h4>Uploaded reports with same manufacturer, model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in model.ReportAll)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/reports/@item" class="btn btn-primary" target="_blank">Details</a>
<a @onclick="() => MergeReports(model.Report.Id, item)" class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.ReportButManufacturer.Count > 0)
{
<div>
<h4>Device reports with same model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
Id
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(int item in model.ReportButManufacturer)
{
<tr>
<td>
@item
</td>
<td>
<a href="/admin/reports/@item" class="btn btn-primary" target="_blank">Details</a>
<a @onclick="() => MergeReports(model.Report.Id, item)" class="btn btn-secondary">Merge</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.StatsAll.Count > 0)
{
<div>
<h4>Device statistics with same manufacturer, model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Manufacturer))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Model))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Revision))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Bus))
</th>
<th>
Has a linked report?
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(DeviceStat item in model.StatsAll)
{
<tr>
<td>
@item.Manufacturer
</td>
<td>
@item.Model
</td>
<td>
@item.Revision
</td>
<td>
@item.Bus
</td>
<td>
@if(item.Report is null)
{
@("No")
}
else
{
@if(item.Report.Id == model.Report.Id)
{
@("Us")
}
else
{
@("Yes")
}
}
</td>
<td>
<a @onclick="() => LinkReports(model.Report.Id, item.Id)" class="btn btn-secondary">Link</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.StatsButManufacturer.Count > 0)
{
<div>
<h4>Device statistics with same model and revision:</h4>
<table class="table">
<thead>
<tr>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Manufacturer))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Model))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Revision))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(DeviceStat), nameof(DeviceStat.Bus))
</th>
<th>
Has a linked report?
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(DeviceStat item in model.StatsButManufacturer)
{
<tr>
<td>
@item.Manufacturer
</td>
<td>
@item.Model
</td>
<td>
@item.Revision
</td>
<td>
@item.Bus
</td>
<td>
@if(item.Report is null)
{
@("No")
}
else
{
<a href="/admin/reports/@item.Report.Id" target="_blank">Yes</a>
}
</td>
<td>
<a @onclick="() => LinkReports(model.Report.Id, item.Id)" class="btn btn-secondary">Link</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.TestedMedias.Count > 0)
{
<div>
<h4>Tested media:</h4>
<table class="table">
<thead>
<tr>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedMedia), nameof(TestedMedia.Manufacturer))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedMedia), nameof(TestedMedia.Model))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedMedia), nameof(TestedMedia.MediumTypeName))
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach(TestedMedia item in model.TestedMedias)
{
<tr>
<td>
@item.Manufacturer
</td>
<td>
@item.Model
</td>
<td>
@item.MediumTypeName
</td>
<td>
<a href="/admin/tested-media/@item.Id" class="btn btn-secondary" target="_blank">Details</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@if(model.TestedSequentialMedias.Count > 0)
{
<div>
<h4>Tested media:</h4>
<table class="table">
<thead>
<tr>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedSequentialMedia), nameof(TestedSequentialMedia.Manufacturer))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedSequentialMedia), nameof(TestedSequentialMedia.Model))
</th>
<th>
@DisplayNameHelper.GetDisplayName(typeof(TestedSequentialMedia), nameof(TestedSequentialMedia.MediumTypeName))
</th>
</tr>
</thead>
<tbody>
@foreach(TestedSequentialMedia item in model.TestedSequentialMedias)
{
<tr>
<td>
@item.Manufacturer
</td>
<td>
@item.Model
</td>
<td>
@item.MediumTypeName
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@@ -0,0 +1,345 @@
using Aaru.CommonTypes.Metadata;
using Aaru.Server.Database.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;
using DbContext = Aaru.Server.Database.DbContext;
namespace Aaru.Server.Components.Admin.Pages.Devices;
public partial class Details
{
bool _initialized;
bool _notFound;
DeviceDetails model;
[Parameter]
public int Id { get; set; }
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await RefreshItemsAsync();
}
async Task RefreshItemsAsync()
{
_initialized = false;
_notFound = false;
StateHasChanged();
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
model = new DeviceDetails
{
Report = await ctx.Devices.Include(static deviceReport => deviceReport.ATAPI)
.Include(static deviceReport => deviceReport.ATA)
.ThenInclude(static ata => ata.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.MultiMediaDevice)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.SequentialDevice)
.FirstOrDefaultAsync(m => m.Id == Id)
};
if(model.Report is null)
{
_initialized = true;
_notFound = true;
StateHasChanged();
return;
}
model.ReportAll = await ctx.Reports
.Where(d => d.Manufacturer == model.Report.Manufacturer &&
d.Model == model.Report.Model &&
d.Revision == model.Report.Revision)
.Select(static d => d.Id)
.ToListAsync();
model.ReportButManufacturer = await ctx.Reports
.Where(d => d.Model == model.Report.Model &&
d.Revision == model.Report.Revision)
.Select(static d => d.Id)
.Where(d => model.ReportAll.All(r => r != d))
.ToListAsync();
model.SameAll = await ctx.Devices
.Where(d => d.Manufacturer == model.Report.Manufacturer &&
d.Model == model.Report.Model &&
d.Revision == model.Report.Revision &&
d.Id != Id)
.Select(static d => d.Id)
.ToListAsync();
model.SameButManufacturer = await ctx.Devices
.Where(d => d.Model == model.Report.Model &&
d.Revision == model.Report.Revision &&
d.Id != Id)
.Select(static d => d.Id)
.Where(d => model.SameAll.All(r => r != d))
.ToListAsync();
model.StatsAll = await ctx.DeviceStats
.Where(d => d.Manufacturer == model.Report.Manufacturer &&
d.Model == model.Report.Model &&
d.Revision == model.Report.Revision &&
d.Report != null &&
d.Report.Id != model.Report.Id)
.ToListAsync();
model.StatsButManufacturer = ctx.DeviceStats
.Where(d => d.Model == model.Report.Model &&
d.Revision == model.Report.Revision &&
d.Report != null &&
d.Report.Id != model.Report.Id)
.AsEnumerable()
.Where(d => model.StatsAll.All(s => s.Id != d.Id))
.ToList();
model.ReadCapabilitiesId =
model.Report.ATA?.ReadCapabilities?.Id ?? model.Report.SCSI?.ReadCapabilities?.Id ?? 0;
// So we can check, as we know IDs with 0 will never exist, and EFCore does not allow null propagation in the LINQ
int ataId = model.Report.ATA?.Id ?? 0;
int atapiId = model.Report.ATAPI?.Id ?? 0;
int scsiId = model.Report.SCSI?.Id ?? 0;
int mmcId = model.Report.SCSI?.MultiMediaDevice?.Id ?? 0;
int sscId = model.Report.SCSI?.SequentialDevice?.Id ?? 0;
model.TestedMedias = await ctx.TestedMedia
.Where(t => t.AtaId == ataId ||
t.AtaId == atapiId ||
t.ScsiId == scsiId ||
t.MmcId == mmcId)
.OrderBy(static t => t.Manufacturer)
.ThenBy(static t => t.Model)
.ThenBy(static t => t.MediumTypeName)
.ToListAsync();
model.TestedSequentialMedias = await ctx.TestedSequentialMedia.Where(t => t.SscId == sscId)
.OrderBy(static t => t.Manufacturer)
.ThenBy(static t => t.Model)
.ThenBy(static t => t.MediumTypeName)
.ToListAsync();
_initialized = true;
StateHasChanged();
}
async Task Merge(int master, int slave)
{
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
Device? masterDevice = await ctx.Devices.FirstOrDefaultAsync(m => m.Id == master);
Device? slaveDevice = await ctx.Devices.FirstOrDefaultAsync(m => m.Id == slave);
if(masterDevice is null || slaveDevice is null) return;
if(masterDevice.ATAId != null && masterDevice.ATAId != slaveDevice.ATAId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => d.AtaId == slaveDevice.ATAId))
{
testedMedia.AtaId = masterDevice.ATAId;
ctx.Update(testedMedia);
}
}
else if(masterDevice.ATAId == null && slaveDevice.ATAId != null)
{
masterDevice.ATAId = slaveDevice.ATAId;
ctx.Update(masterDevice);
}
if(masterDevice.ATAPIId != null && masterDevice.ATAPIId != slaveDevice.ATAPIId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => d.AtaId == slaveDevice.ATAPIId))
{
testedMedia.AtaId = masterDevice.ATAPIId;
ctx.Update(testedMedia);
}
}
else if(masterDevice.ATAPIId == null && slaveDevice.ATAPIId != null)
{
masterDevice.ATAPIId = slaveDevice.ATAPIId;
ctx.Update(masterDevice);
}
if(masterDevice.SCSIId != null && masterDevice.SCSIId != slaveDevice.SCSIId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => d.ScsiId == slaveDevice.SCSIId))
{
testedMedia.ScsiId = masterDevice.SCSIId;
ctx.Update(testedMedia);
}
}
else if(masterDevice.SCSIId == null && slaveDevice.SCSIId != null)
{
masterDevice.SCSIId = slaveDevice.SCSIId;
ctx.Update(masterDevice);
}
masterDevice.ModifiedWhen = DateTime.UtcNow;
ctx.Update(masterDevice);
ctx.Remove(slaveDevice);
await ctx.SaveChangesAsync();
Id = master;
await RefreshItemsAsync();
}
async Task MergeReports(int deviceId, int reportId)
{
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
Device? device = await ctx.Devices.Include(static deviceReport => deviceReport.ATA)
.ThenInclude(static ata => ata.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.MultiMediaDevice)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.SequentialDevice)
.FirstOrDefaultAsync(m => m.Id == deviceId);
UploadedReport? report = await ctx.Reports.Include(static deviceReport => deviceReport.ATA)
.ThenInclude(static ata => ata.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.ReadCapabilities)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.MultiMediaDevice)
.Include(static deviceReport => deviceReport.SCSI)
.ThenInclude(static scsi => scsi.SequentialDevice)
.FirstOrDefaultAsync(m => m.Id == reportId);
if(device?.ATAId != null && device.ATAId != report?.ATAId)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => report != null && d.AtaId == report.ATAId))
{
testedMedia.AtaId = device.ATAId;
ctx.Update(testedMedia);
}
if(device.ATA is { ReadCapabilities: null } && report?.ATA?.ReadCapabilities != null)
{
device.ATA.ReadCapabilities = report.ATA.ReadCapabilities;
ctx.Update(device.ATA);
}
}
else if(device?.ATAId == null && report?.ATAId != null)
{
if(device != null)
{
device.ATAId = report.ATAId;
ctx.Update(device);
}
}
switch(device)
{
case { ATAPIId: not null } when device.ATAPIId != report?.ATAPIId:
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in
ctx.TestedMedia.Where(d => report != null && d.AtaId == report.ATAPIId))
{
testedMedia.AtaId = device.ATAPIId;
ctx.Update(testedMedia);
}
break;
}
case { ATAPIId: null } when report?.ATAPIId != null:
device.ATAPIId = report.ATAPIId;
ctx.Update(device);
break;
}
switch(device)
{
case { SCSIId: not null } when device.SCSIId != report?.SCSIId:
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in ctx.TestedMedia.Where(d => report != null &&
d.ScsiId == report.SCSIId))
{
testedMedia.ScsiId = device.SCSIId;
ctx.Update(testedMedia);
}
if(device.SCSI is { ReadCapabilities: null } && report?.SCSI?.ReadCapabilities != null)
{
device.SCSI.ReadCapabilities = report.SCSI.ReadCapabilities;
ctx.Update(device.SCSI);
}
if(device.SCSI is { MultiMediaDevice: null } && report?.SCSI?.MultiMediaDevice != null)
{
device.SCSI.MultiMediaDevice = report.SCSI.MultiMediaDevice;
ctx.Update(device.SCSI);
}
else if(device.SCSI?.MultiMediaDevice != null && report?.SCSI?.MultiMediaDevice != null)
{
foreach(CommonTypes.Metadata.TestedMedia testedMedia in ctx.TestedMedia.Where(d => d.MmcId ==
report.SCSI.MultiMediaDevice.Id))
{
testedMedia.MmcId = device.SCSI.MultiMediaDevice.Id;
ctx.Update(testedMedia);
}
}
if(device.SCSI is { SequentialDevice: null } && report?.SCSI?.SequentialDevice != null)
{
device.SCSI.SequentialDevice = report.SCSI.SequentialDevice;
ctx.Update(device.SCSI);
}
else if(device.SCSI?.SequentialDevice != null && report?.SCSI?.SequentialDevice != null)
{
foreach(TestedSequentialMedia testedSequentialMedia in
ctx.TestedSequentialMedia.Where(d => d.SscId == report.SCSI.SequentialDevice.Id))
{
testedSequentialMedia.SscId = device.SCSI.SequentialDevice.Id;
ctx.Update(testedSequentialMedia);
}
}
break;
}
case { SCSIId: null } when report?.SCSIId != null:
device.SCSIId = report.SCSIId;
ctx.Update(device);
break;
}
ctx.Remove(report);
await ctx.SaveChangesAsync();
Id = deviceId;
await RefreshItemsAsync();
}
async Task LinkReports(int deviceId, int statsId)
{
await using DbContext ctx = await DbContextFactory.CreateDbContextAsync();
Device? device = await ctx.Devices.FirstOrDefaultAsync(m => m.Id == deviceId);
DeviceStat? stat = await ctx.DeviceStats.FirstOrDefaultAsync(m => m.Id == statsId);
if(stat != null)
{
stat.Report = device;
ctx.Update(stat);
}
await ctx.SaveChangesAsync();
Id = deviceId;
await RefreshItemsAsync();
}
}