1pub const INDEX_HTML: &str = r#"<!doctype html>
11<html lang="de">
12<head>
13<meta charset="utf-8">
14<title>ZeroDDS Dashboard</title>
15<style>
16 body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
17 background: #1a1a1a; color: #ddd; margin: 0; padding: 12px; }
18 h1, h2 { color: #fff; }
19 h1 { font-size: 22px; margin: 0 0 16px 0; }
20 h2 { font-size: 16px; margin: 16px 0 8px 0; border-bottom: 1px solid #444; padding-bottom: 4px; }
21 table { width: 100%; border-collapse: collapse; font-size: 13px; }
22 th { text-align: left; color: #aaa; font-weight: normal; padding: 4px 8px; border-bottom: 1px solid #333; }
23 td { padding: 4px 8px; border-bottom: 1px solid #2a2a2a; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
24 td.num { text-align: right; }
25 .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
26 .card { background: #232323; border-radius: 6px; padding: 12px; }
27 #graph svg { width: 100%; height: 360px; background: #1f1f1f; border-radius: 4px; }
28 #graph .node { fill: #4a9eff; }
29 #graph .edge { stroke: #555; stroke-width: 1.5; }
30 #graph .label { fill: #ddd; font-size: 11px; }
31 button { background: #2a4d7a; color: #fff; border: 0; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
32 button:hover { background: #356da4; }
33 .status-on { color: #5fa05f; font-weight: bold; }
34 .status-off { color: #888; }
35 .meta { color: #888; font-size: 11px; }
36</style>
37</head>
38<body>
39<h1>ZeroDDS Live Dashboard</h1>
40<div class="meta">refresh every 1 s — <span id="status">connecting…</span></div>
41
42<div class="grid">
43 <div class="card">
44 <h2>Topics</h2>
45 <table id="topics-tbl">
46 <thead><tr><th>name</th><th>type</th><th class="num">pub</th><th class="num">sub</th><th class="num">rate hz</th></tr></thead>
47 <tbody></tbody>
48 </table>
49 </div>
50 <div class="card">
51 <h2>Participants</h2>
52 <table id="participants-tbl">
53 <thead><tr><th>name</th><th>guid prefix</th><th class="num">domain</th><th>vendor</th></tr></thead>
54 <tbody></tbody>
55 </table>
56 </div>
57</div>
58
59<div class="grid">
60 <div class="card">
61 <h2>Histograms</h2>
62 <table id="hist-tbl">
63 <thead><tr><th>metric</th><th class="num">count</th><th class="num">p50</th><th class="num">p99</th><th class="num">max</th></tr></thead>
64 <tbody></tbody>
65 </table>
66 </div>
67 <div class="card">
68 <h2>Recording</h2>
69 <p>Status: <span id="rec-status" class="status-off">off</span></p>
70 <p>Path: <span id="rec-path">—</span></p>
71 <p>Frames: <span id="rec-frames">0</span></p>
72 <button id="rec-toggle">Toggle</button>
73 </div>
74</div>
75
76<div class="card">
77 <h2>Discovery Graph</h2>
78 <div id="graph"></div>
79</div>
80
81<script src="https://d3js.org/d3.v7.min.js"></script>
82<script>
83const fmtNs = ns => ns >= 1_000_000 ? (ns/1_000_000).toFixed(2) + ' ms'
84 : ns >= 1_000 ? (ns/1_000).toFixed(1) + ' µs'
85 : ns + ' ns';
86
87async function refresh() {
88 try {
89 const [topics, parts, hists, graph, rec] = await Promise.all([
90 fetch('/api/topics').then(r => r.json()),
91 fetch('/api/participants').then(r => r.json()),
92 fetch('/api/histograms').then(r => r.json()),
93 fetch('/api/graph').then(r => r.json()),
94 fetch('/api/recording').then(r => r.json()),
95 ]);
96 document.getElementById('status').textContent = 'connected (' + new Date().toLocaleTimeString() + ')';
97 renderTopics(topics);
98 renderParticipants(parts);
99 renderHistograms(hists);
100 renderGraph(graph);
101 renderRecording(rec);
102 } catch (e) {
103 document.getElementById('status').textContent = 'error: ' + e.message;
104 }
105}
106
107function renderTopics(items) {
108 const tbody = document.querySelector('#topics-tbl tbody');
109 tbody.innerHTML = items.map(t =>
110 `<tr><td>${esc(t.name)}</td><td>${esc(t.type_name)}</td>` +
111 `<td class="num">${t.publishers}</td><td class="num">${t.subscribers}</td>` +
112 `<td class="num">${t.sample_rate_hz.toFixed(1)}</td></tr>`
113 ).join('');
114}
115function renderParticipants(items) {
116 const tbody = document.querySelector('#participants-tbl tbody');
117 tbody.innerHTML = items.map(p =>
118 `<tr><td>${esc(p.name)}</td><td>${esc(p.guid_prefix_hex)}</td>` +
119 `<td class="num">${p.domain_id}</td><td>${esc(p.vendor_id_hex)}</td></tr>`
120 ).join('');
121}
122function renderHistograms(items) {
123 const tbody = document.querySelector('#hist-tbl tbody');
124 tbody.innerHTML = items.map(h =>
125 `<tr><td>${esc(h.name)}</td><td class="num">${h.count}</td>` +
126 `<td class="num">${fmtNs(h.p50_ns)}</td><td class="num">${fmtNs(h.p99_ns)}</td>` +
127 `<td class="num">${fmtNs(h.max_ns)}</td></tr>`
128 ).join('');
129}
130function renderRecording(r) {
131 const st = document.getElementById('rec-status');
132 st.textContent = r.active ? 'on' : 'off';
133 st.className = r.active ? 'status-on' : 'status-off';
134 document.getElementById('rec-path').textContent = r.output_path || '—';
135 document.getElementById('rec-frames').textContent = r.frames;
136}
137document.getElementById('rec-toggle').onclick = async () => {
138 await fetch('/api/recording/toggle', { method: 'POST' });
139 refresh();
140};
141
142let graphSim = null;
143function renderGraph(g) {
144 const div = document.getElementById('graph');
145 div.innerHTML = '';
146 const svg = d3.select('#graph').append('svg');
147 const width = div.clientWidth, height = 360;
148 svg.attr('viewBox', `0 0 ${width} ${height}`);
149
150 const link = svg.append('g').selectAll('line').data(g.edges)
151 .join('line').attr('class', 'edge');
152 const node = svg.append('g').selectAll('circle').data(g.nodes)
153 .join('circle').attr('class', 'node').attr('r', 8);
154 const labels = svg.append('g').selectAll('text').data(g.nodes)
155 .join('text').attr('class', 'label').text(d => d.label).attr('dx', 10).attr('dy', 4);
156
157 graphSim = d3.forceSimulation(g.nodes)
158 .force('link', d3.forceLink(g.edges).id(d => d.guid).distance(80))
159 .force('charge', d3.forceManyBody().strength(-200))
160 .force('center', d3.forceCenter(width / 2, height / 2))
161 .on('tick', () => {
162 link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
163 .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
164 node.attr('cx', d => d.x).attr('cy', d => d.y);
165 labels.attr('x', d => d.x).attr('y', d => d.y);
166 });
167 // Edge-Source/Target sind im JSON GUIDs — d3.forceLink mapped sie via id().
168 g.edges.forEach(e => { e.source = e.from_guid; e.target = e.to_guid; });
169}
170function esc(s) { return String(s).replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c])); }
171
172refresh();
173setInterval(refresh, 1000);
174</script>
175</body>
176</html>
177"#;