1010
1111<RadzenText TextStyle =" TextStyle.H3" TagName =" TagName.H1" class =" rz-color-primary rz-mb-2" >Azure Dashboard</RadzenText >
1212
13- @if (report == null )
13+ @if (_loading )
1414{
1515 <RadzenProgressBar Mode =" ProgressBarMode.Indeterminate" class =" rz-my-4" />
1616 <RadzenText TextStyle =" TextStyle.Body1" >Loading Azure report .. .</RadzenText >
1717}
18+ else if (! string .IsNullOrWhiteSpace (_loadError ))
19+ {
20+ <RadzenAlert AlertStyle =" AlertStyle.Warning" Variant =" Variant.Outlined" ShowIcon =" true" class =" rz-my-4" >
21+ <div ><strong >Azure dashboard load failed .</strong ></div >
22+ <div style =" margin-top :.35rem " >@_loadError </div >
23+ <div style =" margin-top :.75rem " >
24+ <RadzenButton Text =" Retry" Icon =" refresh" ButtonStyle =" ButtonStyle.Primary" Size =" ButtonSize.Small" Click =" @(async () => await LoadReportAsync())" />
25+ </div >
26+ </RadzenAlert >
27+ }
1828else
1929{
30+ var currentReport = report ?? new AzureReport ();
31+
2032 <RadzenText TextStyle =" TextStyle.Caption" class =" rz-color-secondary rz-mb-2" >
2133 <RadzenStack Orientation =" Orientation.Horizontal" JustifyContent =" JustifyContent.SpaceBetween" AlignItems =" AlignItems.Center" Wrap =" FlexWrap.Wrap" Gap =" 8px" >
22- <span >Subscription : <strong >@report .Subscription ?.Name </strong >   ; |   ; Generated : <strong >@( report .GeneratedAt ? .ToString (" yyyy-MM-dd HH:mm" ) ?? " —" ) </strong >
23- @if (report .GeneratedAt . HasValue && (DateTime .UtcNow - report . GeneratedAt . Value ).TotalHours > 24 )
34+ <span >Subscription : <strong >@currentReport .Subscription ?.Name </strong >   ; |   ; Generated : <strong >@( currentReport .GeneratedAt ? .ToString (" yyyy-MM-dd HH:mm" ) ?? " —" ) </strong >
35+ @if (currentReport .GeneratedAt is DateTime generatedAt && (DateTime .UtcNow - generatedAt ).TotalHours > 24 )
2436 {
25- <RadzenBadge Text =" @($" Data is {(int)(DateTime.UtcNow - report.GeneratedAt.Value ).TotalHours}h old — refresh recommended " )"
37+ <RadzenBadge Text =" @($" Data is {(int)(DateTime.UtcNow - generatedAt ).TotalHours}h old — refresh recommended " )"
2638 BadgeStyle =" BadgeStyle.Warning" IsPill =" true" Style =" margin-left:.5rem;font-size:.7rem" />
2739 }
2840 </span >
3951
4052 <!-- ── Summary Strip ── -->
4153 <div style =" display :grid ;grid-template-columns :repeat (auto-fit ,minmax (140px ,1fr ));gap :.75rem ;margin-bottom :1.75rem " >
42- @SummaryCard(report .WebServices ? .Total .ToString () ?? " 0" , " Services" , " var(--rz-info)" )
43- @SummaryCard(report .WebServices ? .ByStatus ? .Active .ToString () ?? " 0" , " Active" , " var(--rz-success)" )
44- @SummaryCardLink(report .WebServices ? .ByStatus ? .Broken .ToString () ?? " 0" , " Broken ↗" , report .WebServices ? .ByStatus ? .Broken > 0 ? " var(--rz-danger)" : " var(--rz-secondary)" , () => _selectedTabIndex = 0 )
45- @SummaryCard(report .Cost ? .TotalFormatted ?? " $0.00" , " 30-Day Cost" , " var(--rz-warning)" )
54+ @SummaryCard(currentReport .WebServices ? .Total .ToString () ?? " 0" , " Services" , " var(--rz-info)" )
55+ @SummaryCard(currentReport .WebServices ? .ByStatus ? .Active .ToString () ?? " 0" , " Active" , " var(--rz-success)" )
56+ @SummaryCardLink(currentReport .WebServices ? .ByStatus ? .Broken .ToString () ?? " 0" , " Broken ↗" , currentReport .WebServices ? .ByStatus ? .Broken > 0 ? " var(--rz-danger)" : " var(--rz-secondary)" , () => _selectedTabIndex = 0 )
57+ @SummaryCard(currentReport .Cost ? .TotalFormatted ?? " $0.00" , " 30-Day Cost" , " var(--rz-warning)" )
4658 @SummaryCardLink(safeToRemove.Count.ToString() , " Safe to Remove ↗" , safeToRemove .Count > 0 ? " var(--rz-danger)" : " var(--rz-success)" , () => _selectedTabIndex = 1 )
4759 </div >
4860
4961 <RadzenTabs @bind-SelectedIndex =" _selectedTabIndex" >
5062 <Tabs >
5163
5264 <!-- ── Tab 1: Web Services ── -->
53- <RadzenTabsItem Text =" @($" 🌐 Web Services ({report .WebServices?.Total ?? 0}) " )" >
65+ <RadzenTabsItem Text =" @($" 🌐 Web Services ({currentReport .WebServices?.Total ?? 0}) " )" >
5466 <div style =" overflow-x :auto " >
5567 <RadzenDataGrid Data =" @SortedServices" TItem =" WebService" AllowFiltering =" true" AllowSorting =" true"
5668 AllowPaging =" true" PageSize =" 15" ShowPagingSummary =" true" AllowColumnResize =" true" FilterMode =" FilterMode.Simple" >
@@ -188,21 +200,21 @@ else
188200 </RadzenTabsItem >
189201
190202 <!-- ── Tab 3: Cost ── -->
191- <RadzenTabsItem Text =" @($" 💳 Cost ({report .Cost?.TotalFormatted ?? " $0" }) " )" >
203+ <RadzenTabsItem Text =" @($" 💳 Cost ({currentReport .Cost?.TotalFormatted ?? " $0" }) " )" >
192204 <div class =" rz-mt-2" >
193- @if (report .Cost ? .Note != null )
205+ @if (currentReport .Cost ? .Note != null )
194206 {
195207 <div style =" background :rgba (255 ,214 ,0 ,.08 );border :1px solid rgba (255 ,214 ,0 ,.3 );border-radius :6px ;padding :.7rem 1rem ;color :#ffd700 ;margin-bottom :1rem " >
196- 📝 @report .Cost.Note
208+ 📝 @currentReport .Cost.Note
197209 </div >
198210 }
199- @if (report .BurnRate ? .DailyCosts .Count > 0 )
211+ @if (currentReport .BurnRate ? .DailyCosts .Count > 0 )
200212 {
201213 <RadzenRow class =" rz-mb-4" >
202214 <RadzenColumn SizeXS =" 12" SizeMD =" 7" >
203215 <RadzenText TextStyle =" TextStyle.H6" class =" rz-mb-1" >
204216 Monthly Burn Rate
205- <RadzenBadge Text =" @($" Projected: {report .BurnRate.ProjectedFormatted} " )" BadgeStyle =" BadgeStyle.Warning" IsPill =" true" Style =" font-size:.75rem;margin-left:.5rem" />
217+ <RadzenBadge Text =" @($" Projected: {currentReport .BurnRate? .ProjectedFormatted ?? " — " } " )" BadgeStyle =" BadgeStyle.Warning" IsPill =" true" Style =" font-size:.75rem;margin-left:.5rem" />
206218 </RadzenText >
207219 <RadzenText TextStyle =" TextStyle.Caption" class =" rz-color-secondary rz-mb-1" >Based on daily spend this calendar month only — not rolling 30 d </RadzenText >
208220 <RadzenChart Style =" height:260px" >
230242 </RadzenColumn >
231243 </RadzenRow >
232244 }
233- <RadzenDataGrid Data =" @report .Cost?.TopCostDrivers" TItem =" CostDriver" AllowSorting =" true" PageSize =" 30" AllowPaging =" false" >
245+ <RadzenDataGrid Data =" @currentReport .Cost?.TopCostDrivers" TItem =" CostDriver" AllowSorting =" true" PageSize =" 30" AllowPaging =" false" >
234246 <Columns >
235247 <RadzenDataGridColumn TItem =" CostDriver" Property =" Name" Title =" Service / Resource Group" />
236248 <RadzenDataGridColumn TItem =" CostDriver" Property =" Cost" Title =" Cost (USD)" Width =" 130px" TextAlign =" TextAlign.Right" >
@@ -243,19 +255,19 @@ else
243255 </Columns >
244256 </RadzenDataGrid >
245257 <RadzenCard class =" rz-mt-3" Style =" text-align:right" >
246- <RadzenText TextStyle =" TextStyle.H6" >Total : <span style =" color :#ffd700 " >@( report .Cost ? .TotalFormatted ?? " —" ) </span ></RadzenText >
258+ <RadzenText TextStyle =" TextStyle.H6" >Total : <span style =" color :#ffd700 " >@( currentReport .Cost ? .TotalFormatted ?? " —" ) </span ></RadzenText >
247259 </RadzenCard >
248260 </div >
249261 </RadzenTabsItem >
250262
251263 <!-- ── SSL Expiry Tab ── -->
252- <RadzenTabsItem Text =" @($" 🔒 SSL ({report .SslExpiry?.Count ?? 0}) " )" >
264+ <RadzenTabsItem Text =" @($" 🔒 SSL ({currentReport .SslExpiry?.Count ?? 0}) " )" >
253265 <div class =" rz-mt-2" >
254266 @{
255- var sslCritical = report .SslExpiry ? .Count (x => x .DaysLeft is { } d && d < 14 ) ?? 0 ;
256- var sslWarning = report .SslExpiry ? .Count (x => x .DaysLeft is { } d && d is >= 14 and < 60 ) ?? 0 ;
257- var sslErrored = report .SslExpiry ? .Count (x => x .Error != null && x .DaysLeft == null ) ?? 0 ;
258- var sslOk = report .SslExpiry ? .Count (x => x .DaysLeft != null ) ?? 0 ;
267+ var sslCritical = currentReport .SslExpiry ? .Count (x => x .DaysLeft is { } d && d < 14 ) ?? 0 ;
268+ var sslWarning = currentReport .SslExpiry ? .Count (x => x .DaysLeft is { } d && d is >= 14 and < 60 ) ?? 0 ;
269+ var sslErrored = currentReport .SslExpiry ? .Count (x => x .Error != null && x .DaysLeft == null ) ?? 0 ;
270+ var sslOk = currentReport .SslExpiry ? .Count (x => x .DaysLeft != null ) ?? 0 ;
259271 }
260272 @if (sslErrored > 0 && sslOk == 0 )
261273 {
275287 🟡 @sslWarning certificate (s ) expire within 60 days — schedule renewal soon .
276288 </div >
277289 }
278- <RadzenDataGrid Data =" @report .SslExpiry" TItem =" SslEntry" AllowSorting =" true" AllowFiltering =" true"
290+ <RadzenDataGrid Data =" @currentReport .SslExpiry" TItem =" SslEntry" AllowSorting =" true" AllowFiltering =" true"
279291 FilterMode =" FilterMode.Simple" AllowPaging =" true" PageSize =" 20" >
280292 <Columns >
281293 <RadzenDataGridColumn TItem =" SslEntry" Property =" Name" Title =" Service" Width =" 180px" />
@@ -318,17 +330,17 @@ else
318330 </RadzenTabsItem >
319331
320332 <!-- ── App Insights Tab ── -->
321- <RadzenTabsItem Text =" @($" 📊 App Insights ({report .AppInsightsMetrics?.Count ?? 0}) " )" >
333+ <RadzenTabsItem Text =" @($" 📊 App Insights ({currentReport .AppInsightsMetrics?.Count ?? 0}) " )" >
322334 <div class =" rz-mt-2" >
323- @if (report .AppInsightsMetrics ? .Any () != true )
335+ @if (currentReport .AppInsightsMetrics ? .Any () != true )
324336 {
325337 <div style =" background :rgba (100 ,180 ,255 ,.07 );border :1px solid rgba (100 ,180 ,255 ,.25 );border-radius :6px ;padding :.75rem 1rem ;color :var (--rz-text-secondary-color )" >
326338 ℹ️ No Application Insights components found , or metrics could not be retrieved .
327339 </div >
328340 }
329341 else
330342 {
331- <RadzenDataGrid Data =" @report .AppInsightsMetrics" TItem =" AppInsightsMetric"
343+ <RadzenDataGrid Data =" @currentReport .AppInsightsMetrics" TItem =" AppInsightsMetric"
332344 AllowSorting =" true" AllowFiltering =" true" FilterMode =" FilterMode.Simple"
333345 AllowPaging =" true" PageSize =" 20" >
334346 <Columns >
359371 </RadzenTabsItem >
360372
361373 <!-- ── Resource Breakdown Tab ── -->
362- <RadzenTabsItem Text =" @($" 📦 Resources ({report .AllResourceSummary?.Total ?? 0}) " )" >
374+ <RadzenTabsItem Text =" @($" 📦 Resources ({currentReport .AllResourceSummary?.Total ?? 0}) " )" >
363375 <div class =" rz-mt-2" >
364376 <RadzenRow >
365377 <RadzenColumn SizeXS =" 12" SizeMD =" 6" >
@@ -419,17 +431,17 @@ else
419431 </RadzenTabsItem >
420432
421433 <!-- ── Orphaned Resources Tab ── -->
422- <RadzenTabsItem Text =" @($" 🧟 Orphaned ({report .OrphanedResources?.Count ?? 0}) " )" >
434+ <RadzenTabsItem Text =" @($" 🧟 Orphaned ({currentReport .OrphanedResources?.Count ?? 0}) " )" >
423435 <div class =" rz-mt-2" >
424- @if (report .OrphanedResources ? .Any () != true )
436+ @if (currentReport .OrphanedResources ? .Any () != true )
425437 {
426438 <div style =" background :rgba (50 ,255 ,167 ,.08 );border :1px solid rgba (50 ,255 ,167 ,.3 );border-radius :6px ;padding :.7rem 1rem ;color :var (--rz-success-color )" >
427439 ✅ No orphaned resources — no unattached disks , idle public IPs , or empty App Service Plans .
428440 </div >
429441 }
430442 else
431443 {
432- <RadzenDataGrid Data =" @report .OrphanedResources" TItem =" OrphanedResource" AllowSorting =" true" AllowPaging =" true" PageSize =" 15" >
444+ <RadzenDataGrid Data =" @currentReport .OrphanedResources" TItem =" OrphanedResource" AllowSorting =" true" AllowPaging =" true" PageSize =" 15" >
433445 <Columns >
434446 <RadzenDataGridColumn TItem =" OrphanedResource" Property =" Name" Title =" Resource" Width =" 200px" />
435447 <RadzenDataGridColumn TItem =" OrphanedResource" Property =" ResourceGroup" Title =" Resource Group" Width =" 160px" />
460472 </RadzenTabsItem >
461473
462474 <!-- ── Storage Tab ── -->
463- <RadzenTabsItem Text =" @($" 💾 Storage ({report .StorageInventory?.Count ?? 0}) " )" >
475+ <RadzenTabsItem Text =" @($" 💾 Storage ({currentReport .StorageInventory?.Count ?? 0}) " )" >
464476 <div class =" rz-mt-2" >
465- <RadzenDataGrid Data =" @report .StorageInventory" TItem =" StorageItem" AllowFiltering =" true" AllowSorting =" true" PageSize =" 15" AllowPaging =" true" FilterMode =" FilterMode.Simple" >
477+ <RadzenDataGrid Data =" @currentReport .StorageInventory" TItem =" StorageItem" AllowFiltering =" true" AllowSorting =" true" PageSize =" 15" AllowPaging =" true" FilterMode =" FilterMode.Simple" >
466478 <Columns >
467479 <RadzenDataGridColumn TItem =" StorageItem" Property =" Name" Title =" Account" Width =" 180px" />
468480 <RadzenDataGridColumn TItem =" StorageItem" Property =" ResourceGroup" Title =" Resource Group" Width =" 160px" />
509521 private List <WebService > services = new ();
510522 private List <SafeToRemoveItem > safeToRemove = new ();
511523 private int _selectedTabIndex = 0 ;
524+ private bool _loading = true ;
525+ private string ? _loadError ;
512526 private IEnumerable <WebService > SortedServices =>
513527 services .OrderBy (s => s .HttpStatus == " active" ? 0 : 1 ).ThenBy (s => s .FriendlyName ?? s .Name );
514528
@@ -536,16 +550,36 @@ else
536550 // No file system access needed in WASM — all data comes via HTTP.
537551 private async Task LoadReportAsync ()
538552 {
553+ _loading = true ;
554+ _loadError = null ;
555+ StateHasChanged ();
556+
539557 try
540558 {
541- var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
559+ var opts = new JsonSerializerOptions (JsonSerializerDefaults .Web )
560+ {
561+ PropertyNameCaseInsensitive = true
562+ };
563+
542564 report = await Http .GetFromJsonAsync <AzureReport >(" /api/diag/report" , opts );
543- services = report ? .WebServices ? .Services ?? new List <WebService >();
565+ if (report is null )
566+ throw new InvalidOperationException (" The Azure report endpoint returned no data." );
567+
568+ services = report .WebServices ? .Services ?? new List <WebService >();
544569 safeToRemove = BuildSafeToRemove (report );
545570 }
546571 catch (Exception ex )
547572 {
548- Console .WriteLine ($" Azure dashboard load error: {ex .Message }" );
573+ report = null ;
574+ services = new List <WebService >();
575+ safeToRemove = new List <SafeToRemoveItem >();
576+ _loadError = ex .Message ;
577+ Console .Error .WriteLine ($" Azure dashboard load error: {ex }" );
578+ }
579+ finally
580+ {
581+ _loading = false ;
582+ StateHasChanged ();
549583 }
550584 }
551585
0 commit comments