diff --git a/bookmarks-domains.html b/bookmarks-domains.html new file mode 100644 index 0000000..af34af7 --- /dev/null +++ b/bookmarks-domains.html @@ -0,0 +1,99 @@ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/bookmarks-ports.html b/bookmarks-ports.html new file mode 100644 index 0000000..e7d368b --- /dev/null +++ b/bookmarks-ports.html @@ -0,0 +1,92 @@ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/scripts/gen-bookmarks.ps1 b/scripts/gen-bookmarks.ps1
new file mode 100644
index 0000000..78a1db4
--- /dev/null
+++ b/scripts/gen-bookmarks.ps1
@@ -0,0 +1,305 @@
+#!/usr/bin/env pwsh
+# scripts/gen-bookmarks.ps1
+#
+# Regenerate bookmarks-domains.html and bookmarks-ports.html from the live
+# Caddyfile and stacks/*/docker-compose.yml. Run after editing either, then
+# commit the regenerated HTMLs alongside the source change.
+#
+# ./scripts/gen-bookmarks.ps1
+# ./scripts/gen-bookmarks.ps1 -Hostname myhost
+
+[CmdletBinding()]
+param(
+ [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
+ [string]$Hostname = 'valhalla'
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+function Format-HtmlEscape([string]$s) {
+ if ($null -eq $s) { return '' }
+ return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
+}
+
+# Split a text blob into LF-terminated lines (handles CRLF inputs).
+function Get-LfLines([string]$path) {
+ $raw = Get-Content -Raw -Path $path -Encoding UTF8
+ return ($raw -replace "`r", '') -split "`n"
+}
+
+# -------------------------------------------------------------------------
+# Caddyfile -> [{Host, Section, Internal, Upstream}, ...]
+# -------------------------------------------------------------------------
+function Get-CaddySites([string]$caddyFile) {
+ $lines = Get-LfLines $caddyFile
+ $sites = [System.Collections.Generic.List[object]]::new()
+ $currentSection = 'Other'
+ $i = 0
+ while ($i -lt $lines.Count) {
+ $trim = $lines[$i].Trim()
+
+ # Section banner: `# ===`, then `# NAME`, then `# ===`
+ # NOTE: capture the name BEFORE running the second -match, since
+ # PowerShell's -match overwrites $matches with each call.
+ if ($trim -match '^#\s+=+\s*$' -and $i + 2 -lt $lines.Count) {
+ $next = $lines[$i + 1].Trim()
+ $after = $lines[$i + 2].Trim()
+ if ($next -match '^#\s+(.+?)\s*$') {
+ $name = $matches[1]
+ if ($after -match '^#\s+=+\s*$') {
+ $currentSection = ($name -replace '\s*[-—]\s*(public|internal only)\s*$','').Trim()
+ $i += 3
+ continue
+ }
+ }
+ }
+
+ # Site header at column 0: ` ')
+ $out.Add(' ')
+
+ # Group by section, preserving first-seen order.
+ $groups = [ordered]@{}
+ foreach ($s in $sites) {
+ if (-not $groups.Contains($s.Section)) {
+ $groups[$s.Section] = [System.Collections.Generic.List[object]]::new()
+ }
+ $groups[$s.Section].Add($s)
+ }
+
+ foreach ($section in $groups.Keys) {
+ $items = $groups[$section]
+ $publicCount = @($items | Where-Object { -not $_.Internal }).Count
+ $internalCount = @($items | Where-Object { $_.Internal }).Count
+ $suffix =
+ if ($publicCount -eq 0) { ' (internal)' }
+ elseif ($internalCount -eq 0) { ' (public)' }
+ else { '' }
+ $heading = Format-HtmlEscape "$section$suffix"
+ $out.Add('')
+ $out.Add(" ')
+ foreach ($s in $items) {
+ $label = $s.Host -replace '\.ginnoir\.com$',''
+ if ($s.Internal -and $publicCount -gt 0) { $label = "$label [internal]" }
+ $href = Format-HtmlEscape "https://$($s.Host)"
+ $text = Format-HtmlEscape $label
+ $out.Add(" ')
+ }
+
+ $out.Add('')
+ $out.Add(' ')
+ $out.Add(' ')
+ return ($out -join "`n") + "`n"
+}
+
+function New-PortsHtml($services, [string]$hostname) {
+ $out = [System.Collections.Generic.List[string]]::new()
+ $out.Add('')
+ $out.Add('')
+ $out.Add(" ')
+ $out.Add(' ')
+
+ $byStack = [ordered]@{}
+ foreach ($svc in $services) {
+ if (-not $byStack.Contains($svc.Stack)) {
+ $byStack[$svc.Stack] = [System.Collections.Generic.List[object]]::new()
+ }
+ $byStack[$svc.Stack].Add($svc)
+ }
+
+ $ti = (Get-Culture).TextInfo
+ foreach ($stack in $byStack.Keys) {
+ $items = $byStack[$stack]
+ $heading = Format-HtmlEscape ($ti.ToTitleCase($stack))
+ $out.Add('')
+ $out.Add(" ')
+ foreach ($svc in $items) {
+ # Collapse same host-port across protocols (TCP+UDP -> one entry
+ # labelled TCP/UDP). Browsers can't open UDP anyway.
+ $byHostPort = @($svc.Ports | Group-Object -Property Host)
+ $multi = $byHostPort.Count -gt 1
+ foreach ($g in ($byHostPort | Sort-Object { [int]$_.Name })) {
+ $group = @($g.Group)
+ $p = $group[0]
+ $protos = ($group | ForEach-Object { $_.Proto } | Sort-Object -Unique) -join '/'
+ $scheme =
+ if ($p.Container -eq 22) { 'ssh' }
+ elseif ($p.Container -eq 443) { 'https' }
+ else { 'http' }
+ $userPrefix = if ($scheme -eq 'ssh') { 'git@' } else { '' }
+ $href = Format-HtmlEscape ("{0}://{1}{2}:{3}" -f $scheme, $userPrefix, $hostname, $p.Host)
+ $portTag = if ($multi -or $p.Container -ne $p.Host) { " :$($p.Host)" } else { '' }
+ $protoTag = if ($protos -ne 'tcp') { " ($($protos.ToUpper()))" } else { '' }
+ $text = Format-HtmlEscape "$($svc.ContainerName)$portTag$protoTag"
+ $out.Add(" ')
+ }
+
+ $out.Add('')
+ $out.Add(' ')
+ $out.Add(' ')
+ return ($out -join "`n") + "`n"
+}
+
+# -------------------------------------------------------------------------
+# Main
+# -------------------------------------------------------------------------
+$caddyFile = Join-Path $RepoRoot 'Caddyfile'
+$stacksDir = Join-Path $RepoRoot 'stacks'
+$domainsOut = Join-Path $RepoRoot 'bookmarks-domains.html'
+$portsOut = Join-Path $RepoRoot 'bookmarks-ports.html'
+
+Write-Host "Parsing Caddyfile..."
+$sites = Get-CaddySites $caddyFile
+Write-Host " found $($sites.Count) site(s)"
+
+Write-Host "Parsing stacks..."
+$services = Get-StackServices $stacksDir
+$portCount = 0
+foreach ($s in $services) { $portCount += $s.Ports.Count }
+Write-Host " found $($services.Count) service(s) with $portCount published port(s)"
+
+$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+
+Write-Host "Writing $domainsOut"
+[System.IO.File]::WriteAllText($domainsOut, (New-DomainsHtml $sites), $utf8NoBom)
+
+Write-Host "Writing $portsOut"
+[System.IO.File]::WriteAllText($portsOut, (New-PortsHtml $services $Hostname), $utf8NoBom)
+
+Write-Host "Done."
Homelab - Domains (via Caddy)
')
+ $out.Add('Homelab - Domains
')
+ $out.Add(' $heading
")
+ $out.Add(' Homelab - Backdoor Ports (direct to $hostname)
")
+ $out.Add('Homelab - Backdoor Ports
')
+ $out.Add(' $heading
")
+ $out.Add('