Private/Show-NSPConditionalAccessDashboard.ps1

function Show-NSPConditionalAccessDashboard {
    <#
    .SYNOPSIS
        Dashboard: draws the main screen. Banner, status tiles, every policy's status, and what needs attention.
    .DESCRIPTION
        From local files and the session only (Get-NSPConditionalAccessDashboardState and the
        cached Get-NSPConditionalAccessDashboardInsight); never a Graph call. Clears the screen first.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$Session
    )

    function Format-Age {
        param([timespan]$Age)
        if ($Age.TotalMinutes -lt 1) { 'just now' }
        elseif ($Age.TotalMinutes -lt 60) { "$([int]$Age.TotalMinutes) min ago" }
        elseif ($Age.TotalHours -lt 24) { "$([int]$Age.TotalHours) h ago" }
        else { "$([int]$Age.TotalDays) day(s) ago" }
    }

    $state = $Session.State
    $width = [Math]::Min((Get-NSPConsoleWidth), 200)
    $insight = Get-NSPConditionalAccessDashboardInsight -State $state -Cache $Session.Cache -ConfigRoot $Session.ConfigRoot

    Clear-NSPConsole
    $subtitle = if ($state.TenantDomain) { $state.TenantDomain } elseif ($state.TenantId) { $state.TenantId } else { 'no tenant yet: press N' }
    Write-NSPConsoleHeader -Title 'NSP Conditional Access' -Subtitle $subtitle -Width $width

    # Status tiles, two to four per line by window width.
    $session = if ($state.Connected) { @([string]$state.Context.Account, 'Success') }
    elseif ($state.OtherApp) { @('signed in through another app (C to fix)', 'Warning') }
    elseif (-not $state.TenantId) { @('no tenant (N to set one up)', 'Warning') }
    elseif (-not $state.Registered) { @('tool not registered here (C)', 'Warning') }
    else { @('not signed in (C to sign in)', 'Warning') }
    $inventory = if (-not $state.Inventory) { @('none yet', 'Warning') }
    elseif ($state.InventoryAge.TotalHours -ge 24) { @((Format-Age $state.InventoryAge), 'Warning') }
    else { @((Format-Age $state.InventoryAge), 'Success') }
    $profileText = if (-not $state.ProfilePath) { @('none yet', 'Warning') } else {
        $kind = if ($state.Profile.Hybrid) { 'hybrid (AD sync)' } else { 'cloud only' }
        @("built, $kind, home $($state.Profile.HomeCountry)", 'Success')
    }
    $defaults = $null
    if ($insight.Inventory) { $defaults = Get-NSPConditionalAccessMapValue $insight.Inventory 'SecurityDefaults' }
    $defaultsText = if (-not $defaults -or -not (Get-NSPConditionalAccessMapValue $defaults 'Available')) { @('unknown (take an inventory)', 'Muted') }
    elseif (Get-NSPConditionalAccessMapValue $defaults 'IsEnabled') { @('ON (turned off when enforcing)', 'Warning') }
    else { @('off', 'Success') }
    $registration = (Get-NSPConditionalAccessOption).Registration.Default
    $options = Get-NSPConditionalAccessMapValue $state.Profile 'Options'
    if ($options -and (Get-NSPConditionalAccessMapValue $options 'Registration')) { $registration = Get-NSPConditionalAccessMapValue $options 'Registration' }
    $open = @($state.RunsNeedingAttention).Count
    $runsText = if ($open) { @("$open need attention", 'Warning') } else { @('none open', 'Muted') }
    $planText = if ($state.PendingDecisions) { @("$($state.PendingDecisions) undecided in the latest plan", 'Accent') } else { @('nothing undecided', 'Muted') }
    $coverage = 'off'
    $coverageRole = 'Muted'
    if ($state.Profile) {
        $scheduled = (Get-NSPConditionalAccessCoverageSetting -TenantProfile $state.Profile).Scheduled
        if ($scheduled -and $scheduled.Enabled) { $coverage = 'scheduled'; $coverageRole = 'Success' }
        if ($state.LastCoverageJob) {
            $coverage += ", last $($state.LastCoverageJob.Status)"
            if ($state.LastCoverageJob.Status -in 'Held', 'Failed') { $coverageRole = 'Warning' }
        }
    }
    $tiles = @(
        @('Session', $session), @('Inventory', $inventory)
        @('Profile', $profileText), @('Sec. Defaults', $defaultsText)
        @('MFA setup', @($registration, 'Normal')), @('Coverage', @($coverage, $coverageRole))
        @('Plan', $planText), @('Runs', $runsText)
    )
    $perLine = [Math]::Max(2, [Math]::Min(4, [int][Math]::Floor(($width - 2) / 46)))
    $tileWidth = [int][Math]::Floor(($width - 2) / $perLine)
    for ($index = 0; $index -lt $tiles.Count; $index += $perLine) {
        $segments = @()
        foreach ($tile in $tiles[$index..([Math]::Min($index + $perLine, $tiles.Count) - 1)]) {
            $segments += , @(" $($tile[0])", 'Muted', 17)
            $segments += , @($tile[1][0], $tile[1][1], ($tileWidth - 17))
        }
        Write-NSPConsoleSegment -Segment $segments
    }

    # Policies, in columns: with each policy's name where the window is wide enough.
    Write-NSPConsoleLine ''
    Write-NSPConsoleRule -Title 'Policies' -Width $width
    if ($insight.Problem) { Write-NSPConsoleLine " Status not available: $($insight.Problem)" -Role Error }
    elseif (-not @($insight.Policies).Count) { Write-NSPConsoleLine ' Shown once there is an inventory and a tenant profile.' -Role Muted }
    else {
        $named = $width -ge 150
        # Columns share the window; a named cell needs about 90 characters to be worth it.
        $columns = if ($named) { [Math]::Max(1, [Math]::Min(3, [int][Math]::Floor(($width - 2) / 90))) } else { [Math]::Max(1, [Math]::Min(4, [int][Math]::Floor(($width - 2) / 38))) }
        $cell = [int][Math]::Floor(($width - 2) / $columns)
        $rows = @($insight.Policies)
        for ($index = 0; $index -lt $rows.Count; $index += $columns) {
            $segments = @(' ')
            foreach ($row in $rows[$index..([Math]::Min($index + $columns, $rows.Count) - 1)]) {
                $segments += , @(($row.Id -replace '^NSP-', ''), 'Strong', 18)
                $segments += , @($row.Status, $row.Role, 20)
                if ($named) { $segments += , @($row.Name, 'Muted', ($cell - 41)); $segments += ' ' }
            }
            Write-NSPConsoleSegment -Segment $segments
        }
    }

    # Attention.
    $notes = [System.Collections.Generic.List[object]]::new()
    if ($Session.InventoryProblem) { $notes.Add(@("$($Session.InventoryProblem) Press I to try again.", 'Error')) }
    $errors = @($insight.Findings | Where-Object Severity -eq 'Error')
    $warnings = @($insight.Findings | Where-Object Severity -eq 'Warning')
    foreach ($finding in $errors | Select-Object -First 3) { $notes.Add(@("$($finding.Rule): $($finding.Subject)", 'Error')) }
    if ($errors.Count -gt 3) { $notes.Add(@("$($errors.Count - 3) more problem(s)", 'Error')) }
    if ($warnings.Count) { $notes.Add(@("$($warnings.Count) warning(s): $(@($warnings | ForEach-Object { $_.Rule } | Select-Object -Unique) -join ', ')", 'Warning')) }
    foreach ($run in @($state.RunsNeedingAttention) | Select-Object -First 2) {
        $what = if ($run.Status -eq 'Pending') { 'approved but never run' } else { $run.Status }
        $notes.Add(@("Run $($run.RunId): $what", 'Warning'))
    }
    $coverageApp = $state.PSObject.Properties['CoverageApp'] | ForEach-Object { $_.Value }
    if ($coverageApp -and @($coverageApp.MissingPermissions).Count) {
        $notes.Add(@("The coverage app lacks $(@($coverageApp.MissingPermissions) -join ', '), so scheduled runs fail. Repair it: A, then Coverage.", 'Warning'))
    } elseif ($coverageApp -and $coverageApp.ExpiresSoon) {
        $notes.Add(@("The coverage app's certificate expires $($coverageApp.CertificateNotAfter.ToLocalTime().ToString('yyyy-MM-dd')). Renew it: A, then Coverage.", 'Warning'))
    }
    if ($state.Inventory -and $state.InventoryAge.TotalHours -ge 24 -and -not $state.Connected) { $notes.Add(@('The inventory is old; sign in (C) and a fresh one is taken.', 'Warning')) }
    if ($notes.Count) {
        Write-NSPConsoleLine ''
        Write-NSPConsoleRule -Title 'Needs attention ("Check tenant health" explains each)' -Width $width -Role Warning
        foreach ($note in $notes) { Write-NSPConsoleSegment -Segment @(@(' ! ', $note[1]), $note[0]) }
    }
    Write-NSPConsoleRule -Width $width
}