@page "/storage" @attribute [Authorize] @inject IUserSessionService Session @inject ISessionManager SessionMgr @inject IElevationCoordinator Elevation @inject IStorageService StorageSvc @inject StorageCsvExportService CsvExport @inject StorageHtmlExportService HtmlExport @inject WebExportService WebExport @inject IAuditService Audit @inject TranslationSource T @rendermode InteractiveServer

@T["stor.page.title"]

@if (!Session.HasProfile) { return; }
@T["grp.scan.opts"]
@T["stor.hint.folder_depth"]
@if (_sites.Count > 0) { @string.Format(T["perm.sites.selected"], _sites.Count) } @if (_running) { }
@if (!string.IsNullOrEmpty(_error)) {
@_error
} @if (_results.Count > 0) {
@T["stor.report.title"] @string.Format(T["stor.badge.libraries_count"], _results.Count)
@foreach (var n in _results) { }
@T["stor.col.library"] @T["stor.col.site"] @T["stor.col.files"] @T["stor.col.total_mb"] @T["stor.col.versions_mb"] @T["stor.col.lastmod"]
@n.Name @n.SiteTitle @n.TotalFileCount.ToString("N0") @((n.TotalSizeBytes / 1048576.0).ToString("F2")) @((n.VersionSizeBytes / 1048576.0).ToString("F2")) @(n.LastModified?.ToString("yyyy-MM-dd") ?? "")
} @code { private List _sites = new(); private bool _includeSubsites, _includeHidden = true, _includeRecycleBin = true; private int _folderDepth; private bool _running; private string _status = string.Empty, _error = string.Empty; private int _current, _total; private List _results = new(); private List<(string Label, IReadOnlyList Results)> _bySite = new(); private ReportMergeMode _mergeMode = ReportMergeMode.SingleMerged; private CancellationTokenSource? _cts; private async Task RunScan() { _error = string.Empty; _results = new(); _bySite = new(); _running = true; _cts = new CancellationTokenSource(); if (_sites.Count == 0) { _error = T["stor.err.select_site"]; _running = false; return; } var progress = new Progress(p => { _status = p.Message; _current = p.Current; _total = p.Total; InvokeAsync(StateHasChanged); }); try { var opts = new StorageScanOptions(IncludeSubsites: _includeSubsites, FolderDepth: Math.Clamp(_folderDepth, 0, 20), IncludeHiddenLibraries: _includeHidden, IncludeRecycleBin: _includeRecycleBin); var bySite = new List<(string, IReadOnlyList)>(); var flat = new List(); int i = 0; foreach (var site in _sites) { _cts.Token.ThrowIfCancellationRequested(); _status = string.Format(T["stor.status.scanning_site"], site.Title, ++i, _sites.Count); await InvokeAsync(StateHasChanged); var nodes = await Elevation.RunAsync(async c => { var ctx = await SessionMgr.GetOrCreateContextAsync(site.Url, Session.CurrentProfile!, c); return await StorageSvc.CollectStorageAsync(ctx, opts, progress, c); }, _cts.Token); bySite.Add((site.Title, nodes)); flat.AddRange(nodes); } _bySite = bySite; _results = flat; _status = string.Format(T["stor.status.complete_nodes"], _results.Count, _sites.Count); await Audit.LogAsync("StorageScan", Session.CurrentProfile?.Name ?? "", _sites.Select(s => s.Url), $"{_results.Count} nodes; depth={_folderDepth} subsites={_includeSubsites} hidden={_includeHidden} recycle={_includeRecycleBin}"); } catch (OperationCanceledException) { _status = T["stor.status.cancelled"]; } catch (Exception ex) { _error = ex.Message; } finally { _running = false; await InvokeAsync(StateHasChanged); } } private void Cancel() => _cts?.Cancel(); private async Task ExportCsv() { var ts = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var output = ReportMergeHelper.Build(_bySite, _mergeMode, "storage", ts, ReportFormat.Csv, rs => CsvExport.BuildCsv(rs)); await WebExport.DownloadBytesAsync(output.Bytes, output.FileName, output.Mime); } private async Task ExportHtml() { var ts = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var output = ReportMergeHelper.Build(_bySite, _mergeMode, "storage", ts, ReportFormat.Html, rs => HtmlExport.BuildHtml(rs, Session.CurrentBranding)); await WebExport.DownloadBytesAsync(output.Bytes, output.FileName, output.Mime); } }