- 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
102 lines
5.8 KiB
Python
102 lines
5.8 KiB
Python
#!/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 Wi‑Fi 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()
|