chore: initial import

This commit is contained in:
chelsea
2025-11-11 23:11:59 -06:00
parent 7598942bc5
commit c15fe83651
28 changed files with 3755 additions and 4 deletions

View File

@@ -0,0 +1,478 @@
#!/usr/bin/env python3
"""Regenerate intranet/index.html by polling each server over SSH."""
from __future__ import annotations
import base64
import json
import pathlib
import shlex
import subprocess
import textwrap
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
OUTPUT_PATH = REPO_ROOT / "intranet" / "index.html"
@dataclass
class HostConfig:
slug: str
ssh: str
title: str
subtitle: str
tags: List[str]
vhost_notes: Dict[str, str]
docker_notes: Dict[str, str]
HOSTS: List[HostConfig] = [
HostConfig(
slug="la",
ssh="root@la.chelseawoodruff.net",
title="Los Angeles",
subtitle="la.chelseawoodruff.net",
tags=["nginx", "FRP relay"],
vhost_notes={
"chat.scorpi.us": "Public chat frontend with HTTPS redirect",
},
docker_notes={
"frps": "Snowdreamtech FRP reverse proxy",
},
),
HostConfig(
slug="virginia",
ssh="root@virginia.chelseawoodruff.net",
title="Virginia",
subtitle="virginia.chelseawoodruff.net",
tags=["nginx", "Git", "Vikunja", "Dokuwiki", "Custom apps"],
vhost_notes={
"hightimesfrom.space": "Personal landing page",
"pm.scorpi.us": "Leantime / PM suite",
"wiki.scorpi.us": "Dokuwiki knowledge base",
"news.scorpi.us": "Pseudo news clone",
"git.scorpi.us": "Gitea instance",
"reddit.scorpi.us": "Reddit proxy (forced HTTPS)",
"blocked.scorpi.us": "Block page mirror",
"requests.scorpi.us": "Requesty helper",
"youtube.scorpi.us": "PseudoTube experiment",
},
docker_notes={
"claude-proxy": "Claude proxy forwarder",
"solar_420": "Solartime demo app",
"frps": "Snowdreamtech FRP relay",
"balanceboard_app": "Balanceboard UI",
"balanceboard_postgres": "Balanceboard DB",
"vikunja_vikunja_1": "Vikunja task manager",
"vikunja_db_1": "Vikunja Postgres",
"gitea": "Gitea service",
"gitea_db_1": "Gitea Postgres",
"dokuwiki": "Dokuwiki container",
"requesty": "Requesty API",
"psuedo-tube": "PseudoTube UI",
"block-page": "Block-page helper",
},
),
HostConfig(
slug="chicago",
ssh="root@chicago.scorpi.us",
title="Chicago",
subtitle="chicago.scorpi.us",
tags=["nginx", "ADHDbot", "ntfy", "IRC"],
vhost_notes={
"adhd.scorpi.us": "ADHDbot UI/API",
"matrix.scorpi.us": "Matrix homeserver proxy",
"ntfy.scorpi.us": "ntfy notification hub",
},
docker_notes={
"adhdbot-app_adhdbot_1": "ADHDbot FastAPI stack",
"ntfy": "ntfy topic server",
"inspircd": "InspIRCd daemon",
},
),
HostConfig(
slug="dallas",
ssh="root@dallas.scorpi.us",
title="Dallas",
subtitle="dallas.scorpi.us",
tags=["nginx", "Automation"],
vhost_notes={
"n8n.dallas.scorpi.us": "n8n low-code automation suite",
},
docker_notes={
"n8n_n8n_1": "n8n worker (localhost only)",
},
),
HostConfig(
slug="phoenix",
ssh="root@phoenix.scorpi.us",
title="Phoenix",
subtitle="phoenix.scorpi.us",
tags=["Discourse", "Discord bridge"],
vhost_notes={},
docker_notes={
"app": "Discourse stack",
"discord-discourse-bridge": "Discord ↔ Discourse bridge",
},
),
]
REMOTE_PY = textwrap.dedent(
"""
import glob
import json
import os
import subprocess
def collect_nginx():
base = "/etc/nginx/sites-enabled"
sites = []
if not os.path.isdir(base):
return sites
for path in sorted(glob.glob(os.path.join(base, "*"))):
if not os.path.isfile(path):
continue
try:
with open(path, "r", encoding="utf-8") as handle:
lines = handle.readlines()
except OSError:
continue
server_names = []
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if stripped.startswith("server_name"):
content = stripped.split(None, 1)[1] if " " in stripped else ""
content = content.split("#", 1)[0].strip()
if content.endswith(";"):
content = content[:-1].strip()
if content:
server_names.extend([token for token in content.split() if token])
sites.append({"filename": os.path.basename(path), "server_names": server_names})
return sites
def collect_docker():
try:
output = subprocess.check_output(
["docker", "ps", "--format", "{{json .}}"], text=True, timeout=10
)
except Exception:
return []
containers = []
for line in output.splitlines():
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
continue
containers.append(
{
"name": data.get("Names"),
"image": data.get("Image"),
"ports": data.get("Ports"),
}
)
return containers
payload = {
"hostname": os.uname().nodename,
"nginx": collect_nginx(),
"docker": collect_docker(),
}
print("{{JSON}}" + json.dumps(payload) + "{{/JSON}}")
"""
).strip()
def run_remote_script(target: str) -> Dict[str, Any]:
encoded = base64.b64encode(REMOTE_PY.encode("utf-8")).decode("ascii")
remote_cmd = (
"python3 -c "
+ shlex.quote(
"import base64, json; exec(base64.b64decode({}))".format(repr(encoded))
)
)
ssh_cmd = [
"ssh",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=no",
target,
remote_cmd,
]
completed = subprocess.run(
ssh_cmd,
capture_output=True,
text=True,
check=False,
)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip())
marker_start = "{{JSON}}"
marker_end = "{{/JSON}}"
stdout = completed.stdout
start = stdout.find(marker_start)
end = stdout.find(marker_end)
if start == -1 or end == -1:
raise RuntimeError("Could not locate JSON payload in ssh output")
json_text = stdout[start + len(marker_start) : end]
return json.loads(json_text)
def format_ports(ports: Optional[str]) -> str:
ports = (ports or "").strip()
return ports if ports else ""
def render_host_card(config: HostConfig, payload: Optional[Dict[str, Any]], error: Optional[str]) -> str:
tag_html = "".join(f'<span class="tag">{tag}</span>' for tag in config.tags)
if error:
body = f'<p class="error">Unable to load data: {error}</p>'
nginx_html = ""
docker_html = ""
else:
nginx_entries = payload.get("nginx", []) if payload else []
docker_entries = payload.get("docker", []) if payload else []
if nginx_entries:
items = []
for entry in nginx_entries:
names = entry.get("server_names") or [entry.get("filename", "unknown")]
label = ", ".join(names)
note = config.vhost_notes.get(names[0]) or config.vhost_notes.get(entry.get("filename", ""))
if note:
items.append(f"<li><strong>{label}</strong> {note}</li>")
else:
items.append(f"<li>{label}</li>")
nginx_html = "<div><p class=\"section-title\">nginx sites</p><ul>" + "".join(items) + "</ul></div>"
else:
nginx_html = "<div><p class=\"section-title\">nginx sites</p><p class=\"muted\">No vhosts detected.</p></div>"
if docker_entries:
rows = []
for entry in docker_entries:
name = entry.get("name") or "unknown"
image = entry.get("image") or "?"
ports = format_ports(entry.get("ports"))
note = config.docker_notes.get(name)
note_span = f'<br /><span class="muted tiny">{note}</span>' if note else ""
rows.append(
"<tr>"
f"<td>{name}{note_span}</td>"
f"<td>{image}</td>"
f"<td>{ports}</td>"
"</tr>"
)
docker_html = (
"<div><p class=\"section-title\">Docker</p>"
"<table class=\"docker-table\"><thead><tr><th>Name</th><th>Image</th><th>Ports</th></tr></thead>"
"<tbody>"
+ "".join(rows)
+ "</tbody></table></div>"
)
else:
docker_html = "<div><p class=\"section-title\">Docker</p><p class=\"muted\">No containers running.</p></div>"
body = nginx_html + docker_html
return textwrap.dedent(
f"""
<article class="card" id="{config.slug}">
<div>
<p class="eyebrow">{config.title}</p>
<h2>{config.subtitle}</h2>
<div class="tag-list">{tag_html}</div>
</div>
{body}
</article>
"""
).strip()
def render_html(reports: List[str]) -> str:
cards_html = "\n".join(reports)
return textwrap.dedent(
f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Service Inventory</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root {{
color: #0f172a;
font-family: "Inter", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
}}
body {{
margin: 0;
background: radial-gradient(circle at top left, rgba(56, 189, 248, 0.15), transparent 45%),
radial-gradient(circle at bottom right, rgba(192, 132, 252, 0.2), transparent 40%), #e2e8f0;
min-height: 100vh;
padding: clamp(1.5rem, 3vw, 3rem);
}}
.shell {{
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}}
header {{
background: rgba(255, 255, 255, 0.9);
border-radius: 1.5rem;
padding: clamp(1.5rem, 3vw, 2.25rem);
box-shadow: 0 30px 80px rgba(15, 23, 42, 0.15);
border: 1px solid rgba(15, 23, 42, 0.05);
}}
header h1 {{
margin: 0 0 0.25rem;
font-size: clamp(2rem, 4vw, 2.75rem);
color: #0f172a;
}}
header p {{
margin: 0;
color: #475569;
}}
.grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}}
.card {{
background: rgba(255, 255, 255, 0.92);
border-radius: 1.25rem;
padding: 1.5rem;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.1);
border: 1px solid rgba(15, 23, 42, 0.05);
display: flex;
flex-direction: column;
gap: 1rem;
}}
.card h2 {{
margin: 0;
font-size: 1.25rem;
color: #0f172a;
}}
.eyebrow {{
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.72rem;
color: #64748b;
margin-bottom: 0.35rem;
}}
.section-title {{
font-weight: 600;
color: #475569;
margin-bottom: 0.35rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.78rem;
}}
ul {{
margin: 0;
padding-left: 1.25rem;
color: #0f172a;
line-height: 1.45;
}}
ul li + li {{
margin-top: 0.35rem;
}}
.tag-list {{
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}}
.tag {{
border-radius: 999px;
padding: 0.25rem 0.85rem;
font-size: 0.82rem;
background: rgba(59, 130, 246, 0.12);
border: 1px solid rgba(59, 130, 246, 0.25);
color: #1d4ed8;
}}
.docker-table {{
width: 100%;
border-collapse: collapse;
font-size: 0.92rem;
}}
.docker-table th {{
text-align: left;
padding: 0.4rem 0;
color: #64748b;
font-weight: 600;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
}}
.docker-table td {{
padding: 0.35rem 0;
border-bottom: 1px solid rgba(15, 23, 42, 0.03);
}}
.muted {{
color: #94a3b8;
}}
.muted.tiny {{
font-size: 0.8rem;
}}
.error {{
color: #b91c1c;
background: rgba(248, 113, 113, 0.15);
padding: 0.75rem;
border-radius: 0.75rem;
border: 1px solid rgba(248, 113, 113, 0.25);
}}
@media (max-width: 640px) {{
body {{
padding: 1rem;
}}
.card {{
padding: 1.1rem;
}}
}}
</style>
</head>
<body>
<div class="shell">
<header>
<p class="eyebrow">Internal inventory</p>
<h1>Intranet Services</h1>
<p>Live snapshot of nginx vhosts and Docker workloads across Chelsea's network.</p>
</header>
<section class="grid">
{cards_html}
</section>
</div>
</body>
</html>
"""
).strip()
def main() -> None:
reports: List[str] = []
for host in HOSTS:
try:
payload = run_remote_script(host.ssh)
reports.append(render_host_card(host, payload, error=None))
except Exception as exc: # pragma: no cover - network dependent
reports.append(render_host_card(host, payload=None, error=str(exc)))
html = render_html(reports)
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(html, encoding="utf-8")
print(f"Wrote {OUTPUT_PATH}")
if __name__ == "__main__":
main()