more bullshit IN!

This commit is contained in:
lda
2025-08-26 21:47:52 +07:00 Unverified
parent 7371d2ae8c
commit 9de286f59b
22 changed files with 845 additions and 122 deletions
+43
View File
@@ -111,6 +111,13 @@ pre {
a {
color: #93c5fd;
}
.table a.pick {
color: #93c5fd;
text-decoration: none;
}
.table a.pick:hover {
text-decoration: underline;
}
#html {
overflow-y: auto;
@@ -126,3 +133,39 @@ a {
color: var(--muted);
font-size: 12px;
}
/* simple table styling */
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 6px 8px;
border-bottom: 1px solid #2a2a2a;
font-size: 12px;
}
.table th {
text-align: left;
color: var(--muted);
font-weight: 600;
}
.tiny {
font-size: 11px;
}
.dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.dot.ok {
background: var(--ok);
}
.dot.bad {
background: var(--bad);
}
.dot.warn {
background: var(--warn);
}
+158 -4
View File
@@ -5,8 +5,21 @@ const elCheck = $("check");
const elWake = $("wake");
const elLog = $("log");
const elHtml = $("html");
const elLeases = $("leases_html");
const pill = $("status-pill");
const link = $("permalink");
const elPreview = $("preview");
// One-time delegated click handler for all pickable links
function handlePickClick(e) {
const a = e.target && /** @type {HTMLElement} */ (e.target).closest("a.pick");
if (!a) return;
e.preventDefault();
const v = a.getAttribute("data-value");
pickTarget(v);
}
if (elHtml) elHtml.addEventListener("click", handlePickClick);
if (elLeases) elLeases.addEventListener("click", handlePickClick);
function setPill(kind, text) {
pill.className = `pill ${kind}`;
@@ -22,6 +35,46 @@ function setLink(name) {
function getName() {
return (elName.value || "").trim();
}
function extractHostLikeBackend(input) {
let s = String(input || "").trim();
if (!s) return "";
// strip scheme or network-path reference
const schemeIdx = s.indexOf("://");
if (schemeIdx >= 0) s = s.slice(schemeIdx + 3);
else if (s.startsWith("//")) s = s.slice(2);
// strip userinfo
const at = s.lastIndexOf("@");
if (at >= 0) s = s.slice(at + 1);
// bracketed IPv6
if (s.startsWith("[")) {
const end = s.indexOf("]");
if (end > 1) s = s.slice(1, end);
} else {
const slash = s.indexOf("/");
if (slash >= 0) s = s.slice(0, slash);
const colon = s.lastIndexOf(":");
if (colon > 0 && (s.match(/:/g) || []).length === 1) {
const port = s.slice(colon + 1);
if (/^\d+$/.test(port)) s = s.slice(0, colon);
}
}
return s.trim();
}
function updatePreview() {
if (!elPreview) return;
const raw = getName();
const host = extractHostLikeBackend(raw);
elPreview.textContent = host && host !== raw ? `${host}` : "";
}
function pickTarget(value) {
const v = String(value || "").trim();
if (!v) return;
elName.value = v;
updatePreview();
saveName(v);
setLink(v);
fetchStatus(v);
}
function saveName(name) {
try {
localStorage.setItem("wakey:name", name);
@@ -51,9 +104,15 @@ function rankState(s) {
}
function buildStatusUrl(name) {
// If no extra filters, use smart redirect for name/ip/mac/dev/nud detection
const hasExtraFilters = ["ip", "dev", "nud", "mac"].some(
(k) => qs.getAll(k).length
);
if (name && !hasExtraFilters) {
return new URL(`/api/smart/${encodeURIComponent(name)}`, location.origin);
}
const u = new URL("/api/status", location.origin);
if (name) u.searchParams.set("name", name);
// forward multi-value filters from current page URL
for (const k of ["ip", "dev", "nud", "mac"]) {
const vals = qs.getAll(k);
for (const v of vals) u.searchParams.append(k, v);
@@ -64,12 +123,35 @@ function buildStatusUrl(name) {
/** @param {{ name?: String, table: Array<{ ip, dev, mac, state }>, filters: {ip, dev, nud, mac}} | { name?: String, error }} data from /api/status */
function renderStatus(data) {
const tbl = document.createElement("table");
tbl.className = "table";
tbl.innerHTML = `<tr><th>IP</th><th>MAC</th><th>State</th><th>IF</th></tr>`;
for (const row of data.table || []) {
const tr = document.createElement("tr");
const mac = row.mac ?? "";
const dev = row.dev ?? "";
tr.innerHTML = `<td>${row.ip}</td><td>${mac}</td><td>${row.state}</td><td>${dev}</td>`;
const ip = row.ip || "";
const mac = row.mac || "";
const dev = row.dev || "";
const state = row.state || "";
tr.innerHTML = `
<td>${
ip
? `<a href="#" class="pick" data-value="${ip}" title="filter by ip">${ip}</a>`
: ""
}</td>
<td>${
mac
? `<a href="#" class="pick" data-value="${mac}" title="filter by mac">${mac}</a>`
: ""
}</td>
<td>${
state
? `<a href="#" class="pick" data-value="${state}" title="filter by state">${state}</a>`
: ""
}</td>
<td>${
dev
? `<a href="#" class="pick" data-value="${dev}" title="filter by interface">${dev}</a>`
: ""
}</td>`;
tbl.appendChild(tr);
}
elHtml.innerHTML = "";
@@ -93,6 +175,8 @@ function renderStatus(data) {
}
elHtml.appendChild(tbl);
// click-to-filter handled by a single, persistent delegated listener (set once at load)
if ((data.table || []).length > 0) {
const best = data.table.reduce((a, b) =>
rankState(b.state) > rankState(a.state) ? b : a
@@ -134,6 +218,72 @@ async function fetchStatus(name) {
}
}
function renderLeases(leases) {
if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) {
elLeases.textContent = "no leases";
return;
}
const tbl = document.createElement("table");
tbl.className = "table";
tbl.innerHTML = `<tr><th></th><th>IP</th><th>MAC</th><th>Name</th><th>Expires</th></tr>`;
const nowSec = Math.floor(Date.now() / 1000);
for (const l of leases) {
const tr = document.createElement("tr");
const exp = Number(l.expires_epoch || 0);
const expired = exp > 0 && exp <= nowSec;
const when = exp > 0 ? new Date(exp * 1000) : null;
const whenText = when ? when.toLocaleString() : "";
let dotClass = "dot ok";
if (expired) {
dotClass = "dot bad";
} else if (typeof l?.rank === "number") {
if (l.rank >= 5) dotClass = "dot ok";
else if (l.rank >= 2) dotClass = "dot warn";
else dotClass = "dot bad";
}
const title = expired
? "expired"
: l?.nud_state
? String(l.nud_state).toLowerCase()
: "unknown";
const ip = l.ip || "";
const mac = l.mac || "";
const name = l.name || "";
tr.innerHTML = `
<td><span class="${dotClass}" title="${title}"></span></td>
<td>${
ip
? `<a href="#" class="pick" data-value="${ip}" title="filter by ip">${ip}</a>`
: ""
}</td>
<td>${
mac
? `<a href="#" class="pick" data-value="${mac}" title="filter by mac">${mac}</a>`
: ""
}</td>
<td>${
name
? `<a href="#" class="pick" data-value="${name}" title="filter by name">${name}</a>`
: ""
}</td>
<td><span class="tiny">${whenText}</span></td>`;
tbl.appendChild(tr);
}
elLeases.innerHTML = "";
elLeases.appendChild(tbl);
// click-to-filter handled by a single, persistent delegated listener (set once at load)
}
async function fetchLeases() {
try {
const r = await fetch("/api/dhcp_leases?include_state=1");
if (!r.ok) return;
const leases = await r.json();
renderLeases(leases);
} catch {}
}
async function sendWake(name) {
setPill("warn", "waking…");
elLog.textContent = "POST /wake?name=" + name;
@@ -169,14 +319,18 @@ elWake.addEventListener("click", () => {
elName.addEventListener("keydown", (e) => {
if (e.key === "Enter") elCheck.click();
});
elName.addEventListener("input", updatePreview);
// init
const initial = loadName();
if (initial) {
elName.value = initial;
updatePreview();
setLink(initial); // when the permalink doing YOUR job
// auto-check on load in this A/B page
fetchStatus(initial);
fetchLeases();
} else {
setPill("warn", "unknown");
fetchLeases();
}
+15 -7
View File
@@ -3,14 +3,14 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>wakey • home_2</title>
<title>wakey • home</title>
<link rel="stylesheet" href="/home_2.css" />
<script type="module" src="/home_2.js" defer></script>
</head>
<body>
<div class="wrap">
<header>
<h1>home_2 <span class="muted">A/B test page</span></h1>
<h1>home <span class="muted">ping your device with a WoL</span></h1>
<span id="status-pill" class="pill warn">unknown</span>
</header>
@@ -18,7 +18,7 @@
<input
id="name"
type="text"
placeholder="target name e.g. lda.lan"
placeholder="target (name, ip, mac...)"
spellcheck="false"
/>
<button id="check" class="secondary">Check</button>
@@ -26,17 +26,25 @@
</div>
<div class="row muted" style="gap: 16px">
<span
>Uses query <span title="available keys: name, ip, dev, nud">(?name=...)</span> on this page to view the
status.<!-- and header (X-Target-Name) so either extractor
>Uses query
<span title="available keys: name, ip, mac, dev, nud"
>(?name=...)</span
>
on this page to view the status.<!-- and header (X-Target-Name) so either extractor
path works. --></span
>
<a id="permalink" href="#">permalink</a>
><span id="preview" class="tiny"></span
><a id="permalink" href="#">permalink</a>
</div>
<section id="out" class="card">
<pre id="log">ready.</pre>
<div id="html"></div>
</section>
<section id="leases" class="card">
<h3 style="margin-top: 0">DHCP leases</h3>
<div id="leases_html"></div>
</section>
</div>
</body>
</html>