-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathradar.html
More file actions
117 lines (108 loc) · 4.67 KB
/
Copy pathradar.html
File metadata and controls
117 lines (108 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Xw By Phantekzy</title>
<style>
body { background:#0b0f0d; color:#c9f5da; font-family: monospace; display:flex; flex-direction:column; align-items:center; padding:2rem; }
h1 { font-size:14px; letter-spacing:2px; text-transform:uppercase; color:#6fdc9a; }
#radar { position:relative; width:480px; height:480px; border-radius:50%; background:radial-gradient(circle, #0f1a13 0%, #060906 100%); border:1px solid #1f3d29; overflow:hidden; }
.ring { position:absolute; border:1px solid #1f3d29; border-radius:50%; }
.crosshair { position:absolute; background:#1f3d29; }
#sweep { position:absolute; left:50%; top:50%; width:50%; height:2px; background:linear-gradient(90deg, rgba(111,220,154,0.9), transparent); transform-origin:left center; }
.blip { position:absolute; width:14px; height:14px; border-radius:50%; transform:translate(-50%,-50%); box-shadow:0 0 8px currentColor; }
.blip.motion { animation:pulse 0.6s ease-out infinite; }
@keyframes pulse { 0%{ box-shadow:0 0 4px currentColor } 50%{ box-shadow:0 0 16px currentColor } 100%{ box-shadow:0 0 4px currentColor } }
.label { position:absolute; font-size:10px; white-space:nowrap; transform:translate(8px,-6px); }
#table { margin-top:1.5rem; width:480px; border-collapse:collapse; font-size:12px; }
#table th, #table td { border-bottom:1px solid #1f3d29; padding:4px 8px; text-align:left; }
#table th { color:#6fdc9a; font-weight:normal; }
#config { margin-top:1rem; font-size:11px; color:#6b8d78; max-width:480px; text-align:center; }
input { background:#0f1a13; color:#c9f5da; border:1px solid #1f3d29; padding:4px 6px; font-family:monospace; }
</style>
</head>
<body>
<h1>wifi-presence // radar</h1>
<div id="radar">
<div class="ring" style="inset:20%"></div>
<div class="ring" style="inset:45%"></div>
<div class="crosshair" style="left:50%;top:0;bottom:0;width:1px"></div>
<div class="crosshair" style="top:50%;left:0;right:0;height:1px"></div>
<div id="sweep"></div>
<div id="blips"></div>
</div>
<table id="table">
<thead><tr><th>node</th><th>state</th><th>guess</th><th>score</th><th>rssi</th><th>seen</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<div id="config">
Aggregator: <input id="endpoint" value="http://localhost:8090/api/status" size="34" />
Node layout (angle degrees) is set below — edit NODE_ANGLES in this file to match your floorplan.
This is a configured label, not a measured position: the system has no antenna array.
</div>
<script>
const NODE_ANGLES = {
"living-room": 40,
"hallway": 160,
"kitchen": 260,
};
const colors = { motion: "#ff5c5c", occupied: "#e8c15c", empty: "#3a5c46" };
let angle = 0;
function sweep() {
angle = (angle + 2) % 360;
document.getElementById("sweep").style.transform = "rotate(" + angle + "deg)";
requestAnimationFrame(sweep);
}
requestAnimationFrame(sweep);
async function poll() {
const endpoint = document.getElementById("endpoint").value;
try {
const res = await fetch(endpoint);
const nodes = await res.json();
render(nodes);
} catch (e) {
document.getElementById("rows").innerHTML =
'<tr><td colspan="6" style="color:#ff5c5c">could not reach ' + endpoint + " (" + e + ")</td></tr>";
}
setTimeout(poll, 800);
}
function render(nodes) {
const blips = document.getElementById("blips");
blips.innerHTML = "";
const rows = document.getElementById("rows");
rows.innerHTML = "";
nodes.forEach((n, i) => {
const deg = NODE_ANGLES[n.node_id] ?? (i * 137) % 360;
const rad = (deg * Math.PI) / 180;
const r = Math.max(15, 42 - Math.min(n.avg_score, 8) * 3);
const x = 50 + r * Math.cos(rad);
const y = 50 + r * Math.sin(rad);
const blip = document.createElement("div");
blip.className = "blip" + (n.state === "motion" ? " motion" : "");
blip.style.left = x + "%";
blip.style.top = y + "%";
blip.style.color = colors[n.state] || "#3a5c46";
blip.style.background = colors[n.state] || "#3a5c46";
blips.appendChild(blip);
const label = document.createElement("div");
label.className = "label";
label.style.left = x + "%";
label.style.top = y + "%";
label.style.color = colors[n.state] || "#6b8d78";
label.textContent = n.node_id;
blips.appendChild(label);
const tr = document.createElement("tr");
tr.innerHTML =
"<td>" + n.node_id + "</td>" +
"<td style='color:" + (colors[n.state] || "#6b8d78") + "'>" + n.state + "</td>" +
"<td>" + n.guess + "</td>" +
"<td>" + n.score.toFixed(2) + "</td>" +
"<td>" + n.rssi + "</td>" +
"<td>" + (n.last_seen_ms_ago / 1000).toFixed(1) + "s</td>";
rows.appendChild(tr);
});
}
poll();
</script>
</body>
</html>