/opt/jfmsrv01/scripts/publish_splicevpn_handoff_pack.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"
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"