layered-soul/skills/systematic-debugging/scripts/network_story_dashboard.py
Hermes & Sam 5c5df32101 Populate layered-soul: identity, memories, skills, plan (Hermes & Sam)
- SOUL.md: full agent identity, operating principles, voice
- IDENTITY.md: runtime identity, hosts, boundaries
- USER.md: operator context imported from hermes-soul
- AGENTS.md: actual operating rules, infrastructure, quick reference
- memories/curated/: 5 topics (tailscale, forgejo, agents, projects, vaultwarden)
- skills/: 9 cross-harness skills imported from hermes-soul after review
- docs/PLAN-CONFIGURE-PRIVATE-REPO.md: configuration plan
- Validate: passes clean
2026-06-14 00:21:26 +02:00

102 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Minimal static network story dashboard builder.
Purpose: turn bounded network diagnostic logs into a non-technical HTML story
with spike charts, line toggles, filters, and hidden technical details.
Expected layout:
input logs: ~/.local/state/hermes/net-tests/
output HTML: ~/.local/share/hermes/net-dashboard/dashboard.html
This is a template/script for future sessions. It intentionally avoids Node,
databases, and large dependencies. Customize parsers for the exact log schema.
"""
import json
from datetime import datetime
from pathlib import Path
HOME = Path.home()
LOG_DIR = HOME / ".local/state/hermes/net-tests"
OUT_DIR = HOME / ".local/share/hermes/net-dashboard"
OUT = OUT_DIR / "dashboard.html"
def load_live_downloads():
events = []
for path in sorted(LOG_DIR.glob("live-download-*.jsonl")):
samples = []
for line in path.read_text(errors="replace").splitlines():
try:
row = json.loads(line)
except Exception:
continue
if row.get("type") == "sample":
samples.append(row)
if not samples:
continue
def ping(sample, target):
for p in sample.get("ping", []):
if p.get("target") == target:
return p.get("avg_ms")
return None
def tcp_bytes(sample, label):
for c in sample.get("tcp", []):
if c.get("label") == label:
return c.get("bytes_received")
return None
base_bytes = tcp_bytes(samples[0], "osa_public_https") or 0
series = []
for s in samples:
series.append({
"t": s.get("elapsed_s", 0),
"gateway": ping(s, "10.91.179.29"),
"internet": ping(s, "1.1.1.1"),
"domedog": ping(s, "100.103.255.41"),
"downloadMb": round(((tcp_bytes(s, "osa_public_https") or base_bytes) - base_bytes) / 1_000_000, 1),
"freeGb": s.get("disk", {}).get("free_gb"),
})
events.append({
"title": "Large download stress test",
"file": str(path),
"series": series,
"plain": [
"Green staying low means the local Wi-Fi/hotspot hop is healthy.",
"Yellow/red spikes mean the internet or remote SSH path felt laggy.",
"Blue rising means the download was active.",
],
})
return events
def build_html(data):
# Raw JSON in application/json. Do not html.escape() it; that creates "
# and JSON.parse(textContent) fails. Only protect closing script tags.
data_json = json.dumps(data, ensure_ascii=False).replace("</", "<\\/")
return f"""<!doctype html>
<html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Network Story Dashboard</title>
<style>
body{{margin:0;background:#07111f;color:#edf4ff;font-family:system-ui,-apple-system,Segoe UI,sans-serif}}header{{padding:24px 32px;border-bottom:1px solid #26364f}}main{{padding:24px 32px}}.card{{background:#101b2d;border:1px solid #26364f;border-radius:18px;padding:18px;margin:0 0 16px}}button,label{{cursor:pointer}}canvas{{width:100%;height:340px;background:#07101e;border:1px solid #22334d;border-radius:14px}}.muted{{color:#9fb0c8}}
</style></head><body>
<header><h1>Network Story Dashboard</h1><p class='muted'>Low flat lines are good. Tall spikes mean lag. Generated {datetime.now().isoformat(sep=' ', timespec='seconds')}.</p></header>
<main><div class='card'><h2>Did the network spike?</h2><label><input type='checkbox' data-line='gateway' checked> Local WiFi hop</label> <label><input type='checkbox' data-line='internet' checked> Internet lag</label> <label><input type='checkbox' data-line='domedog' checked> Tailscale/domdog lag</label> <label><input type='checkbox' data-line='downloadMb' checked> Download MB</label><canvas id='chart'></canvas></div><div id='events'></div></main>
<script id='data' type='application/json'>{data_json}</script>
<script>
const DATA=JSON.parse(document.getElementById('data').textContent); const event=DATA.events.find(e=>e.series)||{{series:[]}}; const colors={{gateway:'#34d399',internet:'#fbbf24',domedog:'#fb7185',downloadMb:'#60a5fa'}}; let visible={{gateway:true,internet:true,domedog:true,downloadMb:true}};
function draw(){{const c=document.getElementById('chart'),ctx=c.getContext('2d'),r=c.getBoundingClientRect(),dpr=devicePixelRatio||1;c.width=r.width*dpr;c.height=r.height*dpr;ctx.scale(dpr,dpr);const W=r.width,H=r.height,p=38,s=event.series;ctx.clearRect(0,0,W,H);ctx.strokeStyle='#26364f';for(let i=0;i<5;i++){{let y=p+(H-2*p)*i/4;ctx.beginPath();ctx.moveTo(p,y);ctx.lineTo(W-p,y);ctx.stroke();}}let keys=Object.keys(visible).filter(k=>visible[k]),maxT=Math.max(1,...s.map(x=>x.t)),maxY=1;for(const k of keys)for(const x of s)if(x[k]!=null)maxY=Math.max(maxY,x[k]);maxY*=1.08;for(const k of keys){{ctx.strokeStyle=colors[k];ctx.lineWidth=2.5;ctx.beginPath();let started=false;for(const x of s){{if(x[k]==null){{started=false;continue}}let xx=p+(W-2*p)*x.t/maxT,yy=H-p-(H-2*p)*x[k]/maxY;if(!started){{ctx.moveTo(xx,yy);started=true}}else ctx.lineTo(xx,yy)}}ctx.stroke();}}}}
document.querySelectorAll('[data-line]').forEach(cb=>cb.onchange=()=>{{visible[cb.dataset.line]=cb.checked;draw();}});document.getElementById('events').innerHTML=DATA.events.map(e=>`<div class='card'><h2>${{e.title}}</h2><ul>${{(e.plain||[]).map(x=>`<li>${{x}}</li>`).join('')}}</ul><details><summary>technical source</summary><p class='muted'>${{e.file}}</p></details></div>`).join('');addEventListener('resize',draw);draw();
</script></body></html>"""
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
data = {"events": load_live_downloads()}
OUT.write_text(build_html(data))
print(OUT)
if __name__ == "__main__":
main()