Start-AzureLocalServer.ps1

#!/usr/bin/env pwsh
<#PSScriptInfo
.VERSION 1.1.135
.GUID b4e8c3d2-5f9a-4e7b-8c1d-2f3e4a5b6c7d
.AUTHOR Alex ter Neuzen
.COMPANYNAME GetToTheCloud
.COPYRIGHT (c) 2025 Alex ter Neuzen. All rights reserved.
.TAGS Azure AzureLocal AzureStackHCI WebServer Dashboard
.LICENSEURI https://github.com/GetToThe-Cloud/documenter-azure-local/blob/main/LICENSE
.PROJECTURI https://github.com/GetToThe-Cloud/documenter-azure-local
.ICONURI
.EXTERNALMODULEDEPENDENCIES Az.Accounts
.REQUIREDSCRIPTS Get-AzureLocalInventory.ps1
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
    v1.1.135 - Changed filtering for OSSku to look for "Azure Stack HCI"
    v1.1.124 - Cross-subscription scanning, executive PDF export support
    v1.0.0 - Initial release
.PRIVATEDATA
#>


<#
.SYNOPSIS
    Azure Local Inventory Web Server
.DESCRIPTION
    Starts a web server that provides a live view of Azure Local (Azure Stack HCI) inventory
    with authentication, navigation, and PDF export capabilities.
.PARAMETER Port
    Port number for the web server (default: 8081)
#>


param(
    [int]$Port = 8081
)

# Check PowerShell version requirement
$minimumVersion = [version]"7.0.0"
$currentVersion = $PSVersionTable.PSVersion

if ($currentVersion -lt $minimumVersion) {
    Write-Error "PowerShell 7.0 or higher is required. Current version: $currentVersion"
    Write-Host "Please install PowerShell 7 from: https://aka.ms/powershell" -ForegroundColor Yellow
    exit 1
}

# Import required modules
$ErrorActionPreference = "Stop"

Write-Host "🚀 Starting Azure Local Inventory Server..." -ForegroundColor Cyan
Write-Host "✓ PowerShell Version: $currentVersion" -ForegroundColor Green

# Check and import Azure modules
$requiredModules = @(
    'Az.Accounts', 
    'Az.Resources',
    'Az.StackHCI',
    'Az.ConnectedMachine'
)

foreach ($module in $requiredModules) {
    if (-not (Get-Module -ListAvailable -Name $module)) {
        Write-Host "⚠️ Module $module not found. Installing..." -ForegroundColor Yellow
        Install-Module -Name $module -Force -Scope CurrentUser -AllowClobber
    }
    Import-Module $module -ErrorAction SilentlyContinue
}

# Import inventory collection module
$inventoryModulePath = Join-Path $PSScriptRoot "Get-AzureLocalInventory.ps1"
. $inventoryModulePath

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

# Global state
$script:IsAuthenticated = Test-AzureConnection
$script:InventoryData = @{}
$script:LastUpdate = $null
$script:WafConfig = $null

# Load WAF Configuration
$wafConfigPath = Join-Path $PSScriptRoot "waf-config.json"
if (Test-Path $wafConfigPath) {
    try {
        $script:WafConfig = Get-Content $wafConfigPath -Raw | ConvertFrom-Json
        Write-Host "✓ WAF Configuration loaded (Version: $($script: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 ($script:IsAuthenticated) { 'Connected ✓' } else { 'Not Connected ✗' })" -ForegroundColor $(if ($script: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 ""

try {
    while ($listener.IsListening) {
        $context = $listener.GetContext()
        $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 $PSScriptRoot "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 $PSScriptRoot "styles.css"
                if (Test-Path $cssPath) {
                    $content = Get-Content $cssPath -Raw
                    $contentType = "text/css"
                }
            }
            
            '^/app\.js$' {
                $jsPath = Join-Path $PSScriptRoot "app.js"
                if (Test-Path $jsPath) {
                    $content = Get-Content $jsPath -Raw
                    $contentType = "application/javascript"
                }
            }
            
            '^/api/auth/status$' {
                $script:IsAuthenticated = Test-AzureConnection
                $authStatus = @{
                    authenticated = $script:IsAuthenticated
                    context = if ($script: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
                    $script: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 $script:WafConfig) {
                        Write-Host " 📋 Serving WAF configuration" -ForegroundColor Cyan
                        $content = $script: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 ($script:IsAuthenticated) {
                    try {
                        Write-Host " 📊 Collecting Azure Local inventory..." -ForegroundColor Cyan
                        # Reset progress before starting
                        Update-CollectionProgress "Starting inventory collection..." 0
                        $script:InventoryData = Get-AzureLocalInventory
                        $script:LastUpdate = Get-Date
                        $content = $script: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 ($script:IsAuthenticated) {
                    try {
                        Write-Host " 🔄 Refreshing inventory..." -ForegroundColor Cyan
                        # Reset progress before starting
                        Update-CollectionProgress "Starting inventory refresh..." 0
                        $script:InventoryData = Get-AzureLocalInventory
                        $script:LastUpdate = Get-Date
                        $content = @{ 
                            success = $true
                            lastUpdate = $script: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()
    }
}
finally {
    Write-Host "`n🛑 Stopping server..." -ForegroundColor Yellow
    $listener.Stop()
    $listener.Close()
    Write-Host "✓ Server stopped" -ForegroundColor Green
}