Public/Get-sqmAlwaysOnHealthReport.ps1
|
<#
.SYNOPSIS Creates a detailed health report for all Always On availability groups on an instance. .DESCRIPTION Retrieves for each AG on the specified instance: - Synchronization status of all replicas - LSN lag between primary and secondaries (redo queue, send queue) - Database status per replica (Synchronized, Synchronizing, NotSynchronizing, ...) - Connection status of replicas - Listener configuration - Running AutoSeed operations Results are saved as a TXT report, CSV file, and HTML report in the specified directory. The function also returns an object with the detail data and file paths. .PARAMETER SqlInstance SQL Server instance(s). Pipeline-capable. Default: current computer name. .PARAMETER SqlCredential Optional PSCredential for the connection. .PARAMETER MaxRedoQueueMB Warning threshold for the redo queue in MB. Default: 100. .PARAMETER MaxSendQueueMB Warning threshold for the send queue in MB. Default: 50. .PARAMETER OutputPath Output directory for report files. Default: C:\System\WinSrvLog\MSSQL .PARAMETER OutputHtml Generate HTML report (in addition to TXT and CSV). Default: $true .PARAMETER ContinueOnError Continue on error for an instance (otherwise the error is thrown). .PARAMETER EnableException Throw exceptions immediately (overrides ContinueOnError). .PARAMETER Confirm Request confirmation before writing files. .PARAMETER WhatIf Shows which files would be created without actually writing them. .EXAMPLE Get-sqmAgHealthReport .EXAMPLE Get-sqmAgHealthReport -SqlInstance "SQL01" -MaxRedoQueueMB 200 -OutputPath "D:\Reports" .NOTES Author: MSSQLTools Prerequisites: dbatools, Invoke-sqmLogging Default output path: C:\System\WinSrvLog\MSSQL #> function Get-sqmAlwaysOnHealthReport { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [string[]]$SqlInstance = @($env:COMPUTERNAME), [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $false)] [int]$MaxRedoQueueMB = 100, [Parameter(Mandatory = $false)] [int]$MaxSendQueueMB = 50, [Parameter(Mandatory = $false)] [string]$OutputPath = "C:\System\WinSrvLog\MSSQL", [Parameter(Mandatory = $false)] [switch]$OutputHtml = $true, [Parameter(Mandatory = $false)] [switch]$ContinueOnError, [Parameter(Mandatory = $false)] [switch]$EnableException, [Parameter(Mandatory = $false)] [switch]$NoOpen ) begin { $functionName = $MyInvocation.MyCommand.Name $allInstanceResults = [System.Collections.Generic.List[PSCustomObject]]::new() if (-not (Get-Module -ListAvailable -Name dbatools)) { $errMsg = "dbatools-Modul nicht gefunden." Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } Invoke-sqmLogging -Message "Starte $functionName mit OutputPath: $OutputPath" -FunctionName $functionName -Level "INFO" } process { foreach ($instance in $SqlInstance) { $connParams = @{ SqlInstance = $instance } if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential } try { Invoke-sqmLogging -Message "[$instance] Lade Always On-Informationen ..." -FunctionName $functionName -Level "INFO" # Verfuegbarkeitsgruppen abrufen $ags = Get-DbaAvailabilityGroup @connParams -ErrorAction Stop if (-not $ags) { Invoke-sqmLogging -Message "[$instance] Keine Verfuegbarkeitsgruppen vorhanden." -FunctionName $functionName -Level "INFO" continue } # SQL Server-Version ermitteln (2016=13, 2019=15, 2022=16, 2025=17) $sqlMajorVersion = 0 try { $verRow = Invoke-DbaQuery @connParams -Query "SELECT CAST(SERVERPROPERTY('ProductMajorVersion') AS INT) AS V" -EnableException:$true -ErrorAction Stop $sqlMajorVersion = [int]$verRow.V } catch { $sqlMajorVersion = 0 } # DMV-Abfrage: Replikat- und Datenbankstatus mit Queues $dmvQuery = @" SELECT ag.name AS AgName, ar.replica_server_name AS ReplicaName, ar.availability_mode_desc AS AvailabilityMode, ar.failover_mode_desc AS FailoverMode, ars.role_desc AS Role, ars.connected_state_desc AS ConnectionState, ars.synchronization_health_desc AS SyncHealth, DB_NAME(adbrs.database_id) AS DatabaseName, adbrs.synchronization_state_desc AS DbSyncState, adbrs.synchronization_health_desc AS DbSyncHealth, adbrs.redo_queue_size AS RedoQueueKB, adbrs.log_send_queue_size AS SendQueueKB, adbrs.redo_rate AS RedoRateKBs, adbrs.log_send_rate AS SendRateKBs, adbrs.is_suspended AS IsSuspended FROM sys.availability_groups ag JOIN sys.availability_replicas ar ON ar.group_id = ag.group_id JOIN sys.dm_hadr_availability_replica_states ars ON ars.replica_id = ar.replica_id LEFT JOIN sys.dm_hadr_database_replica_states adbrs ON adbrs.replica_id = ar.replica_id ORDER BY ag.name, ars.role_desc DESC, ar.replica_server_name, DB_NAME(adbrs.database_id); "@ $dmvRows = Invoke-DbaQuery @connParams -Query $dmvQuery -EnableException:$EnableException # Laufende AutoSeed-Vorgaenge (ab SQL Server 2016) - versionsspezifisch # SQL 2019+ (v15): group_database_id, transferred/total_size_bytes, estimate_time_complete # SQL 2016/2017 (v13/v14): nur Basis-Spalten, kein group_database_id # SQL 2012/2014 (v<13): DMV existiert nicht - uebersprungen $seedQuery = $null if ($sqlMajorVersion -ge 15) { $seedQuery = @" SELECT ag.name AS AgName, DB_NAME(adbrs.database_id) AS DatabaseName, has.current_state AS SeedState, has.performed_seeding_name AS SeedingType, has.start_time, has.transferred_size_bytes, has.total_size_bytes, has.estimate_time_complete FROM sys.dm_hadr_automatic_seeding has JOIN sys.availability_replicas ar ON ar.replica_id = has.local_physical_seeding_id JOIN sys.availability_groups ag ON ag.group_id = ar.group_id JOIN sys.dm_hadr_database_replica_states adbrs ON adbrs.group_database_id = has.group_database_id AND adbrs.is_local = 1 WHERE has.completion_time IS NULL ORDER BY has.start_time; "@ } elseif ($sqlMajorVersion -ge 13) { # SQL 2016 / 2017: keine group_database_id und keine neueren Groessen-Spalten $seedQuery = @" SELECT ag.name AS AgName, DB_NAME(has.local_database_id) AS DatabaseName, has.current_state AS SeedState, has.performed_seeding_name AS SeedingType, has.start_time, CAST(NULL AS BIGINT) AS transferred_size_bytes, CAST(NULL AS BIGINT) AS total_size_bytes, CAST(NULL AS DATETIME2) AS estimate_time_complete FROM sys.dm_hadr_automatic_seeding has JOIN sys.availability_replicas ar ON ar.replica_id = has.local_physical_seeding_id JOIN sys.availability_groups ag ON ag.group_id = ar.group_id WHERE has.completion_time IS NULL ORDER BY has.start_time; "@ } $seedRows = $null if ($seedQuery) { try { # EnableException:$true erzwingt echte Exception statt dbatools-Warning # (sonst greift catch nie, Warning laeuft durch) $seedRows = Invoke-DbaQuery @connParams -Query $seedQuery -EnableException:$true } catch { if ($_.Exception.Message -match 'Invalid object name|Invalid column name') { Invoke-sqmLogging -Message "[$instance] sys.dm_hadr_automatic_seeding nicht verfuegbar oder Spalten inkompatibel (SQL Server < 2016 oder aelteres CU-Build). AutoSeed-ueberwachung uebersprungen." -FunctionName $functionName -Level "VERBOSE" } elseif (-not $EnableException) { Invoke-sqmLogging -Message "[$instance] Fehler beim Lesen der AutoSeed-Daten: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING" } else { throw } } } # end if ($seedQuery) # Ergebniszeilen sammeln (pro Datenbank/Replikat) $healthRows = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($row in $dmvRows) { if (-not $row.DatabaseName) { continue } # Replikat ohne Datenbankzeile ueberspringen # NULL-sicher: bei disconnected Secondary liefert SQL NULL fuer Queue-Groessen $redoMB = [math]::Round([double]($row.RedoQueueKB -as [long]) / 1024, 1) $sendMB = [math]::Round([double]($row.SendQueueKB -as [long]) / 1024, 1) $queueStatus = if ($row.Role -ne 'PRIMARY' -and $redoMB -gt $MaxRedoQueueMB) { 'Warning' } elseif ($sendMB -gt $MaxSendQueueMB) { 'Warning' } else { 'OK' } $syncOk = ($row.DbSyncState -in @('SYNCHRONIZED', 'SYNCHRONIZING')) -and $row.ConnectionState -eq 'CONNECTED' -and -not $row.IsSuspended $overallStatus = if (-not $syncOk) { 'Critical' } elseif ($queueStatus -eq 'Warning') { 'Warning' } else { 'OK' } $healthRows.Add([PSCustomObject]@{ SqlInstance = $instance AgName = $row.AgName ReplicaName = $row.ReplicaName Role = $row.Role AvailabilityMode = $row.AvailabilityMode ConnectionState = $row.ConnectionState SyncHealth = $row.SyncHealth DatabaseName = $row.DatabaseName DbSyncState = $row.DbSyncState IsSuspended = $row.IsSuspended RedoQueueMB = $redoMB SendQueueMB = $sendMB RedoRateKBs = $row.RedoRateKBs SendRateKBs = $row.SendRateKBs OverallStatus = $overallStatus }) } # AutoSeed-Eintraege hinzufuegen (falls vorhanden) if ($seedRows) { foreach ($s in $seedRows) { $pctComplete = if ($s.total_size_bytes -gt 0) { [math]::Round($s.transferred_size_bytes / $s.total_size_bytes * 100, 1) } else { 0 } $dbSyncState = "AutoSeed: $($s.SeedState) - $pctComplete%" $healthRows.Add([PSCustomObject]@{ SqlInstance = $instance AgName = $s.AgName ReplicaName = '(AutoSeed)' Role = 'SEEDING' AvailabilityMode = 'Automatic' ConnectionState = 'CONNECTED' SyncHealth = 'SEEDING_IN_PROGRESS' DatabaseName = $s.DatabaseName DbSyncState = $dbSyncState IsSuspended = $false RedoQueueMB = 0 SendQueueMB = 0 RedoRateKBs = 0 SendRateKBs = 0 OverallStatus = 'Warning' }) } } # Berichtsdateien schreiben (nur wenn -WhatIf nicht aktiv) $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $datestamp = Get-Date -Format 'yyyy-MM-dd' $safeInst = $instance -replace '[\\/:*?"<>|]', '_' $txtFile = Join-Path $OutputPath "AgHealthReport_${safeInst}_${datestamp}.txt" $csvFile = Join-Path $OutputPath "AgHealthReport_${safeInst}_${datestamp}.csv" $htmlFile = Join-Path $OutputPath "AgHealthReport_${safeInst}_${datestamp}.html" if ($PSCmdlet.ShouldProcess($instance, "Erstelle Health-Bericht in $OutputPath")) { # Verzeichnis anlegen if (-not (Test-Path $OutputPath)) { try { $null = New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop Invoke-sqmLogging -Message "Created output directory: $OutputPath" -FunctionName $functionName -Level "INFO" } catch { $errMsg = "Failed to create output directory '$OutputPath': $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $errMsg } else { continue } } } # Verify directory is writable try { $testFile = Join-Path $OutputPath ".sqmtest" $null = "test" | Out-File -FilePath $testFile -Encoding UTF8 -Force -ErrorAction Stop Remove-Item $testFile -Force -ErrorAction SilentlyContinue } catch { $errMsg = "Output directory '$OutputPath' is not writable: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw $errMsg } else { continue } } # TXT-Bericht erstellen $cntCrit = ($healthRows | Where-Object OverallStatus -eq 'Critical').Count $cntWarn = ($healthRows | Where-Object OverallStatus -eq 'Warning').Count $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("# ================================================================") $lines.Add("# sqmSQLTool - Always On Health Report") $lines.Add("# $(Get-sqmReportReference)") $lines.Add("# Instanz : $instance") $lines.Add("# Erstellt : $timestamp") $lines.Add("# RedoQueue max: ${MaxRedoQueueMB} MB | SendQueue max: ${MaxSendQueueMB} MB") $lines.Add("# Critical: $cntCrit | Warning: $cntWarn") $lines.Add("# ================================================================") foreach ($ag in ($healthRows | Group-Object AgName)) { $lines.Add(""); $lines.Add("# AG: $($ag.Name)") $lines.Add(("{0,-30} {1,-10} {2,-8} {3,-25} {4,-12} {5,-8} {6}" -f 'Replikat', 'Status', 'Role', 'DB-SyncState', 'Connection', 'RedoMB', 'SendMB')) $lines.Add(("-" * 115)) foreach ($e in ($ag.Group | Sort-Object Role, ReplicaName, DatabaseName)) { $repName = $e.ReplicaName if ($repName.Length -gt 30) { $repName = $repName.Substring(0, 27) + '...' } $dbSync = $e.DbSyncState if ($dbSync.Length -gt 25) { $dbSync = $dbSync.Substring(0, 22) + '...' } $lines.Add(("{0,-30} {1,-10} {2,-8} {3,-25} {4,-12} {5,-8} {6}" -f $repName, $e.OverallStatus, $e.Role, $dbSync, $e.ConnectionState, $e.RedoQueueMB, $e.SendQueueMB)) } } $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force # CSV-Datei erstellen $healthRows | Export-Csv -Path $csvFile -Encoding UTF8 -NoTypeInformation -Force # HTML-Bericht erstellen (falls gewuenscht) if ($OutputHtml) { $htmlContent = _GenerateAlwaysOnHealthHtml -HealthRows $healthRows -Instance $instance -Timestamp $timestamp -MaxRedoQueueMB $MaxRedoQueueMB -MaxSendQueueMB $MaxSendQueueMB $htmlContent | Out-File -FilePath $htmlFile -Encoding UTF8 -Force Invoke-sqmLogging -Message "[$instance] HTML-Bericht erstellt: $htmlFile" -FunctionName $functionName -Level "INFO" } else { $htmlFile = $null } Invoke-sqmOpenReport -HtmlFile $htmlFile -TxtFile $txtFile -NoOpen:$NoOpen Invoke-sqmLogging -Message "[$instance] Health-Bericht erstellt: $txtFile" -FunctionName $functionName -Level "INFO" } else { Invoke-sqmLogging -Message "[$instance] WhatIf: Berichtsdateien wuerden erstellt werden." -FunctionName $functionName -Level "VERBOSE" $txtFile = $null $csvFile = $null $htmlFile = $null } # Ergebnisobjekt fuer diese Instanz $result = [PSCustomObject]@{ SqlInstance = $instance Timestamp = $timestamp HealthRows = $healthRows TxtFile = $txtFile CsvFile = $csvFile HtmlFile = $htmlFile Status = if ($cntCrit -gt 0) { 'Critical' } elseif ($cntWarn -gt 0) { 'Warning' } else { 'OK' } } $allInstanceResults.Add($result) if ($cntCrit -gt 0) { Invoke-sqmLogging -Message "[$instance] $cntCrit Critical AG-Issue(s) - Bericht: $txtFile" -FunctionName $functionName -Level "WARNING" } } catch { $errMsg = "Fehler auf '$instance': $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" $allInstanceResults.Add([PSCustomObject]@{ SqlInstance = $instance Status = 'Error' Message = $errMsg HealthRows = $null TxtFile = $null CsvFile = $null HtmlFile = $null }) if ($EnableException) { throw } if (-not $ContinueOnError) { throw $_ } } } } end { Invoke-sqmLogging -Message "$functionName abgeschlossen. $($allInstanceResults.Count) Instanzen verarbeitet." -FunctionName $functionName -Level "INFO" return $allInstanceResults } } # ============ Helper Functions ============ function _GenerateAlwaysOnHealthHtml { param( [PSCustomObject[]]$HealthRows, [string]$Instance, [string]$Timestamp, [int]$MaxRedoQueueMB, [int]$MaxSendQueueMB ) $reportDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $cntCritical = ($HealthRows | Where-Object OverallStatus -eq 'Critical').Count $cntWarning = ($HealthRows | Where-Object OverallStatus -eq 'Warning').Count $cntOk = ($HealthRows | Where-Object OverallStatus -eq 'OK').Count $totalAgs = ($HealthRows | Select-Object -ExpandProperty AgName -Unique).Count $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Always On Health Report</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Arial, sans-serif; background: #0f172a; color: #e2e8f0; padding: 20px; } .container { max-width: 1800px; margin: 0 auto; } .header { background: linear-gradient(160deg, #1e3a8a 0%, #2e5090 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border-left: 4px solid #3b82f6; } .header h1 { color: #60a5fa; font-size: 2em; margin-bottom: 10px; } .header p { color: #94a3b8; font-size: 0.95em; } .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 30px; } .stat-card { background: #0b1e3d; border: 1px solid #3b82f6; padding: 20px; border-radius: 8px; text-align: center; } .stat-value { font-size: 2.2em; font-weight: bold; margin-bottom: 5px; } .stat-label { color: #94a3b8; font-size: 0.9em; } .stat-card:nth-child(1) .stat-value { color: #60a5fa; } .stat-card:nth-child(2) .stat-value { color: #34d399; } .stat-card:nth-child(3) .stat-value { color: #f59e0b; } .stat-card:nth-child(4) .stat-value { color: #ef4444; } .controls { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; } .controls input { background: #0b1e3d; border: 1px solid #3b82f6; color: #e2e8f0; padding: 10px; border-radius: 6px; flex: 1; min-width: 200px; } .controls button { background: #1e3a8a; border: 1px solid #3b82f6; color: #60a5fa; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: bold; } .controls button:hover { background: #2e5090; } .section-title { color: #5dade2; font-size: 1.3em; margin-top: 30px; margin-bottom: 15px; font-weight: 600; border-bottom: 2px solid #3b82f6; padding-bottom: 10px; } table { width: 100%; border-collapse: collapse; background: #0b1e3d; border-radius: 8px; overflow: hidden; margin-bottom: 30px; } th { background: #1e3a8a; color: #60a5fa; padding: 15px; text-align: left; font-weight: 600; border-bottom: 2px solid #3b82f6; } td { padding: 12px 15px; border-bottom: 1px solid #2d5a8c; } tr:hover { background: #1a2f4d; } .ag-name { color: #60a5fa; font-weight: 600; } .replica-name { font-family: monospace; color: #e2e8f0; } .status-critical { color: #ef4444; font-weight: bold; } .status-warning { color: #f59e0b; font-weight: bold; } .status-ok { color: #34d399; font-weight: bold; } .role-primary { background: rgba(59, 130, 246, 0.1); color: #60a5fa; padding: 2px 8px; border-radius: 4px; font-weight: 600; } .role-secondary { background: rgba(148, 163, 184, 0.1); color: #cbd5e1; padding: 2px 8px; border-radius: 4px; } .sync-synchronized { color: #34d399; } .sync-synchronizing { color: #f59e0b; } .sync-not-synchronized { color: #ef4444; } .queue-normal { color: #34d399; } .queue-warning { color: #f59e0b; } .queue-critical { color: #ef4444; } .footer { color: #94a3b8; font-size: 0.85em; text-align: center; margin-top: 50px; padding-top: 20px; border-top: 1px solid #3b82f6; } .hidden { display: none; } </style> </head> <body> <div class="container"> <div class="header"> <h1>📊 Always On Health Report</h1> <p>Instance: <strong>$Instance</strong> | Generated: $reportDate</p> </div> <div class="stats"> <div class="stat-card"> <div class="stat-value">$totalAgs</div> <div class="stat-label">Total AGs</div> </div> <div class="stat-card"> <div class="stat-value">$cntOk</div> <div class="stat-label">OK Status</div> </div> <div class="stat-card"> <div class="stat-value">$cntWarning</div> <div class="stat-label">Warning Issues</div> </div> <div class="stat-card"> <div class="stat-value">$cntCritical</div> <div class="stat-label">Critical Issues</div> </div> </div> <div class="controls"> <input type="text" id="searchBox" placeholder="Search AG name, replica, or database..." onkeyup="filterTable()"> <button onclick="sortTable('AgName')">Sort by AG</button> <button onclick="sortTable('Status')">Sort by Status</button> </div> <div class="section-title">Replica & Database Status</div> <table id="healthTable"> <thead> <tr> <th style="width: 15%;">AG Name</th> <th style="width: 15%;">Replica Name</th> <th style="width: 8%;">Role</th> <th style="width: 15%;">Database</th> <th style="width: 12%;">Sync State</th> <th style="width: 10%;">Connection</th> <th style="width: 10%;">Redo Queue (MB)</th> <th style="width: 10%;">Send Queue (MB)</th> <th style="width: 8%;">Status</th> </tr> </thead> <tbody> "@ # Add table rows foreach ($row in $HealthRows) { $statusClass = switch ($row.OverallStatus) { 'Critical' { 'status-critical' } 'Warning' { 'status-warning' } default { 'status-ok' } } $syncClass = switch ($row.DbSyncState) { 'SYNCHRONIZED' { 'sync-synchronized' } 'SYNCHRONIZING' { 'sync-synchronizing' } default { 'sync-not-synchronized' } } $roleClass = if ($row.Role -eq 'PRIMARY') { 'role-primary' } else { 'role-secondary' } # Queue styling $redoClass = if ($row.Role -eq 'PRIMARY') { 'queue-normal' } elseif ($row.RedoQueueMB -gt $MaxRedoQueueMB) { 'queue-critical' } else { 'queue-normal' } $sendClass = if ($row.SendQueueMB -gt $MaxSendQueueMB) { 'queue-warning' } else { 'queue-normal' } $connectionState = if ($row.ConnectionState -eq 'CONNECTED') { '<span style="color: #34d399;">Connected</span>' } else { '<span style="color: #ef4444;">Disconnected</span>' } $html += @" <tr> <td class="ag-name">$($row.AgName)</td> <td class="replica-name">$($row.ReplicaName)</td> <td><span class="$roleClass">$($row.Role)</span></td> <td>$($row.DatabaseName)</td> <td class="$syncClass">$($row.DbSyncState)</td> <td>$connectionState</td> <td class="$redoClass">$($row.RedoQueueMB)</td> <td class="$sendClass">$($row.SendQueueMB)</td> <td class="$statusClass">$($row.OverallStatus)</td> </tr> "@ } $html += @" </tbody> </table> <div class="footer"> <p>Generated by sqmSQLTool | Get-sqmAlwaysOnHealthReport</p> <p>Configuration: RedoQueue Threshold=$MaxRedoQueueMB MB | SendQueue Threshold=$MaxSendQueueMB MB</p> <p>Report ID: $Timestamp</p> </div> </div> <script> function filterTable() { const input = document.getElementById('searchBox'); const filter = input.value.toLowerCase(); const table = document.getElementById('healthTable'); const rows = table.getElementsByTagName('tr'); for (let i = 1; i < rows.length; i++) { const agName = rows[i].cells[0].textContent.toLowerCase(); const replicaName = rows[i].cells[1].textContent.toLowerCase(); const dbName = rows[i].cells[3].textContent.toLowerCase(); const match = agName.includes(filter) || replicaName.includes(filter) || dbName.includes(filter); rows[i].style.display = match ? '' : 'none'; } } function sortTable(column) { const table = document.getElementById('healthTable'); const tbody = table.querySelector('tbody'); const rows = Array.from(tbody.querySelectorAll('tr')); const columnIndex = { 'AgName': 0, 'Status': 8 }[column] || 0; rows.sort((a, b) => { const aText = a.cells[columnIndex].textContent.trim(); const bText = b.cells[columnIndex].textContent.trim(); return aText.localeCompare(bText); }); rows.forEach(row => tbody.appendChild(row)); } </script> </body> </html> "@ return $html } |