Private/Show-AACDashboardView.ps1

function Show-AACDashboardView {
    <#
    .SYNOPSIS
        Draws Show-AACDashboard's view: the headline status, tiles, the
        session and health side by side, the subscriptions, regions, Advisor,
        Service Health events, unhealthy resources and recent changes.
    .DESCRIPTION
        Every state is drawn with Get-AACStatus's symbols and colours (with
        an ASCII fallback), so it reads the same as the rest of the module
        and in any console.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Dashboard,

        [System.Collections.IDictionary] $Scope = @{}
    )

    $d = $Dashboard
    $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) }
    $glyph = Get-AACGlyph
    $mark = { param([string] $Status) (Get-AACStatus $Status).Markup }
    $when = { param($Time) if ($Time -is [datetime]) { if ($Time.Date -eq [datetime]::Today) { $Time.ToString('HH:mm') } else { $Time.ToString('d MMM HH:mm') } } else { '' } }
    $newTable = {
        param([string] $Title, [string[]] $Column)
        $table = [Spectre.Console.Table]::new()
        $table.Border = [Spectre.Console.TableBorder]::Rounded
        $table.BorderStyle = [Spectre.Console.Style]::Parse('grey35')
        $table.Title = [Spectre.Console.TableTitle]::new("[bold]$(& $escape $Title)[/]")
        foreach ($name in $Column) { $null = $table.AddColumn([Spectre.Console.TableColumn]::new("[grey70]$(& $escape $name)[/]")) }
        $table
    }
    $addRow = { param($Table, [string[]] $Cell) $null = [Spectre.Console.TableExtensions]::AddRow($Table, [Spectre.Console.Rendering.IRenderable[]]@($Cell | ForEach-Object { [Spectre.Console.Markup]::new($_) })) }

    # --- Who, what, when -------------------------------------------------------------------------------------
    $facts = [System.Collections.Generic.List[string]]::new()
    if ($d.Account) { $facts.Add("[white]$(& $escape $d.Account)[/]") }
    if ($d.TenantId) { $facts.Add("tenant $(& $escape $d.TenantId)") }
    foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") }
    $facts.Add($d.GeneratedAt.ToString('d MMM yyyy HH:mm'))
    Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]"
    [Spectre.Console.AnsiConsole]::WriteLine()
    $headline = @{ Success = 'Healthy'; Warning = 'Needs attention'; Failed = 'Action needed' }[$d.Status]
    Write-AACStatusLine $d.Status "[bold]$headline[/] [grey70]$(& $escape $d.Headline)[/]" -Markup
    [Spectre.Console.AnsiConsole]::WriteLine()

    $changes = $d.Changes.Created + $d.Changes.Updated + $d.Changes.Deleted
    Show-AACTileRow -Tile @(
        @{ Value = '{0:N0}' -f @($d.Subscriptions).Count; Caption = 'subscriptions'; Color = 'springgreen2' }
        @{ Value = '{0:N0}' -f $d.Resources; Caption = 'resources'; Color = 'deepskyblue1' }
        @{ Value = '{0:N0}' -f $d.ResourceGroups; Caption = 'resource groups'; Color = 'gold1' }
        @{ Value = '{0:N0}' -f $d.Regions; Caption = 'regions'; Color = 'hotpink' }
        @{ Value = '{0:N0}' -f ($d.Health.Unavailable + $d.Health.Degraded); Caption = 'unhealthy'; Color = $(if ($d.Health.Unavailable) { 'red1' } else { 'orange1' }) }
        @{ Value = '{0:N0}' -f $d.AdvisorHigh; Caption = 'Advisor High'; Color = 'orange1' }
        @{ Value = '{0:N0}' -f $changes; Caption = "changes in $($d.Hours)h"; Color = 'mediumpurple2' }
    )
    [Spectre.Console.AnsiConsole]::WriteLine()

    # --- Session and health, side by side ------------------------------------------------------------------------
    $sessionLines = [System.Collections.Generic.List[string]]::new()
    $sessionLines.Add("[grey58]Account[/] $(& $escape $(if ($d.Account) { $d.Account } else { '-' }))")
    $sessionLines.Add("[grey58]Tenant[/] $(& $escape $(if ($d.TenantId) { $d.TenantId } else { '-' }))")
    if ($d.SignIn) { $sessionLines.Add("[grey58]Sign-in[/] $(& $escape $d.SignIn)") }
    if ($d.TokenExpiresOn -is [datetime]) {
        $minutes = [int]($d.TokenExpiresOn - $d.GeneratedAt).TotalMinutes
        $sessionLines.Add("[grey58]Token[/] $(& $mark $(if ($minutes -gt 5) { 'Success' } else { 'Warning' })) valid until $($d.TokenExpiresOn.ToString('HH:mm')) [grey58](renewed when needed)[/]")
    }
    $healthLines = [System.Collections.Generic.List[string]]::new()
    $healthLines.Add("$(& $mark 'Success') Available [white]$('{0:N0}' -f $d.Health.Available)[/]")
    $healthLines.Add("$(& $mark $(if ($d.Health.Unavailable) { 'Failed' } else { 'Success' })) Unavailable [white]$('{0:N0}' -f $d.Health.Unavailable)[/]")
    $healthLines.Add("$(& $mark $(if ($d.Health.Degraded) { 'Warning' } else { 'Success' })) Degraded [white]$('{0:N0}' -f $d.Health.Degraded)[/]")
    $healthLines.Add("$(& $mark 'Info') Unknown [white]$('{0:N0}' -f $d.Health.Unknown)[/]")
    $serviceIssues = @($d.ServiceEvents | Where-Object Type -EQ 'Service issue').Count
    $healthLines.Add("$(& $mark $(if ($serviceIssues) { 'Failed' } else { 'Success' })) Service issues [white]$serviceIssues[/] [grey58]active[/]")
    $panel = {
        param([string] $Header, [string[]] $Lines, [string] $Border)
        $p = [Spectre.Console.Panel]::new([Spectre.Console.Markup]::new($Lines -join "`n"))
        $p.Border = [Spectre.Console.BoxBorder]::Rounded
        $p.BorderStyle = [Spectre.Console.Style]::Parse($Border)
        $p.Header = [Spectre.Console.PanelHeader]::new(" [bold]$(& $escape $Header)[/] ")
        $p.Expand = $true
        $p
    }
    $healthBorder = (Get-AACStatus $(if ($d.Health.Unavailable -or $serviceIssues) { 'Failed' } elseif ($d.Health.Degraded) { 'Warning' } else { 'Success' })).Color
    $columns = [Spectre.Console.Columns]::new([Spectre.Console.Rendering.IRenderable[]]@((& $panel 'Session' $sessionLines.ToArray() 'grey50'), (& $panel 'Resource Health' $healthLines.ToArray() $healthBorder)))
    $columns.Expand = $true
    [Spectre.Console.AnsiConsole]::Write($columns)
    [Spectre.Console.AnsiConsole]::WriteLine()

    # --- Subscriptions -------------------------------------------------------------------------------------------
    if (@($d.Subscriptions).Count) {
        $table = & $newTable 'Subscriptions' @('Subscription', 'State', 'ID')
        foreach ($s in @($d.Subscriptions)) {
            $state = switch ($s.State) { 'Enabled' { 'Success' } { $_ -in 'Warned', 'PastDue' } { 'Warning' } default { if ($s.State) { 'Failed' } else { 'Info' } } }
            & $addRow $table @("[white]$(& $escape $s.Name)[/]", "$(& $mark $state) $(& $escape $s.State)", "[grey50]$(& $escape $s.SubscriptionId)[/]")
        }
        [Spectre.Console.AnsiConsole]::Write($table)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    # --- Where the resources are ---------------------------------------------------------------------------------
    $regions = @($d.TopRegions)
    if ($regions.Count) {
        $bars = @($regions | Select-Object -First 8 | ForEach-Object { @{ Label = $_.Region; Value = $_.Resources } })
        $rest = @($regions | Select-Object -Skip 8)
        if ($rest.Count) { $bars += @{ Label = "$($rest.Count) other region(s)"; Value = [int](@($rest.Resources) | Measure-Object -Sum).Sum; Color = 'grey50' } }
        Show-AACBarChart -Item $bars -Title 'Resources by region'
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    # --- Service Health, unhealthy resources ------------------------------------------------------------------------
    if (@($d.ServiceEvents).Count) {
        $table = & $newTable 'Azure Service Health - active events' @('', 'Type', 'Title', 'Tracking ID', 'Since', 'Subscriptions')
        foreach ($e in @($d.ServiceEvents)) {
            $state = if ($e.Type -eq 'Service issue') { 'Failed' } elseif ($e.Type -eq 'Security advisory') { 'Warning' } else { 'Info' }
            & $addRow $table @((& $mark $state), (& $escape $e.Type), "[white]$(& $escape $e.Title)[/]", "[grey58]$(& $escape $e.TrackingId)[/]", (& $when $e.Started), "[grey58]$(& $escape $e.Subscription)[/]")
        }
        [Spectre.Console.AnsiConsole]::Write($table)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    if (@($d.UnhealthyResources).Count) {
        $table = & $newTable 'Unhealthy resources (Resource Health)' @('', 'Resource', 'Resource group', 'State', 'Why', 'Since')
        foreach ($r in @($d.UnhealthyResources)) {
            & $addRow $table @((& $mark $(if ($r.State -eq 'Unavailable') { 'Failed' } else { 'Warning' })), "[white]$(& $escape $r.Resource)[/]", (& $escape $r.ResourceGroup), (& $escape $r.State), "[grey70]$(& $escape $r.Summary)[/]", (& $when $r.Since))
        }
        [Spectre.Console.AnsiConsole]::Write($table)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    # --- Advisor ----------------------------------------------------------------------------------------------------
    if (@($d.Advisor).Count) {
        $table = & $newTable 'Azure Advisor recommendations' @('Category', 'High', 'Medium', 'Low')
        $count = { param([int] $N, [string] $Color) if ($N) { "[$Color]$N[/]" } else { '[grey42]0[/]' } }
        foreach ($a in @($d.Advisor)) { & $addRow $table @((& $escape $a.Category), (& $count $a.High 'red1'), (& $count $a.Medium 'orange1'), (& $count $a.Low 'deepskyblue1')) }
        [Spectre.Console.AnsiConsole]::Write($table)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    # --- Recent changes --------------------------------------------------------------------------------------------
    $changeTitle = "Recent changes - last $($d.Hours)h: $($d.Changes.Created) created, $($d.Changes.Updated) updated, $($d.Changes.Deleted) deleted"
    if (@($d.RecentChanges).Count) {
        $table = & $newTable $changeTitle @('When', 'Change', 'Resource', 'Type', 'Resource group', 'By')
        $kinds = @{ Create = @('green3', '+'); Update = @('deepskyblue1', '~'); Delete = @('red1', '-') }
        foreach ($c in @($d.RecentChanges)) {
            $kind = if ($kinds.Contains($c.Change)) { $kinds[$c.Change] } else { @('grey70', ' ') }
            & $addRow $table @((& $when $c.Time), "[$($kind[0])]$($kind[1]) $(& $escape $c.Change)[/]", "[white]$(& $escape $c.Resource)[/]", "[grey58]$(& $escape ($c.ResourceType -replace '^microsoft\.', ''))[/]", (& $escape $c.ResourceGroup), "[grey70]$(& $escape $c.ChangedBy)[/]")
        }
        [Spectre.Console.AnsiConsole]::Write($table)
    }
    else { Write-AACStatusLine Info "No resource changes in the last $($d.Hours)h." }
    [Spectre.Console.AnsiConsole]::WriteLine()

    # --- What couldn't be read ----------------------------------------------------------------------------------------
    foreach ($property in @($d.Unread.PSObject.Properties)) { Write-AACStatusLine Warning "Couldn't read $($property.Name)" -Detail $property.Value }
    Write-AACMarkup '[grey42]-SubscriptionId or -Select narrows the scope; -Hours sets the change window; -PassThru returns the dashboard object.[/]'
}