From 54bca09d0b542c23ba0760bad48c49b7782819f0 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:41:35 -0400 Subject: [PATCH 01/27] Add .NET 10 migration build workflow --- .github/workflows/net10-migration-build.yml | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/net10-migration-build.yml diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml new file mode 100644 index 0000000..97b8a50 --- /dev/null +++ b/.github/workflows/net10-migration-build.yml @@ -0,0 +1,28 @@ +name: .NET 10 migration build + +on: + push: + branches: + - agent/net10-mudblazor-migration + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore TruthGate-IPFS.sln + + - name: Build + run: dotnet build TruthGate-IPFS.sln --configuration Release --no-restore From ee37edf1f436a7a019f91159aac827af84e22f7c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:41:51 -0400 Subject: [PATCH 02/27] Run migration build for pull requests --- .github/workflows/net10-migration-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 97b8a50..3aeda81 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -4,6 +4,9 @@ on: push: branches: - agent/net10-mudblazor-migration + pull_request: + branches: + - magiccodingman/net10 workflow_dispatch: permissions: From b7fdea3b07fdcda0678dab631d19a9dc29078433 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:42:26 -0400 Subject: [PATCH 03/27] Update CI validation for .NET 10 --- .github/workflows/tls-lifecycle.yml | 58 +++++++++-------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 714162a..2246ca2 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -3,17 +3,13 @@ name: TLS lifecycle tests on: pull_request: paths: - - "TruthGate-Web/TruthGate-Web/Configuration/**" - - "TruthGate-Web/TruthGate-Web/Services/ConfigWatchAndIssueService.cs" - - "TruthGate-Web/TruthGate-Web.Tests/**" + - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" push: branches: - master paths: - - "TruthGate-Web/TruthGate-Web/Configuration/**" - - "TruthGate-Web/TruthGate-Web/Services/ConfigWatchAndIssueService.cs" - - "TruthGate-Web/TruthGate-Web.Tests/**" + - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" jobs: @@ -25,37 +21,19 @@ jobs: - uses: actions/setup-dotnet@v4 with: - dotnet-version: "9.0.x" - - - name: Restore - run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj - - - name: Test - id: test - shell: bash - run: | - set +e - dotnet test \ - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ - --no-restore \ - --configuration Release \ - --logger "console;verbosity=normal" \ - > tls-test-output.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - exit 0 - - - name: Upload test diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: tls-test-output - path: tls-test-output.log - if-no-files-found: error - - - name: Report failure - if: steps.test.outputs.status != '0' - shell: bash - run: | - echo "TLS lifecycle tests failed. Download the tls-test-output artifact for the complete compiler/test diagnostics." - exit 1 + dotnet-version: "10.0.x" + + - name: Restore solution + run: dotnet restore TruthGate-IPFS.sln + + - name: Build solution + run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release + + - name: Test TLS lifecycle + run: >- + dotnet test + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + --no-restore + --no-build + --configuration Release + --logger "console;verbosity=normal" From 64159d2ff966dc32430ba37d2e7c0cc03b5db98e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:43:51 -0400 Subject: [PATCH 04/27] Capture complete .NET 10 build diagnostics --- .github/workflows/net10-migration-build.yml | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 3aeda81..22c0ba3 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -28,4 +28,24 @@ jobs: run: dotnet restore TruthGate-IPFS.sln - name: Build - run: dotnet build TruthGate-IPFS.sln --configuration Release --no-restore + id: build + shell: bash + run: | + set +e + dotnet build TruthGate-IPFS.sln --configuration Release --no-restore > net10-build.log 2>&1 + status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + tail -n 200 net10-build.log + exit 0 + + - name: Upload build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: net10-build-log + path: net10-build.log + if-no-files-found: error + + - name: Report build failure + if: steps.build.outputs.status != '0' + run: exit 1 From 2278b010ae11b6b27dc9a666f69672ef5ec9fdf6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:46:25 -0400 Subject: [PATCH 05/27] Migrate metrics charts to MudBlazor 9 --- .../Components/Shared/DashboardMetrics.razor | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor index c105377..f9982a0 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor @@ -77,10 +77,9 @@ CPU % (System vs Process) @@ -91,10 +90,9 @@ System Memory Used (GB) @@ -105,10 +103,9 @@ Process Memory (MB) · Working Set & GC Heap @@ -119,11 +116,10 @@ ThreadPool · Threads & Queue + ChartOptions="@_chartOptions" /> @@ -156,38 +152,35 @@ private int _detailedWindowSeconds = 30; // Detailed: last 30s private bool _rotateXLabels = true; - // ===== Axis cosmetics ===== - private readonly AxisChartOptions _axisOptions = new() - { - MatchBoundsToSize = true, - XAxisLabelRotation = 45 - }; - private readonly ChartOptions _chartOptions = new() - { - YAxisTicks = 8 - }; + // ===== Chart cosmetics ===== + private readonly LineChartOptions _chartOptions = new() + { + MatchBoundsToSize = true, + XAxisLabelRotation = 45, + YAxisTicks = 8 + }; // ===== Series & labels ===== private string[] _xLabels = Array.Empty(); - private readonly List _cpuSeries = new() + private readonly List> _cpuSeries = new() { - new ChartSeries { Name = "System CPU %", Data = Array.Empty() }, - new ChartSeries { Name = "Process CPU %", Data = Array.Empty() } + new ChartSeries { Name = "System CPU %", Data = Array.Empty().AsChartDataSet() }, + new ChartSeries { Name = "Process CPU %", Data = Array.Empty().AsChartDataSet() } }; - private readonly List _sysMemSeries = new() + private readonly List> _sysMemSeries = new() { - new ChartSeries { Name = "System Used (GB)", Data = Array.Empty() } + new ChartSeries { Name = "System Used (GB)", Data = Array.Empty().AsChartDataSet() } }; - private readonly List _procMemSeries = new() + private readonly List> _procMemSeries = new() { - new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty() }, - new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty() } + new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty().AsChartDataSet() }, + new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty().AsChartDataSet() } }; - private readonly List _tpSeries = new() + private readonly List> _tpSeries = new() { - new ChartSeries { Name = "Threads", Data = Array.Empty() }, - new ChartSeries { Name = "Queue Length", Data = Array.Empty() } + new ChartSeries { Name = "Threads", Data = Array.Empty().AsChartDataSet() }, + new ChartSeries { Name = "Queue Length", Data = Array.Empty().AsChartDataSet() } }; // ===== Now-cards ===== @@ -214,7 +207,7 @@ { while (await _timer!.WaitForNextTickAsync(_cts.Token)) { - _axisOptions.XAxisLabelRotation = _rotateXLabels ? 45 : 0; + _chartOptions.XAxisLabelRotation = _rotateXLabels ? 45 : 0; RebuildSeries(); await InvokeAsync(StateHasChanged); } @@ -251,16 +244,16 @@ _xLabels = BuildLabels(window, _isDetailed); // Series fill - _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray(); - _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray(); + _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray().AsChartDataSet(); + _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray().AsChartDataSet(); - _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray(); + _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray().AsChartDataSet(); - _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray(); - _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray(); + _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray().AsChartDataSet(); + _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray().AsChartDataSet(); - _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray(); - _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray(); + _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray().AsChartDataSet(); + _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray().AsChartDataSet(); } private static IReadOnlyList SliceDetailed(IReadOnlyList pts, int windowSeconds) From e260818921dcdabb56f5f0be7024693524f47013 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:50:13 -0400 Subject: [PATCH 06/27] Automate the next MudBlazor migration fixes --- .github/workflows/net10-migration-build.yml | 64 ++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 22c0ba3..0a2d80d 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write jobs: build: @@ -19,6 +19,68 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Apply compiler-driven migration fixes + if: github.event_name == 'push' && github.ref == 'refs/heads/agent/net10-mudblazor-migration' + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + def replace(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text(encoding="utf-8-sig") + if old in text: + file.write_text(text.replace(old, new), encoding="utf-8") + + dashboard = "TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor" + replace(dashboard, " MatchBoundsToSize = true,\n", "") + replace(dashboard, ".AsChartDataSet()", "") + + api_keys = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor" + replace(api_keys, + 'Dialogs.ShowAsync("Add API Key", p,', + 'Dialogs.ShowAsync("Add API Key", p,') + replace(api_keys, + "if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;", + "if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;") + replace(api_keys, + "if (result.Canceled || result.Data is not bool yes || !yes) return;", + "if (result is null || result.Canceled || result.Data is not bool yes || !yes) return;") + + ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" + replace(ip_bans, + "LoadBansGridAsync(GridState state)", + "LoadBansGridAsync(GridState state, CancellationToken cancellationToken)") + replace(ip_bans, + "LoadWhitelistIpGridAsync(GridState state)", + "LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken)") + replace(ip_bans, + "LoadWhitelistPrefixGridAsync(GridState state)", + "LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken)") + replace(ip_bans, "Dialogs.ShowMessageBox(", "Dialogs.ShowMessageBoxAsync(") + replace(ip_bans, + 'await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', + 'var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') + replace(ip_bans, + 'await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', + 'var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') + replace(ip_bans, + "DbFactory.CreateDbContextAsync();", + "DbFactory.CreateDbContextAsync(cancellationToken);") + replace(ip_bans, "q.CountAsync();", "q.CountAsync(cancellationToken);") + replace(ip_bans, ".ToListAsync();", ".ToListAsync(cancellationToken);") + PY + + if ! git diff --quiet; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor + git commit -m "Fix MudBlazor 9 chart, dialog, and grid APIs" + git push origin "HEAD:${GITHUB_REF_NAME}" + fi + - name: Setup .NET 10 uses: actions/setup-dotnet@v4 with: From df5c3b5a4ef03c5d9573147337712e9ea490c982 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:51:05 +0000 Subject: [PATCH 07/27] Fix MudBlazor 9 chart, dialog, and grid APIs --- .../Components/Pages/Settings/ApiKeys.razor | 8 ++--- .../Pages/Settings/IpBansPage.razor | 36 ++++++++++--------- .../Components/Shared/DashboardMetrics.razor | 31 ++++++++-------- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor index fedb91c..5d7aaf9 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor @@ -1,4 +1,4 @@ -@page "/keys" +@page "/keys" @using TruthGate_Web.Components.Pages.Settings.Shared @using TruthGate_Web.Models @using TruthGate_Web.Utils @@ -71,10 +71,10 @@ { x => x.ExistingNames, _keys.Select(k => k.Name).ToList() } }; - var dialog = await Dialogs.ShowAsync("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var dialog = await Dialogs.ShowAsync("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); var result = await dialog.Result; - if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; + if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; // Final uniqueness check (server-side) if (_keys.Any(k => string.Equals(k.Name, r.Name, StringComparison.OrdinalIgnoreCase))) @@ -106,7 +106,7 @@ var dialog = await Dialogs.ShowAsync("Confirm Delete", p); var result = await dialog.Result; - if (result.Canceled || result.Data is not bool yes || !yes) return; + if (result is null || result.Canceled || result.Data is not bool yes || !yes) return; await ConfigSvc.UpdateAsync(cfg => { diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index 1c4f95a..0ab5bce 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -1,4 +1,4 @@ -@page "/ip-bans" +@page "/ip-bans" @using Microsoft.EntityFrameworkCore @using System.Net @using TruthGate_Web.Components.Pages.Settings.Shared @@ -212,7 +212,7 @@ } private BanFilters _banFilters = new(); - private async Task> LoadBansGridAsync(GridState state) + private async Task> LoadBansGridAsync(GridState state, CancellationToken cancellationToken) { var page = state.Page + 1; // Mud uses 0-based; your API is 1-based var pageSize = state.PageSize; @@ -237,7 +237,7 @@ private async Task ConfirmUnbanById(Guid id) { - var res = await Dialogs.ShowMessageBox("Confirm", "Unban this entry?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", "Unban this entry?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanByIdAsync(id); @@ -248,7 +248,7 @@ private async Task ConfirmUnbanIp(string ip) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Unban IP {ip} ?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Unban IP {ip} ?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanIpAsync(ip); @@ -259,7 +259,7 @@ private async Task ConfirmUnbanPrefix(string pfx) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Unban prefix {pfx} ?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Unban prefix {pfx} ?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanIpv6PrefixAsync(pfx); @@ -272,13 +272,15 @@ private async Task OpenSearchDialog() { var parameters = new DialogParameters { { x => x.InitialIp, _banFilters.IpFilter } }; - await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + await dialog.Result; } // -------------------- ADD BAN DIALOG -------------------- private async Task OpenAddBanDialog() { - await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + await dialog.Result; await ReloadBans(); } @@ -289,9 +291,9 @@ private string? _newWhitelistReason; private int? _newWhitelistExpireHours; - private async Task> LoadWhitelistIpGridAsync(GridState state) + private async Task> LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken) { - await using var db = await DbFactory.CreateDbContextAsync(); + await using var db = await DbFactory.CreateDbContextAsync(cancellationToken); var q = db.Whitelists.AsNoTracking().Where(x => x.Ip != null); if (!string.IsNullOrWhiteSpace(_wlIpSearch)) @@ -300,12 +302,12 @@ q = q.Where(x => x.Ip!.Contains(s)); } - var total = await q.CountAsync(); + var total = await q.CountAsync(cancellationToken); var items = await q .OrderByDescending(x => x.CreatedUtc) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new GridData { Items = items, TotalItems = total }; } @@ -331,7 +333,7 @@ private async Task RemoveWhitelistIp(string ip) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Remove {ip} from whitelist?", yesText: "Remove", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Remove {ip} from whitelist?", yesText: "Remove", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.RemoveWhitelistIpAsync(ip); @@ -347,9 +349,9 @@ private string? _newWhitelistReasonPfx; private int? _newWhitelistExpireHoursPfx; - private async Task> LoadWhitelistPrefixGridAsync(GridState state) + private async Task> LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken) { - await using var db = await DbFactory.CreateDbContextAsync(); + await using var db = await DbFactory.CreateDbContextAsync(cancellationToken); var q = db.Whitelists.AsNoTracking().Where(x => x.Ipv6Prefix != null); if (!string.IsNullOrWhiteSpace(_wlPfxSearch)) @@ -358,12 +360,12 @@ q = q.Where(x => x.Ipv6Prefix!.Contains(s)); } - var total = await q.CountAsync(); + var total = await q.CountAsync(cancellationToken); var items = await q .OrderByDescending(x => x.CreatedUtc) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new GridData { Items = items, TotalItems = total }; } @@ -389,7 +391,7 @@ private async Task RemoveWhitelistPrefix(string pfx) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Remove {pfx} from whitelist?", yesText: "Remove", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Remove {pfx} from whitelist?", yesText: "Remove", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.RemoveWhitelistIpv6PrefixAsync(pfx); diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor index f9982a0..821cb77 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @using MudBlazor @using TruthGate_Web.Models.Metrics @* MetricPoint, MetricSnapshot, ThreadSpike *@ @inject IMetricService Metrics @@ -155,7 +155,6 @@ // ===== Chart cosmetics ===== private readonly LineChartOptions _chartOptions = new() { - MatchBoundsToSize = true, XAxisLabelRotation = 45, YAxisTicks = 8 }; @@ -165,22 +164,22 @@ private readonly List> _cpuSeries = new() { - new ChartSeries { Name = "System CPU %", Data = Array.Empty().AsChartDataSet() }, - new ChartSeries { Name = "Process CPU %", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "System CPU %", Data = Array.Empty() }, + new ChartSeries { Name = "Process CPU %", Data = Array.Empty() } }; private readonly List> _sysMemSeries = new() { - new ChartSeries { Name = "System Used (GB)", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "System Used (GB)", Data = Array.Empty() } }; private readonly List> _procMemSeries = new() { - new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty().AsChartDataSet() }, - new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty() }, + new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty() } }; private readonly List> _tpSeries = new() { - new ChartSeries { Name = "Threads", Data = Array.Empty().AsChartDataSet() }, - new ChartSeries { Name = "Queue Length", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "Threads", Data = Array.Empty() }, + new ChartSeries { Name = "Queue Length", Data = Array.Empty() } }; // ===== Now-cards ===== @@ -244,16 +243,16 @@ _xLabels = BuildLabels(window, _isDetailed); // Series fill - _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray().AsChartDataSet(); - _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray().AsChartDataSet(); + _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray(); + _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray(); - _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray().AsChartDataSet(); + _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray(); - _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray().AsChartDataSet(); - _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray().AsChartDataSet(); + _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray(); + _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray(); - _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray().AsChartDataSet(); - _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray().AsChartDataSet(); + _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray(); + _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray(); } private static IReadOnlyList SliceDetailed(IReadOnlyList pts, int windowSeconds) From d86c43f03e705e4afa63c2a7b085c96e225dfaf2 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:52:19 -0400 Subject: [PATCH 08/27] Run migration patches against the repair branch --- .github/workflows/net10-migration-build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 0a2d80d..b536096 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -16,11 +16,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout repair branch uses: actions/checkout@v4 + with: + ref: agent/net10-mudblazor-migration - name: Apply compiler-driven migration fixes - if: github.event_name == 'push' && github.ref == 'refs/heads/agent/net10-mudblazor-migration' shell: bash run: | python3 <<'PY' @@ -78,7 +79,7 @@ jobs: TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor git commit -m "Fix MudBlazor 9 chart, dialog, and grid APIs" - git push origin "HEAD:${GITHUB_REF_NAME}" + git push origin HEAD:agent/net10-mudblazor-migration fi - name: Setup .NET 10 From aff0a368b59d44d3c81da7fef1db5e7e85d0a32c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:54:02 -0400 Subject: [PATCH 09/27] Capture complete .NET 10 test diagnostics --- .github/workflows/tls-lifecycle.yml | 34 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 2246ca2..e458f25 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -30,10 +30,30 @@ jobs: run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release - name: Test TLS lifecycle - run: >- - dotnet test - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj - --no-restore - --no-build - --configuration Release - --logger "console;verbosity=normal" + id: test + shell: bash + run: | + set +e + dotnet test \ + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ + --no-restore \ + --no-build \ + --configuration Release \ + --logger "console;verbosity=normal" \ + > tls-test-output.log 2>&1 + status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + tail -n 200 tls-test-output.log + exit 0 + + - name: Upload test diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: tls-test-output + path: tls-test-output.log + if-no-files-found: error + + - name: Report test failure + if: steps.test.outputs.status != '0' + run: exit 1 From 081c4539f797e0d995da9f8172d12a7063736ffa Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:55:19 -0400 Subject: [PATCH 10/27] Retarget TLS tests to .NET 10 --- TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj index 69e4f2d..825fa90 100644 --- a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj +++ b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable false From 00ef468a4d02182c1d6f8a53d31db96f9dccc3dc Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:56:38 -0400 Subject: [PATCH 11/27] Migrate domain backup file upload to MudBlazor 9 --- .../Settings/Shared/ImportDomainDialog.razor | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor index dfd94d7..3a6e7da 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor @@ -14,15 +14,17 @@ Immediate="true" Required="true" /> -
- - +
+ + + StartIcon="@Icons.Material.Filled.VpnKey" + OnClick="@context.OpenFilePickerAsync"> Select Backup (.tgbackup) -
+ +
@@ -43,8 +45,8 @@ private IBrowserFile? _file; private string? _pass; - private void OnFileChanged(IReadOnlyList picked) - => _file = picked.FirstOrDefault(); + private void OnFileChanged(IBrowserFile picked) + => _file = picked; private async Task Submit() { From 74cbeb3124751acd2fb2ff2fa81945afe5c44b4f Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:57:11 -0400 Subject: [PATCH 12/27] Apply remaining MudBlazor 9 component migrations --- .github/workflows/net10-migration-build.yml | 142 +++++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index b536096..409a556 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -59,6 +59,7 @@ jobs: "LoadWhitelistPrefixGridAsync(GridState state)", "LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken)") replace(ip_bans, "Dialogs.ShowMessageBox(", "Dialogs.ShowMessageBoxAsync(") + replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') replace(ip_bans, 'await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', 'var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') @@ -70,6 +71,138 @@ jobs: "DbFactory.CreateDbContextAsync(cancellationToken);") replace(ip_bans, "q.CountAsync();", "q.CountAsync(cancellationToken);") replace(ip_bans, ".ToListAsync();", ".ToListAsync(cancellationToken);") + + simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" + replace(simple_prompt, '', '') + replace(simple_prompt, + 'Required="@Required"\n Lines=', + 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=') + replace(simple_prompt, + ''' private IEnumerable Validate() + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(_value)) + { + _error = "Please enter a value."; + yield return _error; + } + } +''', + ''' private string? ValidateValue(string? value) + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; + } +''') + replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") + + add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" + replace(add_pinned, + '', + '') + + add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" + replace(add_ipns, + '', + '') + replace(add_ipns, + 'Required="true"\n Disabled="_isEdit" />', + 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', + ) + # The second required/disabled field is the IPNS key; correct its validator after the global replacement. + replace(add_ipns, + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"') + replace(add_ipns, + ''' private IEnumerable Validate() + { + _error = null; + + if (string.IsNullOrWhiteSpace(_model.Name)) + yield return "Name is required."; + if (string.IsNullOrWhiteSpace(_model.Key)) + yield return "IPNS key is required."; + + var leaf = IpfsGateway.ToSafeLeaf(_model.Name); + if (leaf is null) + yield return "Invalid name."; + if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) + yield return "IPNS Key should look like k51… or /ipns/k51…"; + } +''', + ''' private string? ValidateName(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; + + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } + + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; + } +''') + replace(add_ipns, "private async void Submit()", "private async Task Submit()") + replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") + replace(add_ipns, + '', + '') + + search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" + replace(search_ip, '', '') + replace(search_ip, + 'Required="true"\n @onkeydown=', + 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=') + replace(search_ip, + ''' private IEnumerable Validate() + { + _error = null; + if (string.IsNullOrWhiteSpace(_ip)) + { + _error = "Please enter an IP address."; + yield return _error; + yield break; + } + if (!IPAddress.TryParse(_ip, out _)) + { + _error = "Invalid IP format (v4 or v6 required)."; + yield return _error; + } + } +''', + ''' private string? ValidateIp(string? value) + { + _error = null; + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; + } +''') + replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") + replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') + + domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" + replace(domains, 'Class="truncate-text" Title="@context.RedirectUrl"', 'Class="truncate-text" title="@context.RedirectUrl"') + replace(domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"') + replace(domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"') PY if ! git diff --quiet; then @@ -77,8 +210,13 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor \ TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor - git commit -m "Fix MudBlazor 9 chart, dialog, and grid APIs" + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor \ + TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor + git commit -m "Finish MudBlazor 9 form and component migrations" git push origin HEAD:agent/net10-mudblazor-migration fi From c7986e890273f629cd050756866a223f1b487e88 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:58:18 -0400 Subject: [PATCH 13/27] Add temporary compiler-driven migration script --- .github/scripts/apply-net10-migrations.py | 170 ++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/scripts/apply-net10-migrations.py diff --git a/.github/scripts/apply-net10-migrations.py b/.github/scripts/apply-net10-migrations.py new file mode 100644 index 0000000..957ec3d --- /dev/null +++ b/.github/scripts/apply-net10-migrations.py @@ -0,0 +1,170 @@ +from pathlib import Path + + +def replace(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text(encoding="utf-8-sig") + if old in text: + file.write_text(text.replace(old, new), encoding="utf-8") + + +simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" +replace(simple_prompt, '', '') +replace( + simple_prompt, + 'Required="@Required"\n Lines=', + 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=', +) +replace( + simple_prompt, + ''' private IEnumerable Validate() + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(_value)) + { + _error = "Please enter a value."; + yield return _error; + } + } +''', + ''' private string? ValidateValue(string? value) + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; + } +''', +) +replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") + +add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" +replace( + add_pinned, + '', + '', +) + +add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" +replace( + add_ipns, + '', + '', +) +replace( + add_ipns, + 'Required="true"\n Disabled="_isEdit" />', + 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', +) +replace( + add_ipns, + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"', +) +replace( + add_ipns, + ''' private IEnumerable Validate() + { + _error = null; + + if (string.IsNullOrWhiteSpace(_model.Name)) + yield return "Name is required."; + if (string.IsNullOrWhiteSpace(_model.Key)) + yield return "IPNS key is required."; + + var leaf = IpfsGateway.ToSafeLeaf(_model.Name); + if (leaf is null) + yield return "Invalid name."; + if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) + yield return "IPNS Key should look like k51… or /ipns/k51…"; + } +''', + ''' private string? ValidateName(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; + + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } + + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; + } +''', +) +replace(add_ipns, "private async void Submit()", "private async Task Submit()") +replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") +replace( + add_ipns, + '', + '', +) + +search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" +replace(search_ip, '', '') +replace( + search_ip, + 'Required="true"\n @onkeydown=', + 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=', +) +replace( + search_ip, + ''' private IEnumerable Validate() + { + _error = null; + if (string.IsNullOrWhiteSpace(_ip)) + { + _error = "Please enter an IP address."; + yield return _error; + yield break; + } + if (!IPAddress.TryParse(_ip, out _)) + { + _error = "Invalid IP format (v4 or v6 required)."; + yield return _error; + } + } +''', + ''' private string? ValidateIp(string? value) + { + _error = null; + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; + } +''', +) +replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") +replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') + +ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" +replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') + +domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" +replace( + domains, + 'Class="truncate-text" Title="@context.RedirectUrl"', + 'Class="truncate-text" title="@context.RedirectUrl"', +) +replace( + domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"', +) +replace( + domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"', +) From 1a6726c6cd92c2c71cff38baac7ef4c98f4fe1f5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:58:28 -0400 Subject: [PATCH 14/27] Run migration fixes from a standalone script --- .github/workflows/net10-migration-build.yml | 191 +------------------- 1 file changed, 2 insertions(+), 189 deletions(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 409a556..fcdf3f0 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -24,198 +24,11 @@ jobs: - name: Apply compiler-driven migration fixes shell: bash run: | - python3 <<'PY' - from pathlib import Path - - def replace(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text(encoding="utf-8-sig") - if old in text: - file.write_text(text.replace(old, new), encoding="utf-8") - - dashboard = "TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor" - replace(dashboard, " MatchBoundsToSize = true,\n", "") - replace(dashboard, ".AsChartDataSet()", "") - - api_keys = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor" - replace(api_keys, - 'Dialogs.ShowAsync("Add API Key", p,', - 'Dialogs.ShowAsync("Add API Key", p,') - replace(api_keys, - "if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;", - "if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;") - replace(api_keys, - "if (result.Canceled || result.Data is not bool yes || !yes) return;", - "if (result is null || result.Canceled || result.Data is not bool yes || !yes) return;") - - ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" - replace(ip_bans, - "LoadBansGridAsync(GridState state)", - "LoadBansGridAsync(GridState state, CancellationToken cancellationToken)") - replace(ip_bans, - "LoadWhitelistIpGridAsync(GridState state)", - "LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken)") - replace(ip_bans, - "LoadWhitelistPrefixGridAsync(GridState state)", - "LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken)") - replace(ip_bans, "Dialogs.ShowMessageBox(", "Dialogs.ShowMessageBoxAsync(") - replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') - replace(ip_bans, - 'await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', - 'var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') - replace(ip_bans, - 'await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', - 'var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') - replace(ip_bans, - "DbFactory.CreateDbContextAsync();", - "DbFactory.CreateDbContextAsync(cancellationToken);") - replace(ip_bans, "q.CountAsync();", "q.CountAsync(cancellationToken);") - replace(ip_bans, ".ToListAsync();", ".ToListAsync(cancellationToken);") - - simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" - replace(simple_prompt, '', '') - replace(simple_prompt, - 'Required="@Required"\n Lines=', - 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=') - replace(simple_prompt, - ''' private IEnumerable Validate() - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } - } -''', - ''' private string? ValidateValue(string? value) - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(value)) - return _error = "Please enter a value."; - - return null; - } -''') - replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") - - add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" - replace(add_pinned, - '', - '') - - add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" - replace(add_ipns, - '', - '') - replace(add_ipns, - 'Required="true"\n Disabled="_isEdit" />', - 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', - ) - # The second required/disabled field is the IPNS key; correct its validator after the global replacement. - replace(add_ipns, - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"') - replace(add_ipns, - ''' private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; - - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; - } -''', - ''' private string? ValidateName(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "Name is required."; - - return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; - } - - private static string? ValidateKey(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "IPNS key is required."; - - return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || - value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) - ? null - : "IPNS Key should look like k51… or /ipns/k51…"; - } -''') - replace(add_ipns, "private async void Submit()", "private async Task Submit()") - replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") - replace(add_ipns, - '', - '') - - search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" - replace(search_ip, '', '') - replace(search_ip, - 'Required="true"\n @onkeydown=', - 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=') - replace(search_ip, - ''' private IEnumerable Validate() - { - _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } - } -''', - ''' private string? ValidateIp(string? value) - { - _error = null; - if (string.IsNullOrWhiteSpace(value)) - return _error = "Please enter an IP address."; - - if (!IPAddress.TryParse(value, out _)) - return _error = "Invalid IP format (v4 or v6 required)."; - - return null; - } -''') - replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") - replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') - - domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" - replace(domains, 'Class="truncate-text" Title="@context.RedirectUrl"', 'Class="truncate-text" title="@context.RedirectUrl"') - replace(domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"') - replace(domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"') - PY - + python3 .github/scripts/apply-net10-migrations.py if ! git diff --quiet; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor \ - TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor + git add TruthGate-Web git commit -m "Finish MudBlazor 9 form and component migrations" git push origin HEAD:agent/net10-mudblazor-migration fi From db4d5c87fa7715169415d1e80f5f04648763f246 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:58:38 +0000 Subject: [PATCH 15/27] Finish MudBlazor 9 form and component migrations --- .../Components/Pages/Settings/Domains.razor | 8 ++-- .../Pages/Settings/IpBansPage.razor | 6 +-- .../Settings/Shared/SearchIpDialog.razor | 28 ++++++-------- .../Shared/AddOrEditIpnsDialog.razor | 37 ++++++++++--------- .../Shared/AddPinnedItemDialog.razor | 4 +- .../Shared/SimplePromptDialog.razor | 17 ++++----- 6 files changed, 49 insertions(+), 51 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor index daa683b..ffe2c93 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor @@ -1,4 +1,4 @@ -@page "/domains" +@page "/domains" @using System.Globalization @using System.Text.RegularExpressions @using MudBlazor @@ -47,7 +47,7 @@ @context.Domain @if (!string.IsNullOrWhiteSpace(context.RedirectUrl)) { - + @{ var u = context.RedirectUrl ?? string.Empty; } @@ -60,7 +60,7 @@ - + @($"{GetIpnsHost(context, useShort: true)}") @@ -88,7 +88,7 @@ @context.IpnsKeyName *@ - + IPNS: @Short(context.IpnsPeerId) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index 0ab5bce..acb3adc 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -52,7 +52,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -118,7 +118,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -164,7 +164,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor index 997c8b8..142514b 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor @@ -1,4 +1,4 @@ -@using System.Net +@using System.Net @using Microsoft.EntityFrameworkCore @using MudBlazor @using TruthGate_Web.Security @@ -11,7 +11,7 @@ @Message } - + Recent ban records (DB, latest 10) - + @context.Item.CreatedUtc.ToString("u") @@ -106,25 +106,21 @@ _ip = InitialIp?.Trim() ?? ""; } - private IEnumerable Validate() + private string? ValidateIp(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; } private async Task RunSearch() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; try diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor index 11bd822..6f20b04 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor @@ -1,4 +1,4 @@ -@using MudBlazor +@using MudBlazor @using System.Text.Json @using TruthGate_Web.Utils @using TruthGate_Web.Endpoints @@ -6,7 +6,7 @@ - + - +
@@ -60,7 +60,7 @@ - +
@@ -193,20 +193,23 @@ _model.KeepOldCidPinned = InitialKeepOldCidPinned; } - private IEnumerable Validate() + private string? ValidateName(string? value) { - _error = null; + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; } private void Cancel() => MudDialog.Cancel(); @@ -293,9 +296,9 @@ return null; } - private async void Submit() + private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; // Name leaf (immutable on edit) diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor index b473c89..76207eb 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor @@ -1,9 +1,9 @@ -@using MudBlazor +@using MudBlazor @using TruthGate_Web.Utils - + Add Pinned Item @@ -7,7 +7,7 @@ @Message } - + Validate() + private string? ValidateValue(string? value) { _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; } private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; MudDialog.Close(DialogResult.Ok(_value)); } From 190af2f24a6460d4674020d54ea8f1510aa1f25c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:01:14 -0400 Subject: [PATCH 16/27] Update .NET 10 test host packages --- .../TruthGate-Web.Tests/TruthGate-Web.Tests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj index 825fa90..4b520df 100644 --- a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj +++ b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj @@ -9,9 +9,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all From 19b9840f76824ed343b0ec3be57a2701590e1c52 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:03:25 -0400 Subject: [PATCH 17/27] Add .NET 10 test-host diagnostics --- .github/workflows/tls-lifecycle.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index e458f25..3f26f46 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -37,12 +37,14 @@ jobs: dotnet test \ TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ --no-restore \ - --no-build \ --configuration Release \ --logger "console;verbosity=normal" \ + --diag tls-vstest-diag.log \ > tls-test-output.log 2>&1 status=$? echo "status=$status" >> "$GITHUB_OUTPUT" + echo "Test output directory:" + find TruthGate-Web/TruthGate-Web.Tests/bin/Release/net10.0 -maxdepth 1 -type f -printf '%f\n' | sort tail -n 200 tls-test-output.log exit 0 @@ -51,7 +53,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: tls-test-output - path: tls-test-output.log + path: | + tls-test-output.log + tls-vstest-diag.log if-no-files-found: error - name: Report test failure From cbc586d7037b3c33577caec9034f8d3fa1678e1e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:05:31 -0400 Subject: [PATCH 18/27] Restore TLS test project explicitly --- .github/workflows/tls-lifecycle.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 3f26f46..9b1e657 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -26,6 +26,9 @@ jobs: - name: Restore solution run: dotnet restore TruthGate-IPFS.sln + - name: Restore TLS tests + run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + - name: Build solution run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release @@ -43,8 +46,6 @@ jobs: > tls-test-output.log 2>&1 status=$? echo "status=$status" >> "$GITHUB_OUTPUT" - echo "Test output directory:" - find TruthGate-Web/TruthGate-Web.Tests/bin/Release/net10.0 -maxdepth 1 -type f -printf '%f\n' | sort tail -n 200 tls-test-output.log exit 0 From cf0090430ec72bfc3063e7cfa3c7073d0194bcb5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:08:59 -0400 Subject: [PATCH 19/27] Migrate add-ban validation to async MudForm APIs --- .../Pages/Settings/Shared/AddBanDialog.razor | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor index 45e4ca4..55d30b2 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor @@ -10,7 +10,7 @@ @Message } - + @@ -19,6 +19,7 @@ Placeholder="@(_usePrefix ? "2001:db8::/64" : "203.0.113.42 or ::ffff:203.0.113.42")" Immediate="true" Required="true" + Validation="@(new Func(ValidateTarget))" @onkeydown="OnKeyDown" /> @@ -30,7 +31,13 @@ - + Validate() + private string? ValidateTarget(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_target)) - { - _error = "Please provide a target."; - yield return _error!; - yield break; - } - - var t = _target.Trim(); + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please provide a target."; + var target = value.Trim(); if (_usePrefix) { - // Accept "2001:db8::" or "2001:db8::/64" — the service canonicalizes either - var bare = t.Contains('/') ? t[..t.IndexOf('/')] : t; - if (!IPAddress.TryParse(bare, out var parsed) || parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + // Accept "2001:db8::" or "2001:db8::/64" — the service canonicalizes either. + var slash = target.IndexOf('/'); + var bare = slash >= 0 ? target[..slash] : target; + if (!IPAddress.TryParse(bare, out var parsed) || + parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) { - _error = "Invalid IPv6 prefix format."; - yield return _error!; + return _error = "Invalid IPv6 prefix format."; } } - else + else if (!IPAddress.TryParse(target, out _)) { - if (!IPAddress.TryParse(t, out _)) - { - _error = "Invalid IP address."; - yield return _error!; - } + return _error = "Invalid IP address."; } - if (_durationMinutes <= 0) - { - _error = "Duration must be at least 1 minute."; - yield return _error!; - } + return null; + } + + private string? ValidateDuration(int value) + { + if (value <= 0) + return _error = "Duration must be at least 1 minute."; + + return null; } private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; try { - var t = _target!.Trim(); - var dur = TimeSpan.FromMinutes(_durationMinutes); - var reason = string.IsNullOrWhiteSpace(_reason) ? (_usePrefix ? "MANUAL_PREFIX_BAN" : "MANUAL_BAN") : _reason; - - bool ok = _usePrefix - ? await RateSvc.BanIpv6PrefixAsync(t, dur, _scope, _trueBan, reason) - : await RateSvc.BanIpAsync(t, dur, _scope, _trueBan, reason); + var target = _target!.Trim(); + var duration = TimeSpan.FromMinutes(_durationMinutes); + var reason = string.IsNullOrWhiteSpace(_reason) + ? (_usePrefix ? "MANUAL_PREFIX_BAN" : "MANUAL_BAN") + : _reason; + + var ok = _usePrefix + ? await RateSvc.BanIpv6PrefixAsync(target, duration, _scope, _trueBan, reason) + : await RateSvc.BanIpAsync(target, duration, _scope, _trueBan, reason); if (ok) { From 362294cce97647de0a1088ce376ce259a5aa61cb Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:11 -0400 Subject: [PATCH 20/27] Migrate add-user validation to async MudForm APIs --- .../Pages/Settings/Shared/AddUserDialog.razor | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor index 53e1c30..8e2691c 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor @@ -2,10 +2,24 @@ - - - - + + + + @if (!string.IsNullOrEmpty(_error)) { @_error @@ -29,7 +43,7 @@ } private MudForm _form = default!; - private Model _model = new(); + private readonly Model _model = new(); private string? _error; private class Model @@ -39,34 +53,33 @@ public string Confirm { get; set; } = ""; } - private IEnumerable Validate() + private string? ValidateConfirmation(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_model.UserName)) - yield return "Username is required."; + if (string.IsNullOrEmpty(value)) + return _error = "Please confirm the password."; - if (string.IsNullOrEmpty(_model.Password)) - yield return "Password is required."; + if (_model.Password != value) + return _error = "Passwords must match."; - if (_model.Password != _model.Confirm) - yield return "Passwords must match."; + return null; } private void Cancel() => MudDialog.Cancel(); private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; - // normalize username to lowercase (your Config also normalizes) + // Normalize username to lowercase (Config also normalizes it). var result = new AddUserResult - { - UserName = _model.UserName.Trim().ToLowerInvariant(), - Password = _model.Password - }; + { + UserName = _model.UserName.Trim().ToLowerInvariant(), + Password = _model.Password + }; MudDialog.Close(DialogResult.Ok(result)); } -} \ No newline at end of file +} From baec649ae732facc4453bcc4a4ae596b546329e4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:24 -0400 Subject: [PATCH 21/27] Migrate password validation to async MudForm APIs --- .../Shared/ChangePasswordDialog.razor | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor index 07ba96d..06383f3 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor @@ -2,10 +2,20 @@ - + User: @TargetUserName - - + + @if (!string.IsNullOrEmpty(_error)) { @_error @@ -29,7 +39,7 @@ } private MudForm _form = default!; - private Model _model = new(); + private readonly Model _model = new(); private string? _error; private class Model @@ -38,25 +48,27 @@ public string Confirm { get; set; } = ""; } - private IEnumerable Validate() + private string? ValidateConfirmation(string? value) { _error = null; - if (string.IsNullOrEmpty(_model.Password)) - yield return "Password is required."; + if (string.IsNullOrEmpty(value)) + return _error = "Please confirm the password."; - if (_model.Password != _model.Confirm) - yield return "Passwords must match."; + if (_model.Password != value) + return _error = "Passwords must match."; + + return null; } private void Cancel() => MudDialog.Cancel(); private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; var result = new ChangePasswordResult { NewPassword = _model.Password }; MudDialog.Close(DialogResult.Ok(result)); } -} \ No newline at end of file +} From 9018affba1f98230f0f635efd267b91b82174ac3 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:38 -0400 Subject: [PATCH 22/27] Migrate domain validation to async MudForm APIs --- .../Shared/AddOrEditDomainDialog.razor | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor index ceeb101..d57b477 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor @@ -3,20 +3,20 @@ - + + @bind-Value="_model.Domain" + Immediate="true" + Required="true" + RequiredError="Domain is required." + For="@(() => _model.Domain)" />
+ @bind-Value="_model.UseSSL" + Color="Color.Primary" + Label="@(_model.UseSSL ? "Enabled" : "Disabled")" />
When enabled, TruthGate provisions a valid Let’s Encrypt certificate for this domain. @@ -25,16 +25,15 @@
+ @bind-Value="_model.RedirectUrl" + Immediate="true" + Required="false" + For="@(() => _model.RedirectUrl)" /> Leave empty to serve this domain normally. If a Redirect URL is set, all visitors will be instantly redirected there instead. - @if (!string.IsNullOrEmpty(_error)) { @_error @@ -85,14 +84,6 @@ public string? RedirectUrl { get; set; } } - private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Domain)) - yield return "Domain is required."; - } - private void Cancel() => MudDialog.Cancel(); [Inject] IHttpClientFactory HttpFactory { get; set; } = default!; @@ -100,38 +91,46 @@ private static string ToSafeFolderLeaf(string input) { - var s = (input ?? "").Trim().ToLowerInvariant(); - // kill pathy stuff — you only want a single folder leaf here - s = s.Replace('\\', '/').Trim('/'); - if (s.Contains("..")) throw new InvalidOperationException("Invalid domain."); - if (string.IsNullOrWhiteSpace(s)) throw new InvalidOperationException("Empty domain."); - return s; + var value = (input ?? "").Trim().ToLowerInvariant(); + // Kill path-like input — this must remain a single folder leaf. + value = value.Replace('\\', '/').Trim('/'); + if (value.Contains("..")) throw new InvalidOperationException("Invalid domain."); + if (string.IsNullOrWhiteSpace(value)) throw new InvalidOperationException("Empty domain."); + return value; } private async Task Submit() { - await _form.Validate(); + _error = null; + await _form.ValidateAsync(); if (!_form.IsValid) return; - var domainLeaf = ToSafeFolderLeaf(_model.Domain); - var basePath = "/production/sites"; - var fullPath = $"{basePath}/{domainLeaf}"; + try + { + var domainLeaf = ToSafeFolderLeaf(_model.Domain); + const string basePath = "/production/sites"; + var fullPath = $"{basePath}/{domainLeaf}"; - // Ensure the folder path exists (creates parents as needed), returns final CID - var cid = await IpfsGateway.EnsureMfsFolderExistsAsync(fullPath, HttpFactory); + // Ensure the folder path exists (creates parents as needed), returning the final CID. + _ = await IpfsGateway.EnsureMfsFolderExistsAsync(fullPath, HttpFactory); - // (Optional) warm/refresh your cached CID for this MFS path, so the rest of your app sees it: - var ttl = TimeSpan.FromHours(2); - _ = await IpfsGateway.GetCidForMfsPathAsync( + // Warm/refresh the cached CID so the rest of the app sees the newly created path. + var ttl = TimeSpan.FromHours(2); + _ = await IpfsGateway.GetCidForMfsPathAsync( fullPath, HttpFactory, Cache, ttl, IpfsGateway.CacheMode.Refresh); - var result = new DomainResult + var result = new DomainResult + { + Domain = _model.Domain, // Normalization happens on the page. + UseSSL = _model.UseSSL, + RedirectUrl = _model.RedirectUrl + }; + + MudDialog.Close(DialogResult.Ok(result)); + } + catch (Exception ex) { - Domain = _model.Domain, // normalization happens on the page - UseSSL = _model.UseSSL, - RedirectUrl = _model.RedirectUrl - }; - - MudDialog.Close(DialogResult.Ok(result)); + _error = ex.Message; + } } } From 10596be23e120b7ac7354088e9ffa1a300d24a7e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:10:27 -0400 Subject: [PATCH 23/27] Pin patched SQLite native bundle --- TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index e416567..d18a0fb 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -22,6 +22,7 @@ +
From 0b3fb9a01abf5d72d3d4f63e6d1f0e7d949b4171 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:12:08 -0400 Subject: [PATCH 24/27] Finalize .NET 10 TLS validation workflow --- .github/workflows/tls-lifecycle.yml | 35 +++++------------------------ 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 9b1e657..386d0bf 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -33,32 +33,9 @@ jobs: run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release - name: Test TLS lifecycle - id: test - shell: bash - run: | - set +e - dotnet test \ - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ - --no-restore \ - --configuration Release \ - --logger "console;verbosity=normal" \ - --diag tls-vstest-diag.log \ - > tls-test-output.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - tail -n 200 tls-test-output.log - exit 0 - - - name: Upload test diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: tls-test-output - path: | - tls-test-output.log - tls-vstest-diag.log - if-no-files-found: error - - - name: Report test failure - if: steps.test.outputs.status != '0' - run: exit 1 + run: >- + dotnet test + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + --no-restore + --configuration Release + --logger "console;verbosity=normal" From 2b4b2e050dba4b86abb15be4df5a22d37283a93e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:13:14 -0400 Subject: [PATCH 25/27] Remove temporary migration build workflow --- .github/workflows/net10-migration-build.yml | 65 --------------------- 1 file changed, 65 deletions(-) delete mode 100644 .github/workflows/net10-migration-build.yml diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml deleted file mode 100644 index fcdf3f0..0000000 --- a/.github/workflows/net10-migration-build.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: .NET 10 migration build - -on: - push: - branches: - - agent/net10-mudblazor-migration - pull_request: - branches: - - magiccodingman/net10 - workflow_dispatch: - -permissions: - contents: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repair branch - uses: actions/checkout@v4 - with: - ref: agent/net10-mudblazor-migration - - - name: Apply compiler-driven migration fixes - shell: bash - run: | - python3 .github/scripts/apply-net10-migrations.py - if ! git diff --quiet; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add TruthGate-Web - git commit -m "Finish MudBlazor 9 form and component migrations" - git push origin HEAD:agent/net10-mudblazor-migration - fi - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - - name: Restore - run: dotnet restore TruthGate-IPFS.sln - - - name: Build - id: build - shell: bash - run: | - set +e - dotnet build TruthGate-IPFS.sln --configuration Release --no-restore > net10-build.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - tail -n 200 net10-build.log - exit 0 - - - name: Upload build diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: net10-build-log - path: net10-build.log - if-no-files-found: error - - - name: Report build failure - if: steps.build.outputs.status != '0' - run: exit 1 From c36e6a575e34253f905d1c6e4a66aff087cb036b Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:13:20 -0400 Subject: [PATCH 26/27] Remove temporary migration helper script --- .github/scripts/apply-net10-migrations.py | 170 ---------------------- 1 file changed, 170 deletions(-) delete mode 100644 .github/scripts/apply-net10-migrations.py diff --git a/.github/scripts/apply-net10-migrations.py b/.github/scripts/apply-net10-migrations.py deleted file mode 100644 index 957ec3d..0000000 --- a/.github/scripts/apply-net10-migrations.py +++ /dev/null @@ -1,170 +0,0 @@ -from pathlib import Path - - -def replace(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text(encoding="utf-8-sig") - if old in text: - file.write_text(text.replace(old, new), encoding="utf-8") - - -simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" -replace(simple_prompt, '', '') -replace( - simple_prompt, - 'Required="@Required"\n Lines=', - 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=', -) -replace( - simple_prompt, - ''' private IEnumerable Validate() - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } - } -''', - ''' private string? ValidateValue(string? value) - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(value)) - return _error = "Please enter a value."; - - return null; - } -''', -) -replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") - -add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" -replace( - add_pinned, - '', - '', -) - -add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" -replace( - add_ipns, - '', - '', -) -replace( - add_ipns, - 'Required="true"\n Disabled="_isEdit" />', - 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', -) -replace( - add_ipns, - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"', -) -replace( - add_ipns, - ''' private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; - - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; - } -''', - ''' private string? ValidateName(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "Name is required."; - - return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; - } - - private static string? ValidateKey(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "IPNS key is required."; - - return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || - value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) - ? null - : "IPNS Key should look like k51… or /ipns/k51…"; - } -''', -) -replace(add_ipns, "private async void Submit()", "private async Task Submit()") -replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") -replace( - add_ipns, - '', - '', -) - -search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" -replace(search_ip, '', '') -replace( - search_ip, - 'Required="true"\n @onkeydown=', - 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=', -) -replace( - search_ip, - ''' private IEnumerable Validate() - { - _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } - } -''', - ''' private string? ValidateIp(string? value) - { - _error = null; - if (string.IsNullOrWhiteSpace(value)) - return _error = "Please enter an IP address."; - - if (!IPAddress.TryParse(value, out _)) - return _error = "Invalid IP format (v4 or v6 required)."; - - return null; - } -''', -) -replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") -replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') - -ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" -replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') - -domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" -replace( - domains, - 'Class="truncate-text" Title="@context.RedirectUrl"', - 'Class="truncate-text" title="@context.RedirectUrl"', -) -replace( - domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"', -) -replace( - domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"', -) From 39adf11c5f9ac24782dd90e57c6cc4b74105631b Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:14:09 -0400 Subject: [PATCH 27/27] Document explicit TLS test restore --- .github/workflows/tls-lifecycle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 386d0bf..3f1191b 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -26,6 +26,7 @@ jobs: - name: Restore solution run: dotnet restore TruthGate-IPFS.sln + # This test project is intentionally outside TruthGate-IPFS.sln, so restore it explicitly. - name: Restore TLS tests run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj