diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1
index a28485d..6f5330e 100644
--- a/PiHoleShell/PiHoleShell.psm1
+++ b/PiHoleShell/PiHoleShell.psm1
@@ -34,7 +34,9 @@ Export-ModuleMember -Function @(
'Get-PiHoleList', 'Search-PiHoleListDomain', 'Add-PiHoleList', 'Remove-PiHoleList', 'Update-PiHoleList', `
#FTLInformation
'Get-PiHoleInfoMessage', 'Get-PiHoleInfoHost', 'Get-PiHoleInfoClient', 'Get-PiHoleInfoLogin', 'Get-PiHoleInfoSystem', 'Get-PiHoleInfoFtl', `
- 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', `
+ 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics', 'Get-PiHoleInfoMessageCount', 'Remove-PiHoleInfoMessage', 'Get-PiHoleLogWebserver', `
#History
- 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient'
+ 'Get-PiHoleHistory', 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryClient', 'Get-PiHoleHistoryDatabaseClient', `
+ #Teleporter
+ 'Get-PiHoleTeleporterDownload'
)
\ No newline at end of file
diff --git a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1 b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1
index 845b89d..feaff04 100644
--- a/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1
+++ b/PiHoleShell/Public/FTLInformation/Get-PiHoleLogWebserver.ps1
@@ -1,11 +1,32 @@
-
function Get-PiHoleLogWebserver {
<#
.SYNOPSIS
-Get info about logs for webserver
+Get webserver log content
+
+.DESCRIPTION
+Request content from the log of the embedded CivetWeb HTTP server. Every response includes a
+NextID; pass it back as -NextID on your next call to only get lines added since then, making
+periodic polling for new log lines easy without checking for duplicates.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER NextID
+Only return log lines added after this ID (returned as NextID on a previous call). Omit to
+get the full available log
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleLogWebserver -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
- #Work In Progress
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/logs/webserver')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
@@ -13,19 +34,16 @@ Get info about logs for webserver
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
- [int]$NextID,
+ [Nullable[int]]$NextID,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)
try {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
- if ($NextID) {
- $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver?nextId=$NextId"
-
- }
- else {
- $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver"
+ $Uri = "$($PiHoleServer.OriginalString)/api/logs/webserver"
+ if ($PSBoundParameters.ContainsKey('NextID')) {
+ $Uri += "?nextID=$NextID"
}
$Params = @{
@@ -43,13 +61,26 @@ Get info about logs for webserver
}
else {
- #$ObjectFinal = @()
+ $Log = foreach ($Item in $Response.log) {
+ [PSCustomObject]@{
+ Timestamp = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.timestamp).LocalTime
+ Message = $Item.message
+ Priority = $Item.prio
+ }
+ }
+
+ $Object = [PSCustomObject]@{
+ Log = $Log
+ NextID = $Response.nextID
+ Pid = $Response.pid
+ File = $Response.file
+ }
+ Write-Output $Object
}
}
catch {
Write-Error -Message $_.Exception.Message
- break
}
finally {
@@ -57,4 +88,4 @@ Get info about logs for webserver
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
-}
\ No newline at end of file
+}
diff --git a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1
index 50d09ba..70d58f8 100644
--- a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1
+++ b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1
@@ -3,20 +3,31 @@ function Get-PiHoleList {
.SYNOPSIS
Get lists
+.DESCRIPTION
+Request Pi-hole's subscribed allow/block lists. Omit -List to get every list; specify it to
+get just that one.
+
.PARAMETER PiHoleServer
The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
.PARAMETER Password
The API Password you generated from your PiHole server
+.PARAMETER List
+The URL of a specific list to return. Omit to return every list
+
.PARAMETER IgnoreSsl
Set to $true to skip SSL certificate validation
.PARAMETER RawOutput
This will dump the response instead of the formatted object
+.EXAMPLE
+Get-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+
+.EXAMPLE
+Get-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -List "https://hosts-file.net/ad_servers.txt"
#>
- #Work In Progress
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/lists/-list-')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
diff --git a/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1 b/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1
index 4da0524..07fc347 100644
--- a/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1
+++ b/PiHoleShell/Public/Teleporter/Get-PiHoleTeleporterDownload.ps1
@@ -1,11 +1,32 @@
function Get-PiHoleTeleporterDownload {
<#
.SYNOPSIS
-Get info about logs for webserver
+Export Pi-hole settings
+.DESCRIPTION
+Downloads an archived copy of Pi-hole's current configuration (a Teleporter backup) to disk.
+The API always returns a binary application/zip archive, not JSON, so there's no -RawOutput
+option here - the downloaded file is the only output.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER FolderPath
+The folder to save the backup file into. Must already exist
+
+.PARAMETER FileName
+The name to give the backup file, without an extension - ".zip" is appended automatically
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.EXAMPLE
+Get-PiHoleTeleporterDownload -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -FolderPath "C:\Backups" -FileName "pihole-backup"
#>
- #Work In Progress
- [CmdletBinding()]
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/teleporter')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
param (
[Parameter(Mandatory = $true)]
@@ -16,51 +37,41 @@ Get info about logs for webserver
[System.IO.DirectoryInfo]$FolderPath,
[Parameter(Mandatory = $true)]
[string]$FileName,
- [bool]$IgnoreSsl = $false,
- [bool]$RawOutput = $false
+ [bool]$IgnoreSsl = $false
)
try {
- $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
-
if (!(Test-Path -Path $FolderPath)) {
throw "$FolderPath does not exist!"
}
- $FileName = "$FileName.tar.gz"
+ $FileName = "$FileName.zip"
$OutFile = "$FolderPath\$FileName"
if (Test-Path -Path $OutFile) {
throw "$OutFile already exists!"
}
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$($PiHoleServer.OriginalString)/api/teleporter"
Method = "Get"
SkipCertificateCheck = $IgnoreSsl
- ContentType = "application/json"
+ OutFile = $OutFile
}
- $Response = Invoke-RestMethod @Params -OutFile $OutFile
-
- if ($RawOutput) {
- Write-Output $Response
- }
+ Invoke-RestMethod @Params
- else {
- $ObjectFinal = @()
- $Object = [PSCustomObject]@{
- FileName = $FileName
- FilePath = $OutFile
- RootFolder = $FolderPath
- FileSizeKB = [math]::Ceiling((Get-Item $OutFile).Length / 1KB)
- }
- $ObjectFinal += $Object
- Write-Output $ObjectFinal
+ $Object = [PSCustomObject]@{
+ FileName = $FileName
+ FilePath = $OutFile
+ RootFolder = $FolderPath
+ FileSizeKB = [math]::Ceiling((Get-Item $OutFile).Length / 1KB)
}
+ Write-Output $Object
}
catch {
Write-Error -Message $_.Exception.Message
- break
}
finally {
@@ -68,4 +79,4 @@ Get info about logs for webserver
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index b4711ea..59c247c 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ A PowerShell module for automating and scripting against the **Pi-hole v6 REST A
- [Getting an API Password](#getting-an-api-password)
- [Quick Start](#quick-start)
- [Command Reference](#command-reference)
+- [Example Output](docs/EXAMPLES.md)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)
@@ -82,6 +83,8 @@ Every function accepts the same core parameters:
Functions marked 🚧 are still under active development — signatures and output shapes may change. This section is generated from the module's actual exported functions by `tools/Update-ReadmeCommandReference.ps1`, and kept in sync automatically on every `develop` → `main` pull request — don't hand-edit the block below.
+See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every function below.
+
### Actions
@@ -113,7 +116,7 @@ Functions marked 🚧 are still under active development — signatures and outp
| Function | Description |
|---|---|
| `Add-PiHoleList` | Add a new list |
-| `Get-PiHoleList` 🚧 | Get lists |
+| `Get-PiHoleList` | Get lists |
| `Remove-PiHoleList` | Remove a list |
| `Search-PiHoleListDomain` | _No description yet_ |
| `Update-PiHoleList` | Update a list |
@@ -155,7 +158,9 @@ Functions marked 🚧 are still under active development — signatures and outp
| `Get-PiHoleInfoSensors` | Get info about various sensors |
| `Get-PiHoleInfoSystem` | Get info about various system parameters |
| `Get-PiHoleInfoVersion` | Get Pi-hole version |
+| `Get-PiHoleLogWebserver` | Get webserver log content |
| `Get-PiHolePadd` | Get summarized data for PADD |
+| `Get-PiHoleTeleporterDownload` | Export Pi-hole settings |
| `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message |
### Authentication
diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md
new file mode 100644
index 0000000..69b539a
--- /dev/null
+++ b/docs/EXAMPLES.md
@@ -0,0 +1,1048 @@
+# Example Output
+
+Real output captured from every exported function against a live Pi-hole v6 server, generated by `tools/Update-ExampleOutput.ps1`. Values (query counts, IDs, timestamps, etc.) reflect whatever that specific server had at capture time - the shape of the output is what matters here, not the exact numbers.
+
+Regenerate with:
+
+```powershell
+./tools/Update-ExampleOutput.ps1
+# or, to also capture the small number of functions with real side effects on a live server:
+./tools/Update-ExampleOutput.ps1 -IncludeDisruptive
+```
+
+## Actions
+
+### Invoke-PiHoleFlushNetwork
+
+```powershell
+Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+Status : Flushed
+
+_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_
+```
+
+### Invoke-PiHoleFlushLogs
+
+```powershell
+Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+Status : Flushed
+
+_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_
+```
+
+### Restart-PiHoleDnsService
+
+```powershell
+Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+Status : Restarted
+
+_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_
+```
+
+### Update-PiHoleActionsGravity
+
+_Rebuilds the entire gravity database - typically takes 1-2 minutes._
+
+```powershell
+Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+Status : Completed
+
+_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_
+```
+
+## DNS Control
+
+### Get-PiHoleDnsBlockingStatus
+
+```powershell
+Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Blocking : enabled
+Timer : 0
+```
+
+### Set-PiHoleDnsBlocking
+
+```powershell
+Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 60
+```
+
+```
+_Not captured this run - re-run with -IncludeDisruptive to include a live example._
+```
+
+## Group Management
+
+### New-PiHoleGroup
+
+```powershell
+New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHoleShellDocsExampleGroup" -Comment "Example group"
+```
+
+```
+
+Name : PiHoleShellDocsExampleGroup
+Comment : Example group
+Enabled : True
+Id : 4
+DateAdded : 9/26/2026 11:55:24 AM
+DateModified : 9/26/2026 11:55:24 AM
+```
+
+### Get-PiHoleGroup
+
+```powershell
+Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Name : Default
+Comment : The default group
+Enabled : True
+Id : 0
+DateAdded : 6/25/2025 12:32:45 AM
+DateModified : 6/25/2025 12:32:45 AM
+
+Name : testgroup2
+Comment :
+Enabled : True
+Id : 2
+DateAdded : 7/1/2025 10:13:36 PM
+DateModified : 7/1/2025 10:13:36 PM
+
+Name : testgroup1
+Comment :
+Enabled : True
+Id : 3
+DateAdded : 7/1/2025 10:13:40 PM
+DateModified : 7/1/2025 10:13:40 PM
+
+Name : PiHoleShellDocsExampleGroup
+Comment : Example group
+Enabled : True
+Id : 4
+DateAdded : 9/26/2026 11:55:24 AM
+DateModified : 9/26/2026 11:55:24 AM
+```
+
+### Update-PiHoleGroup
+
+```powershell
+Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHoleShellDocsExampleGroup" -Enabled $false
+```
+
+```
+
+Name : PiHoleShellDocsExampleGroup
+Comment : Example group
+Enabled : False
+Id : 4
+DateAdded : 9/26/2026 11:55:24 AM
+DateModified : 9/26/2026 11:55:56 AM
+```
+
+### Remove-PiHoleGroup
+
+```powershell
+Remove-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHoleShellDocsExampleGroup"
+```
+
+```
+
+Name : PiHoleShellDocsExampleGroup
+Status : Deleted
+```
+
+## List Management
+
+### Add-PiHoleList
+
+```powershell
+Add-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -Address "https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt" -Type Block -Comment "Example list"
+```
+
+```
+
+Address : https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt
+Comment : Example list
+Groups : {Default}
+Enabled : True
+Id : 65
+DateAdded : 9/26/2026 11:57:06 AM
+DateModified : 9/26/2026 11:57:06 AM
+Type : Block
+DateUpdated :
+Number : 0
+InvalidDomains : 0
+AbpEntries : 0
+Status : 0
+```
+
+### Get-PiHoleList
+
+```powershell
+Get-PiHoleList -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Address : https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
+Comment :
+Groups : {Default}
+Enabled : True
+Id : 51
+DateAdded : 7/6/2025 2:20:13 AM
+DateModified : 7/6/2025 2:20:13 AM
+Type : block
+DateUpdated : 7/6/2025 2:20:13 AM
+Number : 75945
+InvalidDomains : 1
+AbpEntries : 0
+Status : 2
+
+Address : https://adaway.org/hosts.txt
+Comment :
+Groups : {Default}
+Enabled : True
+Id : 52
+DateAdded : 7/6/2025 2:20:40 AM
+DateModified : 7/6/2025 2:20:40 AM
+Type : block
+DateUpdated : 7/6/2025 2:20:40 AM
+Number : 6540
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+Address : https://v.firebog.net/hosts/AdguardDNS.txt
+Comment :
+Groups : {Default}
+Enabled : True
+Id : 53
+DateAdded : 7/6/2025 2:20:46 AM
+DateModified : 7/6/2025 2:20:46 AM
+Type : block
+DateUpdated : 7/6/2025 2:20:46 AM
+Number : 181864
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+Address : https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
+Comment :
+Groups : {Default}
+Enabled : True
+Id : 54
+DateAdded : 7/6/2025 2:20:59 AM
+DateModified : 7/6/2025 2:20:59 AM
+Type : block
+DateUpdated : 7/6/2025 2:20:59 AM
+Number : 42531
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+Address : https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
+Comment :
+Groups : {Default}
+Enabled : True
+Id : 55
+DateAdded : 7/6/2025 2:21:09 AM
+DateModified : 7/6/2025 2:21:09 AM
+Type : block
+DateUpdated : 7/6/2025 2:21:09 AM
+Number : 2700
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+_(showing 5 of 15 results)_
+```
+
+### Update-PiHoleList
+
+```powershell
+Update-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -Address "https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt" -Type Block -Enabled $false
+```
+
+```
+
+Address : https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt
+Comment : Example list
+Groups : {Default}
+Enabled : False
+Id : 65
+DateAdded : 9/26/2026 11:57:06 AM
+DateModified : 9/26/2026 11:58:08 AM
+Type : Block
+DateUpdated :
+Number : 0
+InvalidDomains : 0
+AbpEntries : 0
+Status : 0
+```
+
+### Search-PiHoleListDomain
+
+```powershell
+Search-PiHoleListDomain -PiHoleServer $PiHoleServer -Password $Password -Domain "doubleclick.net"
+```
+
+```
+
+Domain : doubleclick.net
+Address : https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
+Comment :
+Enabled : True
+Id : 51
+Type : block
+Groups : {0}
+DateAdded : 7/6/2025 2:20:13 AM
+DateModified : 7/6/2025 2:20:13 AM
+DateUpdated : 9/25/2026 3:30:51 PM
+Number : 75945
+InvalidDomains : 1
+AbpEntries : 0
+Status : 2
+
+Domain : doubleclick.net
+Address : https://adaway.org/hosts.txt
+Comment :
+Enabled : True
+Id : 52
+Type : block
+Groups : {0}
+DateAdded : 7/6/2025 2:20:40 AM
+DateModified : 7/6/2025 2:20:40 AM
+DateUpdated : 7/6/2025 2:25:21 AM
+Number : 6540
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+Domain : doubleclick.net
+Address : https://v.firebog.net/hosts/AdguardDNS.txt
+Comment :
+Enabled : True
+Id : 53
+Type : block
+Groups : {0}
+DateAdded : 7/6/2025 2:20:46 AM
+DateModified : 7/6/2025 2:20:46 AM
+DateUpdated : 9/25/2026 12:13:06 PM
+Number : 181864
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+Domain : doubleclick.net
+Address : https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
+Comment :
+Enabled : True
+Id : 54
+Type : block
+Groups : {0}
+DateAdded : 7/6/2025 2:20:59 AM
+DateModified : 7/6/2025 2:20:59 AM
+DateUpdated : 12/27/2025 9:28:21 PM
+Number : 42531
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+Domain : doubleclick.net
+Address : https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
+Comment :
+Enabled : True
+Id : 55
+Type : block
+Groups : {0}
+DateAdded : 7/6/2025 2:21:09 AM
+DateModified : 7/6/2025 2:21:09 AM
+DateUpdated : 6/6/2026 11:13:26 PM
+Number : 2700
+InvalidDomains : 0
+AbpEntries : 0
+Status : 2
+
+_(showing 5 of 7 results)_
+```
+
+### Remove-PiHoleList
+
+```powershell
+Remove-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -Address "https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt" -Type Block
+```
+
+```
+
+Address : https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt
+Type : Block
+Status : Removed
+```
+
+## Metrics
+
+### Get-PiHoleStatsSummary
+
+```powershell
+Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Total : 0
+Blocked : 0
+PercentBlocked : 0
+UniqueDomains : 0
+Forwarded : 0
+Cached : 0
+Frequency : 0
+Types : @{A=0; AAAA=0; ANY=0; SRV=0; SOA=0; PTR=0; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0}
+Status : @{Unknown=0; Gravity=0; Forwarded=0; Cache=0; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=0; ExternalBlockedEde15=0}
+Replies : @{Unknown=0; Nodata=0; Nxdomain=0; Cname=0; Ip=0; Domain=0; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0}
+Clients : @{Active=0; Total=0}
+Gravity : @{DomainsBeingBlocked=500421; LastUpdate=1790441671}
+```
+
+### Get-PiHoleStatsRecentBlocked
+
+```powershell
+Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Get-PiHoleStatsQueryType
+
+```powershell
+Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Type : A
+Count : 0
+
+Type : AAAA
+Count : 0
+
+Type : ANY
+Count : 0
+
+Type : SRV
+Count : 0
+
+Type : SOA
+Count : 0
+
+_(showing 5 of 16 results)_
+```
+
+### Get-PiHoleStatsTopDomain
+
+```powershell
+Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Get-PiHoleStatsTopClient
+
+```powershell
+Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Get-PiHoleStatsUpstream
+
+```powershell
+Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+TotalQueries : 0
+ForwardedQueries : 0
+Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=0; ResponseTime=0; Variance=0}}
+```
+
+### Get-PiHoleStatsQuerySuggestions
+
+```powershell
+Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Domain : {}
+ClientIp : {}
+ClientName : {}
+Upstream : {blocklist, cache, permitted}
+Type : {A, AAAA, ANY, SRV…}
+Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…}
+Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…}
+Dnssec : {UNKNOWN, SECURE, INSECURE, BOGUS…}
+```
+
+### Get-PiHoleStatsDatabaseSummary
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+SumQueries : 0
+SumBlocked : 0
+PercentBlocked : 0
+TotalClients : 0
+```
+
+### Get-PiHoleStatsDatabaseQueryType
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Type : A
+Count : 0
+
+Type : AAAA
+Count : 0
+
+Type : ANY
+Count : 0
+
+Type : SRV
+Count : 0
+
+Type : SOA
+Count : 0
+
+_(showing 5 of 16 results)_
+```
+
+### Get-PiHoleStatsDatabaseTopDomain
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Get-PiHoleStatsDatabaseTopClient
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Get-PiHoleStatsDatabaseUpstream
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+TotalQueries : 0
+ForwardedQueries : 0
+Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=0; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}}
+```
+
+## Configuration & Diagnostics
+
+### Get-PiHoleConfig
+
+```powershell
+Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Dns : @{Upstreams=System.Object[]; CNAMEdeepInspect=True; BlockESNI=True; EDNS0ECS=True; IgnoreLocalhost=False; ShowDNSSEC=True; AnalyzeOnlyAandAAAA=False; PiholePTR=PI.HOLE; ReplyWhenBusy=ALLOW; BlockTTL=2; Hosts=System.Object[]; DomainNeeded=False; ExpandHosts=False; BogusPriv=True; Dnssec=False; Interface=wlan0; HostRecord=; ListeningMode=LOCAL; QueryLogging=False; CnameRecords=; Port=53; Localise=True; RevServers=; Domain=; Cache=; Blocking=; SpecialDomains=; Reply=; RateLimit=}
+Dhcp : @{Active=False; Start=; End=; Router=; Netmask=; LeaseTime=; Ipv6=False; RapidCommit=False; MultiDNS=False; Logging=False; IgnoreUnknownClients=False; Hosts=}
+Ntp : @{Ipv4=; Ipv6=; Sync=}
+Resolver : @{ResolveIPv4=True; ResolveIPv6=True; MacNames=True; NetworkNames=True; RefreshNames=IPV4_ONLY}
+Database : @{DBimport=True; MaxDBdays=2; DBinterval=60; UseWAL=True; ForceDisk=False; Network=}
+Webserver : @{Domain=pi.hole; Acl=; Port=8089,8489s; Threads=50; Headers=System.Object[]; ServeAll=False; AdvancedOpts=; Session=; Tls=; Paths=; Interface=; Api=}
+Files : @{Database=/etc/pihole/pihole-FTL.db; TmpDb=/etc/pihole/pihole-tmp.db; Gravity=/etc/pihole/gravity.db; GravityTmp=/tmp; Macvendor=/etc/pihole/macvendor.db; Pcap=; Log=}
+Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsmasqD=False; DnsmasqLines=; ExtraLogging=False; ReadOnly=False; NormalizeCPU=True; HideDnsmasqWarn=False; HideConnectionError=False; Check=}
+Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=False; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False}
+```
+
+### Get-PiHolePadd
+
+```powershell
+Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+CpuPercent : 0
+MemoryPercent : 0
+ActiveClients : 0
+Blocking : enabled
+Cache : @{Size=10000; Inserted=0; Evicted=0}
+Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0}
+GravitySize : 500421
+HostModel : Raspberry Pi Zero W Rev 1.1
+IFace : @{v4=; v6=}
+NodeName : dns3.localdomain
+Pid : 389
+Queries : @{Total=0; Blocked=0; PercentBlocked=0; QueryFrequency=0}
+RecentBlocked :
+Sensors : @{CpuTemp=40.622; HotLimit=60; Unit=C}
+System : @{Uptime=558344; Memory=; Procs=75; Cpu=; Ftl=}
+TopBlocked :
+TopClient :
+TopDomain :
+Version : @{Core=; Web=; Ftl=; Docker=}
+```
+
+### Get-PiHoleInfoHost
+
+```powershell
+Get-PiHoleInfoHost -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+DomainName : (none)
+Machine : armv6l
+NodeName : dns3.localdomain
+Release : 6.12.109+rpt-rpi-v6
+SysName : Linux
+Version : #1 Raspbian 1:6.12.109-1+rpt1 (2026-09-11)
+Model : Raspberry Pi Zero W Rev 1.1
+BiosVendor :
+BoardName :
+BoardVendor :
+BoardVersion :
+ProductName :
+ProductFamily :
+ProductVersion :
+SysVendor :
+```
+
+### Get-PiHoleInfoSystem
+
+```powershell
+Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Uptime : 558364
+Memory : @{Ram=; Swap=}
+Procs : 74
+Cpu : @{NumProcessors=1; PercentCpu=100.800003051758; Load=}
+Ftl : @{PercentMemory=2.29744291305542; PercentCpu=49.7000007629395}
+```
+
+### Get-PiHoleInfoFtl
+
+```powershell
+Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Database : @{Gravity=500421; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=}
+PrivacyLevel : 0
+QueryFrequency : 0
+Clients : @{Total=0; Active=0}
+Pid : 389
+Uptime : 160835.739884
+PercentMemory : 2.29415488243103
+PercentCpu : 49.7999992370605
+AllowDestructive : True
+Dnsmasq : @{DnsCacheInserted=0; DnsCacheLiveFreed=0; DnsQueriesForwarded=0; DnsAuthAnswered=0; DnsLocalAnswered=0; DnsStaleAnswered=0; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0}
+```
+
+### Get-PiHoleInfoSensors
+
+```powershell
+Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}}
+CpuTemp : 40.084
+HotLimit : 60
+Unit : C
+```
+
+### Get-PiHoleInfoDatabase
+
+```powershell
+Get-PiHoleInfoDatabase -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Size : 13983744
+Type : Regular file
+Mode : rw-r-----
+AccessTime : 6/25/2025 12:32:44 AM
+ModifiedTime : 9/26/2026 11:50:35 AM
+ChangeTime : 9/26/2026 11:50:35 AM
+Owner : @{User=; Group=}
+Queries : 0
+EarliestTimestamp : 9/25/2026 11:50:36 AM
+QueriesDisk : 0
+EarliestTimestampDisk :
+SqliteVersion : 3.53.1
+```
+
+### Get-PiHoleInfoVersion
+
+```powershell
+Get-PiHoleInfoVersion -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Core : @{Local=; Remote=}
+Web : @{Local=; Remote=}
+Ftl : @{Local=; Remote=}
+Docker : @{Local=; Remote=}
+```
+
+### Get-PiHoleInfoMetrics
+
+```powershell
+Get-PiHoleInfoMetrics -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Dns : @{Cache=; Replies=}
+Dhcp : @{Ack=0; Nak=0; Decline=0; Offer=0; Discover=0; Inform=0; Request=0; Release=0; NoAnswer=0; Bootp=0; Pxe=0; Leases=}
+```
+
+### Get-PiHoleInfoClient
+
+_Does not require -Password - the API does not authenticate this endpoint._
+
+```powershell
+Get-PiHoleInfoClient -PiHoleServer $PiHoleServer
+```
+
+```
+
+RemoteAddress : 192.168.1.162
+HttpVersion : 1.1
+Method : GET
+Headers : {@{Name=Host; Value=dns3.localdomain:8489}, @{Name=User-Agent; Value=Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6}, @{Name=Accept-Encoding; Value=gzip, deflate, br}, @{Name=Content-Type; Value=application/json}…}
+```
+
+### Get-PiHoleInfoLogin
+
+_Does not require -Password - the API does not authenticate this endpoint (it is meant to be usable before logging in)._
+
+```powershell
+Get-PiHoleInfoLogin -PiHoleServer $PiHoleServer
+```
+
+```
+
+HttpsPort : 8489
+Dns : True
+```
+
+### Get-PiHoleInfoMessage
+
+```powershell
+Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Id : 1
+Timestamp : 9/26/2026 11:50:37 AM
+Type : LOAD
+Plain : Long-term load (15min avg) larger than number of processors: 1.6 > 1
+Html : Long-term load (15min avg) larger than number of processors: 1.6 > 1
This may slow down DNS resolution and can cause bottlenecks.
+```
+
+### Get-PiHoleInfoMessageCount
+
+```powershell
+Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Count : 1
+```
+
+### Remove-PiHoleInfoMessage
+
+_Diagnosis messages arise from real FTL warnings and cannot be manufactured on demand, so this example shows the error for a message ID that does not exist rather than a fabricated success._
+
+```powershell
+Remove-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password -MessageId 3
+```
+
+```
+
+Error : Response status code does not indicate success: 404 (Not Found).
+```
+
+### Get-PiHoleLogWebserver
+
+```powershell
+Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Log : @{Timestamp=9/26/2026 11:50:37 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}
+NextID : 1
+Pid : 389
+File : /var/log/pihole/webserver.log
+```
+
+### Get-PiHoleTeleporterDownload
+
+_FolderPath/FileName are yours to choose; the captured output below used a scratch temp folder for this run instead of C:\Backups._
+
+```powershell
+Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -FolderPath "C:\Backups" -FileName "pihole-backup"
+```
+
+```
+
+FileName : pihole-backup.zip
+FilePath : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample\pihole-backup.zip
+RootFolder : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample
+FileSizeKB : 24
+```
+
+### Get-PiHoleHistory
+
+```powershell
+Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Timestamp : 9/25/2026 12:05:00 PM
+Total : 0
+Cached : 0
+Blocked : 0
+Forwarded : 0
+
+Timestamp : 9/25/2026 12:15:00 PM
+Total : 0
+Cached : 0
+Blocked : 0
+Forwarded : 0
+
+Timestamp : 9/25/2026 12:25:00 PM
+Total : 0
+Cached : 0
+Blocked : 0
+Forwarded : 0
+
+Timestamp : 9/25/2026 12:35:00 PM
+Total : 0
+Cached : 0
+Blocked : 0
+Forwarded : 0
+
+Timestamp : 9/25/2026 12:45:00 PM
+Total : 0
+Cached : 0
+Blocked : 0
+Forwarded : 0
+
+_(showing 5 of 145 results)_
+```
+
+### Get-PiHoleHistoryClient
+
+```powershell
+Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Timestamp : 9/25/2026 12:05:00 PM
+Clients : @{IP=others; Name=; Count=0}
+
+Timestamp : 9/25/2026 12:15:00 PM
+Clients : @{IP=others; Name=; Count=0}
+
+Timestamp : 9/25/2026 12:25:00 PM
+Clients : @{IP=others; Name=; Count=0}
+
+Timestamp : 9/25/2026 12:35:00 PM
+Clients : @{IP=others; Name=; Count=0}
+
+Timestamp : 9/25/2026 12:45:00 PM
+Clients : @{IP=others; Name=; Count=0}
+
+_(showing 5 of 145 results)_
+```
+
+### Get-PiHoleHistoryDatabase
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Get-PiHoleHistoryDatabaseClient
+
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
+
+```powershell
+Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+## Authentication
+
+### Get-PiHoleCurrentAuthSession
+
+```powershell
+Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Id : 0
+CurrentSession : False
+Valid : True
+TlsLogin : True
+TlsMixed : False
+LoginAt : 9/26/2026 11:26:28 AM
+LastActive : 9/26/2026 11:26:28 AM
+ValidUntil : 9/26/2026 11:56:28 AM
+RemoteAddress : 192.168.1.162
+UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
+XForwardedFor :
+App : True
+Cli : False
+
+Id : 1
+CurrentSession : False
+Valid : True
+TlsLogin : True
+TlsMixed : False
+LoginAt : 9/26/2026 11:50:14 AM
+LastActive : 9/26/2026 11:50:23 AM
+ValidUntil : 9/26/2026 12:20:23 PM
+RemoteAddress : 192.168.1.162
+UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
+XForwardedFor :
+App : True
+Cli : False
+
+Id : 2
+CurrentSession : False
+Valid : True
+TlsLogin : True
+TlsMixed : False
+LoginAt : 9/26/2026 11:50:38 AM
+LastActive : 9/26/2026 11:50:42 AM
+ValidUntil : 9/26/2026 12:20:42 PM
+RemoteAddress : 192.168.1.162
+UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
+XForwardedFor :
+App : True
+Cli : False
+
+Id : 3
+CurrentSession : True
+Valid : True
+TlsLogin : True
+TlsMixed : False
+LoginAt : 9/26/2026 11:51:30 AM
+LastActive : 9/26/2026 11:51:38 AM
+ValidUntil : 9/26/2026 12:21:38 PM
+RemoteAddress : 192.168.1.162
+UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
+XForwardedFor :
+App : True
+Cli : False
+```
+
+### Remove-PiHoleAuthSession
+
+_Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller's own session._
+
+```powershell
+Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 3
+```
+
+```
+
+
+Id : 3
+Status : Removed
+```
+
+### Remove-PiHoleCurrentAuthSession
+
+_The internal best-effort logout every other function calls automatically when it finishes - it never produces output, even on failure (a warning only). Shown here for completeness since it is still a publicly exported function._
+
+```powershell
+Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid
+```
+
+```
+(no output)
+```
diff --git a/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1 b/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1
new file mode 100644
index 0000000..05e5d71
--- /dev/null
+++ b/tests/FTLInformation/Get-PiHoleLogWebserver.Integration.Tests.ps1
@@ -0,0 +1,55 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleLogWebserver (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ It 'returns webserver log content as a formatted object' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.NextID | Should -BeGreaterOrEqual 0
+ $result.File | Should -Not -BeNullOrEmpty
+ }
+
+ It 'only returns lines added after NextID on a follow-up call' -Skip:(-not $script:ConfigAvailable) {
+ $first = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl
+
+ $second = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -NextID $first.NextID
+ $second | Format-List | Out-String | Write-Host
+
+ # No guarantee new webserver log lines were generated between the two calls, so this
+ # just confirms the call succeeds and NextID never goes backwards.
+ $second.NextID | Should -BeGreaterOrEqual $first.NextID
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result.PSObject.Properties.Name | Should -Contain 'log'
+ $result.PSObject.Properties.Name | Should -Contain 'nextID'
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleLogWebserver -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1 b/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1
new file mode 100644
index 0000000..730f051
--- /dev/null
+++ b/tests/Teleporter/Get-PiHoleTeleporterDownload.Integration.Tests.ps1
@@ -0,0 +1,72 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleTeleporterDownload (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+
+ $script:TestFolder = Join-Path ([System.IO.Path]::GetTempPath()) "PiHoleShellPesterTeleporter"
+ if (Test-Path $script:TestFolder) {
+ Remove-Item -Path $script:TestFolder -Recurse -Force
+ }
+ New-Item -ItemType Directory -Path $script:TestFolder | Out-Null
+ }
+
+ AfterAll {
+ if (Test-Path $script:TestFolder) {
+ Remove-Item -Path $script:TestFolder -Recurse -Force
+ }
+ }
+
+ It 'downloads a real ZIP backup and returns a formatted object' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'pester-backup'
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.FileName | Should -Be 'pester-backup.zip'
+ $result.FileSizeKB | Should -BeGreaterThan 0
+ Test-Path $result.FilePath | Should -BeTrue
+
+ # Confirm it's a genuine, openable ZIP archive, not just a file with a .zip name.
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($result.FilePath)
+ try {
+ $zip.Entries.Count | Should -BeGreaterThan 0
+ }
+ finally {
+ $zip.Dispose()
+ }
+ }
+
+ It 'errors when the destination file already exists' -Skip:(-not $script:ConfigAvailable) {
+ Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'duplicate-backup' | Out-Null
+
+ $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'duplicate-backup' -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when the destination folder does not exist' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath (Join-Path $script:TestFolder 'does-not-exist') -FileName 'x' -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleTeleporterDownload -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -FolderPath $script:TestFolder -FileName 'bad-password-backup' -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1
new file mode 100644
index 0000000..c72e23c
--- /dev/null
+++ b/tools/Update-ExampleOutput.ps1
@@ -0,0 +1,375 @@
+<#
+.SYNOPSIS
+Regenerates docs/EXAMPLES.md with real, captured output from every exported PiHoleShell function.
+
+.DESCRIPTION
+Runs each exported function against a real Pi-hole server (using tests/IntegrationConfig.local.ps1
+for credentials, same as the integration tests) and captures its actual output into a per-function
+Markdown section, grouped the same way as README.md's Command Reference.
+
+State-changing functions that create/update/delete a real resource (groups, lists, sessions) do so
+against a clearly-named throwaway resource ("PiHoleShellDocsExample...") that is removed immediately
+after its output is captured - the same create/verify/cleanup pattern already used throughout this
+module's own integration tests.
+
+Functions with real, non-trivial side effects on a live system (rebuilding gravity, restarting the
+DNS resolver, flushing the network/query log tables, toggling DNS blocking) are NOT invoked by
+default. Pass -IncludeDisruptive to also live-capture those; without it, their sections keep a
+static, dated example that was captured previously and is embedded in this script.
+
+.PARAMETER IncludeDisruptive
+Also live-run functions with real side effects on a running Pi-hole server: gravity rebuild, DNS
+service restart, network/query-log flush, and DNS blocking toggle. Off by default.
+
+.PARAMETER RepoRoot
+Path to the repository root. Defaults to the parent of this script's folder.
+#>
+[CmdletBinding()]
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Console progress/status output for an interactive tool script, not module code.')]
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Invoke-Quietly intentionally swallows errors for best-effort setup/cleanup calls that are not the example being documented.')]
+param (
+ [switch]$IncludeDisruptive,
+ [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..'))
+)
+
+$ErrorActionPreference = 'Stop'
+
+$modulePath = Join-Path $RepoRoot 'PiHoleShell/PiHoleShell.psm1'
+$configPath = Join-Path $RepoRoot 'tests/IntegrationConfig.local.ps1'
+$outputPath = Join-Path $RepoRoot 'docs/EXAMPLES.md'
+
+if (-not (Test-Path $configPath)) {
+ throw "Missing $configPath - copy it from tests/IntegrationConfig.example.ps1 and fill in your real Pi-hole server details first."
+}
+
+Import-Module $modulePath -Force
+. $configPath
+# $PiHoleServer, $PiHoleToken, $PiHoleIgnoreSsl are now set by the config file above.
+
+$categoryOrder = [ordered]@{
+ Actions = 'Actions'
+ DnsControl = 'DNS Control'
+ GroupManagement = 'Group Management'
+ ListManagement = 'List Management'
+ Metrics = 'Metrics'
+ Config = 'Configuration & Diagnostics'
+ Authentication = 'Authentication'
+}
+$sectionsByCategory = [ordered]@{}
+foreach ($key in $categoryOrder.Keys) { $sectionsByCategory[$key] = [System.Collections.Generic.List[object]]::new() }
+
+function Add-Example {
+ param (
+ [Parameter(Mandatory)] [string]$Category,
+ [Parameter(Mandatory)] [string]$FunctionName,
+ [Parameter(Mandatory)] [string]$Invocation,
+ [string]$Note,
+ $Result,
+ [switch]$Skipped,
+ [string]$StaticOutput
+ )
+
+ if ($Skipped -and $StaticOutput) {
+ $output = "$StaticOutput`n`n_(A real example captured previously - not re-run by default since this function has a real side effect on a live server. Pass -IncludeDisruptive to capture a fresh one.)_"
+ }
+ elseif ($Skipped) {
+ $output = "_Not captured this run - re-run with -IncludeDisruptive to include a live example._"
+ }
+ elseif ($null -eq $Result -or ($Result -is [array] -and $Result.Count -eq 0)) {
+ $output = '(no output)'
+ }
+ else {
+ $items = @($Result)
+ $truncated = $items.Count -gt 5
+ $shown = if ($truncated) { $items | Select-Object -First 5 } else { $items }
+ $output = ($shown | Format-List | Out-String).TrimEnd()
+ if ($truncated) {
+ $output += "`n`n_(showing 5 of $($items.Count) results)_"
+ }
+ }
+
+ $sectionsByCategory[$Category].Add([PSCustomObject]@{
+ FunctionName = $FunctionName
+ Invocation = $Invocation
+ Note = $Note
+ Output = $output
+ })
+}
+
+function Invoke-Quietly {
+ # Runs a state-changing call purely for its side effect (setup/cleanup between examples)
+ # without that call itself becoming a documented example.
+ param([scriptblock]$ScriptBlock)
+ try { & $ScriptBlock | Out-Null } catch { }
+}
+
+Write-Host "Capturing example output against $PiHoleServer ..."
+
+#region Authentication
+Add-Example -Category Authentication -FunctionName 'Get-PiHoleCurrentAuthSession' `
+ -Invocation 'Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+# Remove-PiHoleAuthSession/-CurrentAuthSession both end a session, so each needs its own throwaway
+# session to remove rather than the one the documented call itself would otherwise need to log in
+# with - removing your only active session out from under yourself isn't something to demonstrate.
+$null = & (Get-Module PiHoleShell) { param($s, $p, $i) Request-PiHoleAuth -PiHoleServer $s -Password $p -IgnoreSsl $i } $PiHoleServer $PiHoleToken $PiHoleIgnoreSsl
+Start-Sleep -Seconds 1
+# Excludes CurrentSession (Get-PiHoleCurrentAuthSession's own transient session for this very
+# call) so this reliably picks $extraSid1 above rather than the session used to look it up.
+$extraSessionId = (Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl | Where-Object { -not $_.CurrentSession } | Sort-Object LoginAt -Descending | Select-Object -First 1).Id
+Add-Example -Category Authentication -FunctionName 'Remove-PiHoleAuthSession' `
+ -Invocation "Remove-PiHoleAuthSession -PiHoleServer `$PiHoleServer -Password `$Password -Id $extraSessionId" `
+ -Note 'Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller''s own session.' `
+ -Result (Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Id $extraSessionId)
+
+$extraSid2 = & (Get-Module PiHoleShell) { param($s, $p, $i) Request-PiHoleAuth -PiHoleServer $s -Password $p -IgnoreSsl $i } $PiHoleServer $PiHoleToken $PiHoleIgnoreSsl
+Add-Example -Category Authentication -FunctionName 'Remove-PiHoleCurrentAuthSession' `
+ -Invocation 'Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid' `
+ -Note 'The internal best-effort logout every other function calls automatically when it finishes - it never produces output, even on failure (a warning only). Shown here for completeness since it is still a publicly exported function.' `
+ -Result $null
+Invoke-Quietly { Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $extraSid2 -IgnoreSsl $PiHoleIgnoreSsl }
+#endregion
+
+#region DnsControl
+Add-Example -Category DnsControl -FunctionName 'Get-PiHoleDnsBlockingStatus' `
+ -Invocation 'Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+if ($IncludeDisruptive) {
+ Add-Example -Category DnsControl -FunctionName 'Set-PiHoleDnsBlocking' `
+ -Invocation 'Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 5' `
+ -Note 'Temporarily disables blocking, then it re-enables automatically after the given time.' `
+ -Result (Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Blocking False -TimeInSeconds 5)
+ Start-Sleep -Seconds 6
+}
+else {
+ Add-Example -Category DnsControl -FunctionName 'Set-PiHoleDnsBlocking' `
+ -Invocation 'Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 60' -Skipped
+}
+#endregion
+
+#region Config
+Add-Example -Category Config -FunctionName 'Get-PiHoleConfig' `
+ -Invocation 'Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHolePadd' `
+ -Invocation 'Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+foreach ($fn in 'Get-PiHoleInfoHost', 'Get-PiHoleInfoSystem', 'Get-PiHoleInfoFtl', 'Get-PiHoleInfoSensors', 'Get-PiHoleInfoDatabase', 'Get-PiHoleInfoVersion', 'Get-PiHoleInfoMetrics') {
+ Add-Example -Category Config -FunctionName $fn `
+ -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" `
+ -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+}
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleInfoClient' `
+ -Invocation 'Get-PiHoleInfoClient -PiHoleServer $PiHoleServer' `
+ -Note 'Does not require -Password - the API does not authenticate this endpoint.' `
+ -Result (Get-PiHoleInfoClient -PiHoleServer $PiHoleServer -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleInfoLogin' `
+ -Invocation 'Get-PiHoleInfoLogin -PiHoleServer $PiHoleServer' `
+ -Note 'Does not require -Password - the API does not authenticate this endpoint (it is meant to be usable before logging in).' `
+ -Result (Get-PiHoleInfoLogin -PiHoleServer $PiHoleServer -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleInfoMessage' `
+ -Invocation 'Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleInfoMessageCount' `
+ -Invocation 'Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+$dummyMessageId = 999999
+Remove-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -MessageId $dummyMessageId -Confirm:$false -ErrorVariable removeMessageError -ErrorAction SilentlyContinue | Out-Null
+Add-Example -Category Config -FunctionName 'Remove-PiHoleInfoMessage' `
+ -Invocation 'Remove-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password -MessageId 3' `
+ -Note 'Diagnosis messages arise from real FTL warnings and cannot be manufactured on demand, so this example shows the error for a message ID that does not exist rather than a fabricated success.' `
+ -Result $(if ($removeMessageError) { [PSCustomObject]@{ Error = $removeMessageError[-1].Exception.Message } })
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleLogWebserver' `
+ -Invocation 'Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+$teleporterFolder = Join-Path ([System.IO.Path]::GetTempPath()) 'PiHoleShellDocsExample'
+if (Test-Path $teleporterFolder) { Remove-Item $teleporterFolder -Recurse -Force }
+New-Item -ItemType Directory -Path $teleporterFolder | Out-Null
+Add-Example -Category Config -FunctionName 'Get-PiHoleTeleporterDownload' `
+ -Invocation 'Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -FolderPath "C:\Backups" -FileName "pihole-backup"' `
+ -Note 'FolderPath/FileName are yours to choose; the captured output below used a scratch temp folder for this run instead of C:\Backups.' `
+ -Result (Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -FolderPath $teleporterFolder -FileName 'pihole-backup')
+Remove-Item $teleporterFolder -Recurse -Force
+#endregion
+
+#region GroupManagement
+$docsGroupName = 'PiHoleShellDocsExampleGroup'
+Invoke-Quietly { Remove-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName -WarningAction SilentlyContinue }
+
+Add-Example -Category GroupManagement -FunctionName 'New-PiHoleGroup' `
+ -Invocation "New-PiHoleGroup -PiHoleServer `$PiHoleServer -Password `$Password -GroupName `"$docsGroupName`" -Comment `"Example group`"" `
+ -Result (New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName -Comment 'Example group')
+
+# Same settle-time reasoning as the list section below - give the test server a moment before
+# relying on the group just being created.
+Start-Sleep -Seconds 3
+
+Add-Example -Category GroupManagement -FunctionName 'Get-PiHoleGroup' `
+ -Invocation 'Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category GroupManagement -FunctionName 'Update-PiHoleGroup' `
+ -Invocation "Update-PiHoleGroup -PiHoleServer `$PiHoleServer -Password `$Password -GroupName `"$docsGroupName`" -Enabled `$false" `
+ -Result (Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName -Enabled $false)
+
+Add-Example -Category GroupManagement -FunctionName 'Remove-PiHoleGroup' `
+ -Invocation "Remove-PiHoleGroup -PiHoleServer `$PiHoleServer -Password `$Password -GroupName `"$docsGroupName`"" `
+ -Result (Remove-PiHoleGroup -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -GroupName $docsGroupName)
+#endregion
+
+#region ListManagement
+$docsListAddress = 'https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt'
+Invoke-Quietly { Remove-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Confirm:$false }
+
+Add-Example -Category ListManagement -FunctionName 'Add-PiHoleList' `
+ -Invocation "Add-PiHoleList -PiHoleServer `$PiHoleServer -Password `$Password -Address `"$docsListAddress`" -Type Block -Comment `"Example list`"" `
+ -Result (Add-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Comment 'Example list')
+
+# The test server takes a moment to make a just-added list queryable again - a real hardware
+# limitation of this Pi Zero W, not a module bug (confirmed by re-running the same calls a few
+# seconds apart by hand). Give it a moment before relying on the list existing below.
+Start-Sleep -Seconds 3
+
+Add-Example -Category ListManagement -FunctionName 'Get-PiHoleList' `
+ -Invocation 'Get-PiHoleList -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category ListManagement -FunctionName 'Update-PiHoleList' `
+ -Invocation "Update-PiHoleList -PiHoleServer `$PiHoleServer -Password `$Password -Address `"$docsListAddress`" -Type Block -Enabled `$false" `
+ -Result (Update-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Enabled $false)
+
+Add-Example -Category ListManagement -FunctionName 'Search-PiHoleListDomain' `
+ -Invocation 'Search-PiHoleListDomain -PiHoleServer $PiHoleServer -Password $Password -Domain "doubleclick.net"' `
+ -Result (Search-PiHoleListDomain -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Domain 'doubleclick.net')
+
+Add-Example -Category ListManagement -FunctionName 'Remove-PiHoleList' `
+ -Invocation "Remove-PiHoleList -PiHoleServer `$PiHoleServer -Password `$Password -Address `"$docsListAddress`" -Type Block" `
+ -Result (Remove-PiHoleList -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Address $docsListAddress -Type Block -Confirm:$false)
+#endregion
+
+#region Metrics
+foreach ($fn in 'Get-PiHoleStatsSummary', 'Get-PiHoleStatsRecentBlocked', 'Get-PiHoleStatsQueryType', 'Get-PiHoleStatsTopDomain', 'Get-PiHoleStatsTopClient', 'Get-PiHoleStatsUpstream', 'Get-PiHoleStatsQuerySuggestions') {
+ Add-Example -Category Metrics -FunctionName $fn `
+ -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" `
+ -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+}
+
+foreach ($fn in 'Get-PiHoleStatsDatabaseSummary', 'Get-PiHoleStatsDatabaseQueryType', 'Get-PiHoleStatsDatabaseTopDomain', 'Get-PiHoleStatsDatabaseTopClient', 'Get-PiHoleStatsDatabaseUpstream') {
+ Add-Example -Category Metrics -FunctionName $fn `
+ -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" `
+ -Note 'Defaults to the last 8 hours; pass -From/-Until for a different window.' `
+ -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+}
+#endregion
+
+#region History
+# The History folder isn't its own README category (Update-ReadmeCommandReference.ps1 folds it,
+# and a few other small folders, into "Configuration & Diagnostics") - matching that here too.
+foreach ($fn in 'Get-PiHoleHistory', 'Get-PiHoleHistoryClient') {
+ Add-Example -Category Config -FunctionName $fn `
+ -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" `
+ -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+}
+foreach ($fn in 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryDatabaseClient') {
+ Add-Example -Category Config -FunctionName $fn `
+ -Invocation "$fn -PiHoleServer `$PiHoleServer -Password `$Password" `
+ -Note 'Defaults to the last 8 hours; pass -From/-Until for a different window.' `
+ -Result (& $fn -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+}
+#endregion
+
+#region Actions
+if ($IncludeDisruptive) {
+ Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushNetwork' `
+ -Invocation 'Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Confirm:$false)
+
+ Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushLogs' `
+ -Invocation 'Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Confirm:$false)
+
+ Add-Example -Category Actions -FunctionName 'Restart-PiHoleDnsService' `
+ -Invocation 'Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+ Add-Example -Category Actions -FunctionName 'Update-PiHoleActionsGravity' `
+ -Invocation 'Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $Password' `
+ -Note 'Rebuilds the entire gravity database - typically takes 1-2 minutes.' `
+ -Result (Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Confirm:$false)
+}
+else {
+ # These four were verified with real output during this module's development; reusing that
+ # rather than fabricating a plausible-looking one or re-running a disruptive action by default.
+ Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushNetwork' `
+ -Invocation 'Invoke-PiHoleFlushNetwork -PiHoleServer $PiHoleServer -Password $Password' `
+ -Skipped -StaticOutput "Status : Flushed"
+ Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushLogs' `
+ -Invocation 'Invoke-PiHoleFlushLogs -PiHoleServer $PiHoleServer -Password $Password' `
+ -Skipped -StaticOutput "Status : Flushed"
+ Add-Example -Category Actions -FunctionName 'Restart-PiHoleDnsService' `
+ -Invocation 'Restart-PiHoleDnsService -PiHoleServer $PiHoleServer -Password $Password' `
+ -Skipped -StaticOutput "Status : Restarted"
+ Add-Example -Category Actions -FunctionName 'Update-PiHoleActionsGravity' `
+ -Invocation 'Update-PiHoleActionsGravity -PiHoleServer $PiHoleServer -Password $Password' `
+ -Note 'Rebuilds the entire gravity database - typically takes 1-2 minutes.' `
+ -Skipped -StaticOutput "Status : Completed"
+}
+#endregion
+
+# --- Assemble the Markdown document ---
+$lines = [System.Collections.Generic.List[string]]::new()
+$lines.Add('# Example Output')
+$lines.Add('')
+$lines.Add("Real output captured from every exported function against a live Pi-hole v6 server, generated by ``tools/Update-ExampleOutput.ps1``. Values (query counts, IDs, timestamps, etc.) reflect whatever that specific server had at capture time - the shape of the output is what matters here, not the exact numbers.")
+$lines.Add('')
+$lines.Add('Regenerate with:')
+$lines.Add('')
+$lines.Add('```powershell')
+$lines.Add('./tools/Update-ExampleOutput.ps1')
+$lines.Add('# or, to also capture the small number of functions with real side effects on a live server:')
+$lines.Add('./tools/Update-ExampleOutput.ps1 -IncludeDisruptive')
+$lines.Add('```')
+$lines.Add('')
+
+foreach ($key in $categoryOrder.Keys) {
+ $examples = $sectionsByCategory[$key]
+ if ($examples.Count -eq 0) { continue }
+
+ $lines.Add("## $($categoryOrder[$key])")
+ $lines.Add('')
+
+ foreach ($example in $examples) {
+ $lines.Add("### $($example.FunctionName)")
+ $lines.Add('')
+ if ($example.Note) {
+ $lines.Add("_$($example.Note)_")
+ $lines.Add('')
+ }
+ $lines.Add('```powershell')
+ $lines.Add($example.Invocation)
+ $lines.Add('```')
+ $lines.Add('')
+ $lines.Add('```')
+ $lines.Add($example.Output)
+ $lines.Add('```')
+ $lines.Add('')
+ }
+}
+
+$docsDir = Split-Path -Path $outputPath -Parent
+if (-not (Test-Path $docsDir)) {
+ New-Item -ItemType Directory -Path $docsDir | Out-Null
+}
+Set-Content -Path $outputPath -Value ($lines -join "`n") -NoNewline
+Write-Host "Wrote $outputPath"