Start-AzureLocalServer.ps1

<#
.SYNOPSIS
    Start the Azure Local Inventory web dashboard.
.DESCRIPTION
    Launches a local HTTP server on the specified port and opens a browser-ready dashboard
    for exploring Azure Local (Azure Stack HCI) inventory. The dashboard provides
    authentication flow, WAF assessment, cost analysis, and executive PDF export.
.PARAMETER Port
    TCP port for the local web server. Defaults to 8081.
.EXAMPLE
    Start-AzureLocalServer
    Starts the dashboard on the default port (8081).
.EXAMPLE
    Start-AzureLocalServer -Port 9090
    Starts the dashboard on port 9090.
#>

function Start-AzureLocalServer {
    [CmdletBinding()]
    param(
        [int]$Port = 8081
    )

    $moduleRoot = $PSScriptRoot

    Write-Host "🚀 Starting Azure Local Inventory Server..." -ForegroundColor Cyan
    Write-Host "✓ PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Green

    # Check Azure connection (private helper)
    function Test-AzureConnection {
    try {
        $context = Get-AzContext
        if ($null -eq $context) {
            return $false
        }
        return $true
    }
    catch {
        return $false
    }
}

    # Server-scoped state
    $isAuthenticated = Test-AzureConnection
    $inventoryData   = @{}
    $lastUpdate      = $null
    $wafConfig       = $null

    # Load WAF Configuration
    $wafConfigPath = Join-Path $moduleRoot "waf-config.json"
    if (Test-Path $wafConfigPath) {
        try {
            $wafConfig = Get-Content $wafConfigPath -Raw | ConvertFrom-Json
            Write-Host "✓ WAF Configuration loaded (Version: $($wafConfig.version))" -ForegroundColor Green
        }
        catch {
            Write-Host "⚠️ Failed to load WAF configuration: $($_.Exception.Message)" -ForegroundColor Yellow
        }
    }
    else {
        Write-Host "⚠️ WAF configuration file not found at: $wafConfigPath" -ForegroundColor Yellow
    }

    Write-Host "🔐 Azure Authentication Status: $(if ($isAuthenticated) { 'Connected ✓' } else { 'Not Connected ✗' })" -ForegroundColor $(if ($isAuthenticated) { 'Green' } else { 'Yellow' })

    # HTTP Listener
    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add("http://localhost:$Port/")
    $listener.Start()

    Write-Host "🌐 Server started at http://localhost:$Port" -ForegroundColor Green
    Write-Host "📊 Access the Azure Local Inventory Dashboard in your browser" -ForegroundColor Cyan
    Write-Host "Press Ctrl+C to stop the server" -ForegroundColor Gray
    Write-Host ""

    # CancellationTokenSource used by the Ctrl+C handler to signal shutdown
    $cts = [System.Threading.CancellationTokenSource]::new()

    # Register Ctrl+C so it stops the listener gracefully instead of hard-killing
    $cancelHandler = {
        param($sender, $args)
        $args.Cancel = $true   # prevent the process from being terminated immediately
        $cts.Cancel()
        $listener.Stop()
    }
    [Console]::add_CancelKeyPress($cancelHandler)

try {
    while ($listener.IsListening) {
        # GetContextAsync + 500 ms polling allows Ctrl+C to be processed
        # between requests rather than blocking indefinitely.
        $task = $listener.GetContextAsync()
        try {
            while (-not $task.Wait(500)) {
                if ($cts.IsCancellationRequested) { break }
            }
        } catch { }

        if ($cts.IsCancellationRequested -or -not $listener.IsListening) { break }

        $context  = $task.Result
        $request  = $context.Request
        $response = $context.Response
        
        $path = $request.Url.AbsolutePath
        $method = $request.HttpMethod
        
        Write-Host "$(Get-Date -Format 'HH:mm:ss') $method $path" -ForegroundColor Gray
        
        # Content type and response
        $content = ""
        $contentType = "text/html; charset=utf-8"
        
        # Route handling
        switch -Regex ($path) {
            '^/$' {
                # Serve main page
                $indexPath = Join-Path $moduleRoot "index.html"
                if (Test-Path $indexPath) {
                    $content = Get-Content $indexPath -Raw
                } else {
                    $content = "<html><body><h1>Azure Local Inventory Server</h1><p>index.html not found</p></body></html>"
                }
            }
            
            '^/styles\.css$' {
                $cssPath = Join-Path $moduleRoot "styles.css"
                if (Test-Path $cssPath) {
                    $content = Get-Content $cssPath -Raw
                    $contentType = "text/css"
                }
            }
            
            '^/app\.js$' {
                $jsPath = Join-Path $moduleRoot "app.js"
                if (Test-Path $jsPath) {
                    $content = Get-Content $jsPath -Raw
                    $contentType = "application/javascript"
                }
            }
            
            '^/api/auth/status$' {
                $isAuthenticated = Test-AzureConnection
                $authStatus = @{
                    authenticated = $isAuthenticated
                    context = if ($isAuthenticated) {
                        $ctx = Get-AzContext
                        @{
                            account = $ctx.Account.Id
                            subscription = $ctx.Subscription.Name
                            tenant = $ctx.Tenant.Id
                        }
                    } else { $null }
                }
                $content = $authStatus | ConvertTo-Json
                $contentType = "application/json"
            }
            
            '^/api/auth/login$' {
                try {
                    Connect-AzAccount -UseDeviceAuthentication | Out-Null
                    $isAuthenticated = $true
                    $content = @{ success = $true; message = "Authentication successful" } | ConvertTo-Json
                } catch {
                    $content = @{ success = $false; message = $_.Exception.Message } | ConvertTo-Json
                }
                $contentType = "application/json"
            }
            
            '^/api/waf/config$' {
                # Serve WAF configuration
                try {
                    if ($null -ne $wafConfig) {
                        Write-Host " 📋 Serving WAF configuration" -ForegroundColor Cyan
                        $content = $wafConfig | ConvertTo-Json -Depth 20 -Compress:$false
                        Write-Host " ✅ WAF config sent ($(($content.Length / 1KB).ToString('N2')) KB)" -ForegroundColor Green
                    } else {
                        Write-Host " ⚠️ WAF configuration not available" -ForegroundColor Yellow
                        $response.StatusCode = 503
                        $content = @{ error = "WAF configuration not loaded" } | ConvertTo-Json
                    }
                } catch {
                    Write-Host " ❌ Error serving WAF config: $($_.Exception.Message)" -ForegroundColor Red
                    $response.StatusCode = 500
                    $content = @{ error = $_.Exception.Message } | ConvertTo-Json
                }
                $contentType = "application/json"
            }
            
            '^/api/inventory/progress$' {
                # Return current inventory collection progress
                try {
                    $progress = Get-CollectionProgress
                    $content = $progress | ConvertTo-Json
                } catch {
                    $response.StatusCode = 500
                    $content = @{ 
                        message = "Initializing..."
                        percent = 0
                        lastUpdate = (Get-Date).ToString('o')
                        error = $_.Exception.Message 
                    } | ConvertTo-Json
                }
                $contentType = "application/json"
            }
            
            '^/api/inventory/data$' {
                if ($isAuthenticated) {
                    try {
                        Write-Host " 📊 Collecting Azure Local inventory..." -ForegroundColor Cyan
                        # Reset progress before starting
                        Update-CollectionProgress "Starting inventory collection..." 0
                        $inventoryData = Get-AzureLocalInventory
                        $lastUpdate    = Get-Date
                        $content = $inventoryData | ConvertTo-Json -Depth 10
                    } catch {
                        $content = @{ error = $_.Exception.Message } | ConvertTo-Json
                    }
                } else {
                    $response.StatusCode = 401
                    $content = @{ error = "Not authenticated" } | ConvertTo-Json
                }
                $contentType = "application/json"
            }
            
            '^/api/inventory/refresh$' {
                if ($isAuthenticated) {
                    try {
                        Write-Host " 🔄 Refreshing inventory..." -ForegroundColor Cyan
                        # Reset progress before starting
                        Update-CollectionProgress "Starting inventory refresh..." 0
                        $inventoryData = Get-AzureLocalInventory
                        $lastUpdate    = Get-Date
                        $content = @{ 
                            success    = $true
                            lastUpdate = $lastUpdate.ToString('o')
                        } | ConvertTo-Json
                    } catch {
                        $content = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                    }
                } else {
                    $response.StatusCode = 401
                    $content = @{ success = $false; error = "Not authenticated" } | ConvertTo-Json
                }
                $contentType = "application/json"
            }
            
            default {
                $response.StatusCode = 404
                $content = "<html><body><h1>404 - Not Found</h1></body></html>"
            }
        }
        
        # Send response
        $buffer = [System.Text.Encoding]::UTF8.GetBytes($content)
        $response.ContentLength64 = $buffer.Length
        $response.ContentType = $contentType
        $response.Headers.Add("Access-Control-Allow-Origin", "*")
        $response.OutputStream.Write($buffer, 0, $buffer.Length)
        $response.Close()
    }
}
catch [System.Net.HttpListenerException] {
    # Thrown when the listener is stopped while GetContextAsync is waiting — expected on Ctrl+C.
}
catch {
    Write-Host "❌ Server error: $($_.Exception.Message)" -ForegroundColor Red
}
    finally {
        Write-Host "`n🛑 Stopping server..." -ForegroundColor Yellow
        [Console]::remove_CancelKeyPress($cancelHandler)
        if ($listener.IsListening) { $listener.Stop() }
        $listener.Close()
        $cts.Dispose()
        Write-Host "✓ Server stopped" -ForegroundColor Green
    }
}