diff --git a/Aaru.Server.New/Aaru.Server.New.csproj b/Aaru.Server.New/Aaru.Server.New.csproj
index 9ce7942d..7a2232df 100644
--- a/Aaru.Server.New/Aaru.Server.New.csproj
+++ b/Aaru.Server.New/Aaru.Server.New.csproj
@@ -23,4 +23,8 @@
+
+
+
+
diff --git a/Aaru.Server.New/Components/Layout/NavMenu.razor b/Aaru.Server.New/Components/Layout/NavMenu.razor
index 7f973e97..7cc131de 100644
--- a/Aaru.Server.New/Components/Layout/NavMenu.razor
+++ b/Aaru.Server.New/Components/Layout/NavMenu.razor
@@ -20,6 +20,12 @@
+
+
+ Statistics
+
+
+
diff --git a/Aaru.Server.New/Components/Layout/NavMenu.razor.css b/Aaru.Server.New/Components/Layout/NavMenu.razor.css
index b9e7935d..203d235d 100644
--- a/Aaru.Server.New/Components/Layout/NavMenu.razor.css
+++ b/Aaru.Server.New/Components/Layout/NavMenu.razor.css
@@ -66,6 +66,10 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E");
}
+.bi-graph-up-nav-menu {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-graph-up' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M0 0h1v15h15v1H0zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07'/%3E%3C/svg%3E");
+}
+
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
diff --git a/Aaru.Server.New/Components/Pages/Stats.razor b/Aaru.Server.New/Components/Pages/Stats.razor
new file mode 100644
index 00000000..23f9c674
--- /dev/null
+++ b/Aaru.Server.New/Components/Pages/Stats.razor
@@ -0,0 +1,6 @@
+@page "/Stats"
+@using Aaru.Server.Database
+
+@inject Microsoft.EntityFrameworkCore.IDbContextFactory DbContextFactory
+
+Aaru: Statistics
\ No newline at end of file
diff --git a/Aaru.Server.New/Components/Pages/Stats.razor.cs b/Aaru.Server.New/Components/Pages/Stats.razor.cs
new file mode 100644
index 00000000..8f6f3e0c
--- /dev/null
+++ b/Aaru.Server.New/Components/Pages/Stats.razor.cs
@@ -0,0 +1,129 @@
+using Aaru.CommonTypes.Interop;
+using Aaru.CommonTypes.Metadata;
+using Aaru.Server.Database.Models;
+using Microsoft.EntityFrameworkCore;
+using DbContext = Aaru.Server.Database.DbContext;
+using PlatformID = Aaru.CommonTypes.Interop.PlatformID;
+
+namespace Aaru.Server.New.Components.Pages;
+
+public partial class Stats
+{
+ ///
+ protected override async Task OnInitializedAsync()
+ {
+ await base.OnInitializedAsync();
+
+ // TOOD: Cache real OS name in database, lookups would be much faster
+ await using DbContext _ctx = await DbContextFactory.CreateDbContextAsync();
+
+ var operatingSystems = (await _ctx.OperatingSystems.OrderBy(static os => os.Name)
+ .ThenBy(static os => os.Version)
+ .Select(static nvs => new NameValueStats
+ {
+ name =
+ $"{GetPlatformName(nvs.Name, nvs.Version)}{(string.IsNullOrEmpty(nvs.Version) ? "" : " ")}{nvs.Version}",
+ Value = nvs.Count
+ })
+ .ToListAsync()).OrderBy(static os => os.name)
+ .ToList();
+
+ var versions = (await _ctx.Versions.Select(static nvs => new NameValueStats
+ {
+ name = nvs.Name == "previous" ? "Previous than 3.4.99.0" : nvs.Name,
+ Value = nvs.Count
+ })
+ .ToListAsync()).OrderBy(static version => version.name)
+ .ToList();
+
+ List commands = await _ctx.Commands.OrderBy(static c => c.Name).ToListAsync();
+
+ List filters = await _ctx.Filters.OrderBy(static filter => filter.Name).ToListAsync();
+
+ List mediaImages = await _ctx.MediaFormats.OrderBy(static format => format.Name).ToListAsync();
+
+ List partitions = await _ctx.Partitions.OrderBy(static partition => partition.Name).ToListAsync();
+
+ List filesystems =
+ await _ctx.Filesystems.OrderBy(static filesystem => filesystem.Name).ToListAsync();
+
+ List realMedia = [];
+ List virtualMedia = [];
+
+ await foreach(Media nvs in _ctx.Medias.AsAsyncEnumerable())
+ {
+ try
+ {
+ (string type, string subType) mediaType =
+ MediaType.MediaTypeToString((CommonTypes.MediaType)Enum.Parse(typeof(CommonTypes.MediaType),
+ nvs.Type));
+
+ if(nvs.Real)
+ {
+ realMedia.Add(new MediaItem
+ {
+ Type = mediaType.type,
+ SubType = mediaType.subType,
+ Count = nvs.Count
+ });
+ }
+ else
+ {
+ virtualMedia.Add(new MediaItem
+ {
+ Type = mediaType.type,
+ SubType = mediaType.subType,
+ Count = nvs.Count
+ });
+ }
+ }
+ catch
+ {
+ if(nvs.Real)
+ {
+ realMedia.Add(new MediaItem
+ {
+ Type = nvs.Type,
+ SubType = null,
+ Count = nvs.Count
+ });
+ }
+ else
+ {
+ virtualMedia.Add(new MediaItem
+ {
+ Type = nvs.Type,
+ SubType = null,
+ Count = nvs.Count
+ });
+ }
+ }
+ }
+
+ realMedia = realMedia.OrderBy(static media => media.Type).ThenBy(static media => media.SubType).ToList();
+ virtualMedia = virtualMedia.OrderBy(static media => media.Type).ThenBy(static media => media.SubType).ToList();
+
+
+ List devices = await _ctx.DeviceStats.Include(static deviceStat => deviceStat.Report)
+ .Select(static device => new DeviceItem
+ {
+ Manufacturer = device.Manufacturer,
+ Model = device.Model,
+ Revision = device.Revision,
+ Bus = device.Bus,
+ ReportId = device.Report != null && device.Report.Id != 0
+ ? device.Report.Id
+ : 0
+ })
+ .ToListAsync();
+
+ devices = devices.OrderBy(static device => device.Manufacturer)
+ .ThenBy(static device => device.Model)
+ .ThenBy(static device => device.Revision)
+ .ThenBy(static device => device.Bus)
+ .ToList();
+ }
+
+ static string GetPlatformName(string name, string version) =>
+ DetectOS.GetPlatformName((PlatformID)Enum.Parse(typeof(PlatformID), name), version);
+}
\ No newline at end of file
diff --git a/Aaru.Server.New/Program.cs b/Aaru.Server.New/Program.cs
index c869a6cc..ac3daace 100644
--- a/Aaru.Server.New/Program.cs
+++ b/Aaru.Server.New/Program.cs
@@ -3,6 +3,7 @@ using Aaru.Server.New.Components.Account;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
+using DbContext = Aaru.Server.Database.DbContext;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
@@ -24,10 +25,10 @@ builder.Services.AddAuthentication(options =>
string connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
-builder.Services.AddDbContext(options => options
- .UseMySql(connectionString,
- new MariaDbServerVersion(new Version(10, 4, 0)))
- .UseLazyLoadingProxies());
+builder.Services.AddDbContextFactory(options => options
+ .UseMySql(connectionString,
+ new MariaDbServerVersion(new Version(10, 4, 0)))
+ .UseLazyLoadingProxies());
builder.Services.AddDatabaseDeveloperPageExceptionFilter();