Preview — /tmp/repair-splicevpn-handoff-portal.sh

/tmp/repair-splicevpn-handoff-portal.sh

#!/usr/bin/env bash
set -euo pipefail

SCRIPTS="/opt/jfmsrv01/scripts"
PUBLIC_ROOT="/var/www/handoff/public"
DEFAULT_SLUG="vqngx6j8dc9q1lvt7g2a7a646q2t"
SRC="/opt/splicevpn/HANDOVER.md"
PUBSCRIPT="$SCRIPTS/publish_handoff.sh"
HOOK="$SCRIPTS/publish_splicevpn_handoff_pack.sh"

STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP="/root/splicevpn-portal-repair-backup-$STAMP"

echo "=== REPAIR SPLICEVPN HANDOFF PORTAL ==="
date
echo "Backup: $BACKUP"

if [ ! -s "$SRC" ]; then
  echo "ERROR: $SRC missing or empty"
  exit 1
fi

SLUG="${HANDOFF_SLUG:-$DEFAULT_SLUG}"
if [ ! -d "$PUBLIC_ROOT/$SLUG" ]; then
  SLUG="$(find "$PUBLIC_ROOT" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %f\n' 2>/dev/null | sort -nr | awk 'NR==1{print $2}')"
fi

if [ -z "${SLUG:-}" ] || [ ! -d "$PUBLIC_ROOT/$SLUG" ]; then
  echo "ERROR: could not detect handoff public slug under $PUBLIC_ROOT"
  exit 1
fi

PUBLIC_BASE="$PUBLIC_ROOT/$SLUG"
PUBLIC_SPLICE="$PUBLIC_BASE/splicevpn"
PUBLIC_SERVER="$PUBLIC_BASE/server"

mkdir -p "$BACKUP" "$SCRIPTS" "$PUBLIC_SPLICE" "$PUBLIC_SERVER"

backup_path() {
  local p="$1"
  if [ -e "$p" ]; then
    mkdir -p "$BACKUP$(dirname "$p")"
    cp -a "$p" "$BACKUP$p"
  fi
}

backup_path "$PUBLIC_BASE/index.html"
backup_path "$PUBLIC_SPLICE"
backup_path "$PUBLIC_SERVER/SPLICEVPN_HANDOVER.html"
backup_path "$PUBLIC_SERVER/SPLICEVPN_HANDOVER.md"
backup_path "$SCRIPTS/HANDOVER.md"
backup_path "$SCRIPTS/CHANGELOG.md"
backup_path "$PUBSCRIPT"
backup_path "$HOOK"

cat > "$HOOK" <<'HOOK'
#!/usr/bin/env bash
set -euo pipefail

SCRIPTS="/opt/jfmsrv01/scripts"
PUBLIC_ROOT="/var/www/handoff/public"
DEFAULT_SLUG="vqngx6j8dc9q1lvt7g2a7a646q2t"
SRC="/opt/splicevpn/HANDOVER.md"

SLUG="${HANDOFF_SLUG:-$DEFAULT_SLUG}"
if [ ! -d "$PUBLIC_ROOT/$SLUG" ]; then
  SLUG="$(find "$PUBLIC_ROOT" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %f\n' 2>/dev/null | sort -nr | awk 'NR==1{print $2}')"
fi

PUBLIC_BASE="$PUBLIC_ROOT/$SLUG"
PUBLIC_SPLICE="$PUBLIC_BASE/splicevpn"
PUBLIC_SERVER="$PUBLIC_BASE/server"

mkdir -p "$PUBLIC_SPLICE/source-html" "$PUBLIC_SERVER" "$SCRIPTS"

export SRC PUBLIC_BASE PUBLIC_SPLICE PUBLIC_SERVER SCRIPTS

python3 - <<'PY'
from pathlib import Path
import html, hashlib, os, re, subprocess, time, urllib.parse

SRC = Path(os.environ["SRC"])
PUBLIC_BASE = Path(os.environ["PUBLIC_BASE"])
PUBLIC_SPLICE = Path(os.environ["PUBLIC_SPLICE"])
PUBLIC_SERVER = Path(os.environ["PUBLIC_SERVER"])
SCRIPTS = Path(os.environ["SCRIPTS"])

def esc(x):
    return html.escape(str(x), quote=True)

def redact(text):
    patterns = [
        (r'(?i)(password\s*[:=[REDACTED]
        (r'(?i)(api[_-]?key\s*[:=]\s*)["\']?[^"\'\s<]+', r'\1[REDACTED]'),
        (r'(?i)(secret\s*[:=[REDACTED]
        (r'(?i)(token\s*[:=[REDACTED]
        (r'(?i)(private[_-]?key\s*[:=]\s*)["\']?[^"\'\s<]+', r'\1[REDACTED]'),
        (r'spl_[A-Za-z0-9._:\-]{8,}', 'spl_[REDACTED]'),
    ]
    for pat, repl in patterns:
        text = re.sub(pat, repl, text)
    return text

CSS = """
<style>
body{font:15px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:1100px;margin:2rem auto;padding:0 1rem;color:#111;background:#fff}
h1{font-size:1.7rem;margin:.2rem 0 1rem}
h2{margin-top:1.5rem;border-bottom:1px solid #ddd;padding-bottom:.25rem}
.card{border:1px solid #ddd;border-radius:10px;padding:1rem;margin:1rem 0;background:#fafafa}
pre{white-space:pre-wrap;background:#f4f4f5;border:1px solid #ddd;border-radius:8px;padding:1rem;overflow:auto}
code{background:#f4f4f5;border-radius:4px;padding:1px 4px}
table{width:100%;border-collapse:collapse;margin:.75rem 0}
th,td{border-bottom:1px solid #e5e7eb;text-align:left;padding:.45rem;vertical-align:top}
th{background:#f4f4f5}
a{color:#0369a1}
.small{color:#666;font-size:.9rem}
.ok{color:#15803d}.warn{color:#9a3412}
</style>
"""

def write_page(path, title, body):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{esc(title)}</title>
{CSS}
</head>
<body>
<h1>{esc(title)}</h1>
{body}
</body>
</html>
""")

handover_text = redact(SRC.read_text(errors="replace"))

# Main SpliceVPN handover HTML + markdown copies
(PUBLIC_SPLICE / "HANDOVER.md").write_text(handover_text)
(SCRIPTS / "SPLICEVPN_HANDOVER.md").write_text(handover_text)
write_page(
    PUBLIC_SPLICE / "HANDOVER.html",
    "SpliceVPN — Handover",
    "<p class='small'>Generated from <code>/opt/splicevpn/HANDOVER.md</code>.</p><pre>" + esc(handover_text) + "</pre>"
)

# Backward-compatible links under /server/
(PUBLIC_SERVER / "SPLICEVPN_HANDOVER.md").write_text(handover_text)
write_page(
    PUBLIC_SERVER / "SPLICEVPN_HANDOVER.html",
    "SpliceVPN — Handover",
    "<p class='small'>Generated from <code>/opt/splicevpn/HANDOVER.md</code>.</p><pre>" + esc(handover_text) + "</pre>"
)
(SCRIPTS / "SPLICEVPN_HANDOVER.html").write_text((PUBLIC_SPLICE / "HANDOVER.html").read_text())

# File tree
tree_roots = [
    Path("/opt/splicevpn"),
    Path("/opt/splice-src"),
    Path("/var/www/sites/splice"),
]
rows = []
for root in tree_roots:
    if not root.exists():
        rows.append(f"<tr><td>{esc(root)}</td><td colspan='3'>missing</td></tr>")
        continue
    for p in sorted(root.rglob("*")):
        try:
            st = p.stat()
        except Exception:
            continue
        rel = str(p)
        if any(x in rel.lower() for x in ["/.git/", "node_modules", "/vendor/", ".env", "wgsplice.key"]):
            continue
        rows.append(
            "<tr>"
            f"<td><code>{esc(rel)}</code></td>"
            f"<td>{'dir' if p.is_dir() else 'file'}</td>"
            f"<td>{st.st_size if p.is_file() else ''}</td>"
            f"<td>{esc(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(st.st_mtime)))}</td>"
            "</tr>"
        )
write_page(
    PUBLIC_SPLICE / "file-tree.html",
    "SpliceVPN — File Tree",
    "<table><tr><th>Path</th><th>Type</th><th>Bytes</th><th>Modified</th></tr>" + "\n".join(rows) + "</table>"
)

# Downloads inventory
downloads = [
    "splice-agent",
    "splice-agent.exe",
    "splicevpn.tar.gz",
    "splicevpn-windows-amd64.zip",
    "install-splicevpn.ps1",
    "Install-SpliceVPN.bat",
]
download_rows = []
for name in downloads:
    p = Path("/var/www/sites/splice") / name
    if p.exists():
        h = hashlib.sha256()
        with p.open("rb") as f:
            for chunk in iter(lambda: f.read(1024 * 1024), b""):
                h.update(chunk)
        download_rows.append(
            "<tr>"
            f"<td><code>{esc(name)}</code></td>"
            f"<td>{p.stat().st_size}</td>"
            f"<td><code>{h.hexdigest()}</code></td>"
            f"<td>{esc(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(p.stat().st_mtime)))}</td>"
            "</tr>"
        )
    else:
        download_rows.append(f"<tr><td><code>{esc(name)}</code></td><td colspan='3'>missing</td></tr>")
write_page(
    PUBLIC_SPLICE / "downloads.html",
    "SpliceVPN — Download Inventory",
    "<p>This is an HTML inventory of the public client download files.</p>"
    "<table><tr><th>File</th><th>Bytes</th><th>SHA256</th><th>Modified</th></tr>"
    + "\n".join(download_rows) + "</table>"
)

# Service/status page
commands = [
    ("splice-coordinator.service", "systemctl --no-pager -l status splice-coordinator.service || true"),
    ("wg-quick@wgsplice.service", "systemctl --no-pager -l status wg-quick@wgsplice.service || true"),
    ("wg show wgsplice", "wg show wgsplice || true"),
    ("listening ports", "ss -lunpt 2>/dev/null | grep -E '(:7777|:51820|:80|:443)' || true"),
]
parts = []
for title, cmd in commands:
    out = subprocess.run(cmd, shell=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout
    parts.append(f"<h2>{esc(title)}</h2><pre>{esc(redact(out))}</pre>")
write_page(PUBLIC_SPLICE / "service-status.html", "SpliceVPN — Service Status", "\n".join(parts))

# Source HTML browser
allowed_exts = {".go",".mod",".sum",".html",".css",".js",".ps1",".bat",".sh",".service",".conf",".txt",".md",".json",".yaml",".yml"}
source_roots = [
    Path("/opt/splicevpn"),
    Path("/opt/splice-src"),
    Path("/var/www/sites/splice"),
]
source_links = []
for root in source_roots:
    if not root.exists():
        continue
    for p in sorted(root.rglob("*")):
        if not p.is_file():
            continue
        rels = str(p)
        lower = rels.lower()
        if any(x in lower for x in ["/.git/", "node_modules", "/vendor/", ".env", ".key", ".pem", ".p12", ".pfx", ".crt", ".zip", ".gz", ".exe"]):
            continue
        if p.suffix.lower() not in allowed_exts:
            continue
        try:
            if p.stat().st_size > 1024 * 1024:
                continue
            data = redact(p.read_text(errors="replace"))
        except Exception:
            continue
        safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "__", str(p).strip("/")) + ".html"
        out = PUBLIC_SPLICE / "source-html" / safe_name
        write_page(
            out,
            f"Source — {p}",
            f"<p><code>{esc(p)}</code></p><pre>{esc(data)}</pre>"
        )
        source_links.append((str(p), "source-html/" + urllib.parse.quote(safe_name)))

source_rows = [
    f"<tr><td><code>{esc(path)}</code></td><td><a href='{href}'>HTML</a></td></tr>"
    for path, href in source_links
]
write_page(
    PUBLIC_SPLICE / "source-browser.html",
    "SpliceVPN — HTML Source Browser",
[REDACTED SECRET-LIKE LINE]
    "<table><tr><th>Source file</th><th>HTML view</th></tr>"
    + "\n".join(source_rows) + "</table>"
)

# Summary page
write_page(
    PUBLIC_SPLICE / "index.html",
    "SpliceVPN — Handoff Pack",
    """
<div class="card">
<p><strong>Status:</strong> hub-and-spoke WireGuard relay MVP documented.</p>
<p><strong>Important:</strong> this is not full peer-to-peer yet. Current MVP traffic is relayed through the user's own SpliceVPN hub.</p>
</div>
<ul>
<li><a href="HANDOVER.html">HANDOVER.html</a></li>
<li><a href="file-tree.html">file-tree.html</a></li>
<li><a href="source-browser.html">source-browser.html</a></li>
<li><a href="downloads.html">downloads.html</a></li>
<li><a href="service-status.html">service-status.html</a></li>
<li><a href="HANDOVER.md">HANDOVER.md</a></li>
</ul>
"""
)

# Patch main portal index: remove broken old SpliceVPN section and insert proper styled card before Shared/footer.
idx = PUBLIC_BASE / "index.html"
if idx.exists():
    s = idx.read_text(errors="replace")
else:
    s = "<!doctype html><html><head><meta charset='utf-8'><title>jfmsrv01/handoff</title></head><body><h1>jfmsrv01/handoff</h1></body></html>"

s = re.sub(r"\s*<!-- SPLICEVPN-HANDOVER-LINK-START -->.*?<!-- SPLICEVPN-HANDOVER-LINK-END -->\s*", "\n", s, flags=re.S)
s = re.sub(r"\s*<!-- SPLICEVPN-SECTION-START -->.*?<!-- SPLICEVPN-SECTION-END -->\s*", "\n", s, flags=re.S)
s = re.sub(r"\s*<h2>\s*SpliceVPN\s*</h2>\s*<ul>.*?SPLICEVPN_HANDOVER.*?</ul>\s*", "\n", s, flags=re.I|re.S)
s = re.sub(r"\s*SpliceVPN\s*(?:<br\s*/?>|\n|\r|\s)*SPLICEVPN_HANDOVER\.html\s*(?:<br\s*/?>|\n|\r|\s)*SPLICEVPN_HANDOVER\.md\s*", "\n", s, flags=re.I)

block = """
<!-- SPLICEVPN-SECTION-START -->
<section id="splicevpn" style="border:1px solid #1f2937;border-radius:8px;margin:1.4rem 0;background:#070b12;color:#cbd5e1;overflow:hidden">
  <div style="padding:1rem 1.25rem;border-bottom:1px solid #1f2937">
    <h2 style="margin:0;color:#3b82f6;font-size:1.2rem">🔐 SpliceVPN</h2>
    <p style="margin:.35rem 0 0;color:#94a3b8">WireGuard hub-relay MVP handover. Current architecture is relayed through the user's own SpliceVPN hub, not full peer-to-peer yet.</p>
  </div>
  <div style="display:grid;grid-template-columns:repeat(2,minmax(260px,1fr));gap:0;border-top:0">
    <div style="padding:1rem 1.25rem;border-right:1px solid #1f2937">
      <div style="font-size:.72rem;letter-spacing:.14em;color:#94a3b8;font-weight:700;margin-bottom:.7rem">BROWSE ONLINE</div>
      <ul style="margin:.2rem 0 0;padding-left:1.2rem;line-height:1.85">
        <li><a style="color:#3b82f6" href="splicevpn/HANDOVER.html">HANDOVER.html</a></li>
        <li><a style="color:#3b82f6" href="splicevpn/file-tree.html">file-tree.html</a></li>
        <li><a style="color:#3b82f6" href="splicevpn/source-browser.html">source-browser.html</a></li>
        <li><a style="color:#3b82f6" href="splicevpn/downloads.html">downloads.html</a></li>
        <li><a style="color:#3b82f6" href="splicevpn/service-status.html">service-status.html</a></li>
      </ul>
    </div>
    <div style="padding:1rem 1.25rem">
      <div style="font-size:.72rem;letter-spacing:.14em;color:#94a3b8;font-weight:700;margin-bottom:.7rem">DOWNLOAD</div>
      <ul style="margin:.2rem 0 0;padding-left:1.2rem;line-height:1.85">
        <li><a style="color:#3b82f6" href="splicevpn/HANDOVER.md">HANDOVER.md</a></li>
      </ul>
    </div>
  </div>
</section>
<!-- SPLICEVPN-SECTION-END -->
"""

anchors = [
    r"<h2[^>]*>\s*.*Shared\s*[—-]\s*Redis",
    r"##\s*Shared\s*[—-]\s*Redis",
    r"jfmsrv01 handoff portal",
    r"</body>",
]
inserted = False
for pat in anchors:
    m = re.search(pat, s, flags=re.I)
    if m:
        s = s[:m.start()] + block + "\n" + s[m.start():]
        inserted = True
        break
if not inserted:
    s += block

idx.write_text(s)

print("Wrote SpliceVPN handoff pack:")
print(PUBLIC_SPLICE)
print("Rendered source files:", len(source_links))
PY

echo
echo "SpliceVPN handoff pack published."
echo "Public pack: $PUBLIC_SPLICE/index.html"
HOOK

chmod +x "$HOOK"

echo
echo "=== UPDATE MAIN HANDOVER + CHANGELOG ==="

if [ -f "$SCRIPTS/HANDOVER.md" ] && ! grep -q "SpliceVPN handoff pack" "$SCRIPTS/HANDOVER.md"; then
  cat >> "$SCRIPTS/HANDOVER.md" <<'MD'

## 2026-06-09 — SpliceVPN handoff pack

SpliceVPN has been added as its own handoff pack and public portal section.

Source of truth:

- `/opt/splicevpn/HANDOVER.md`

Published HTML pack:

- `splicevpn/HANDOVER.html`
- `splicevpn/file-tree.html`
- `splicevpn/source-browser.html`
- `splicevpn/downloads.html`
- `splicevpn/service-status.html`

Important note:

- Current MVP is hub-and-spoke WireGuard relay through the user's own SpliceVPN hub.
- It is not full peer-to-peer yet.
- Public wording must not claim traffic never detours or that direct peer-to-peer is already complete.
MD
fi

if [ -f "$SCRIPTS/CHANGELOG.md" ] && ! grep -q "SpliceVPN handoff pack" "$SCRIPTS/CHANGELOG.md"; then
  cat >> "$SCRIPTS/CHANGELOG.md" <<'MD'

## 2026-06-09 — SpliceVPN handoff pack

- Added proper SpliceVPN section to public handoff portal.
- Added HTML-rendered SpliceVPN handover, file tree, source browser, downloads inventory, and service status.
- Added publisher hook so future handoff publishes can re-add the SpliceVPN section after the main generated index is rebuilt.
[REDACTED SECRET-LIKE LINE]
MD
fi

echo
echo "=== INSTALL PUBLISH HOOK ==="

if [ -f "$PUBSCRIPT" ]; then
  if ! grep -q "publish_splicevpn_handoff_pack.sh" "$PUBSCRIPT"; then
    cat >> "$PUBSCRIPT" <<'SH'

# SpliceVPN extra handoff pack.
# Keep this at the end so it patches the generated index after publish_handoff.sh rebuilds it.
if [ -x /opt/jfmsrv01/scripts/publish_splicevpn_handoff_pack.sh ]; then
  /opt/jfmsrv01/scripts/publish_splicevpn_handoff_pack.sh || true
fi
SH
    echo "Added SpliceVPN hook to $PUBSCRIPT"
  else
    echo "SpliceVPN hook already present in $PUBSCRIPT"
  fi
else
  echo "WARNING: $PUBSCRIPT not found. Hook script created but not attached."
fi

echo
echo "=== RUN HOOK NOW ==="
"$HOOK"

echo
echo "=== VERIFY LINKS ==="

python3 - "$PUBLIC_BASE/index.html" "$PUBLIC_BASE" <<'PY'
from pathlib import Path
import re, sys

idx = Path(sys.argv[1])
base = Path(sys.argv[2])
s = idx.read_text(errors="replace")

links = re.findall(r'href=["\']([^"\']+)["\']', s)
splice_links = [x for x in links if x.startswith("splicevpn/")]
missing = []
for href in splice_links:
    target = base / href.split("#", 1)[0].split("?", 1)[0]
    if not target.exists():
        missing.append(href)

print("SpliceVPN links in portal:", len(splice_links))
print("Missing SpliceVPN linked files:", len(missing))
for m in missing:
    print("MISSING:", m)

if "SPLICEVPN-SECTION-START" in s:
    print("Portal section marker: OK")
else:
    print("Portal section marker: MISSING")
PY

echo
echo "=== FILES CREATED ==="
find "$PUBLIC_SPLICE" -maxdepth 2 -type f | sort

echo
echo "Backup saved at:"
echo "$BACKUP"
echo
echo "Open:"
echo "https://vqngx6j8dc9q1lvt7g2a7a646q2t.jfmcommunications.com.au/$SLUG/"
echo "https://vqngx6j8dc9q1lvt7g2a7a646q2t.jfmcommunications.com.au/$SLUG/splicevpn/HANDOVER.html"
echo "https://vqngx6j8dc9q1lvt7g2a7a646q2t.jfmcommunications.com.au/$SLUG/splicevpn/source-browser.html"
echo
echo "=== DONE ==="