moving things around

This commit is contained in:
lda
2025-08-24 13:51:48 +07:00 Unverified
parent 1e5bdfc225
commit 93bb04bbab
13 changed files with 486 additions and 381 deletions
Generated
+7
View File
@@ -1127,6 +1127,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "valuable"
version = "0.1.1"
@@ -1147,6 +1153,7 @@ dependencies = [
"strum",
"thiserror",
"tokio",
"urlencoding",
]
[[package]]
+1
View File
@@ -15,6 +15,7 @@ serde_with = { version = "3.14.0", features = ["json"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
thiserror = "2.0.16"
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread"] }
urlencoding = "2.1.3"
[profile.release]
opt-level = "z"
+3 -3
View File
@@ -5,11 +5,11 @@ use thiserror::Error;
#[derive(Debug, Display, Error)]
pub enum IPNeighParseError {
IpWhere,
IpWhere, // i never seen a ip neigh where the first thing aint an ip
IpParseError(AddrParseError),
DevWhere,
// DevWhere,
MacParseError(macaddr::ParseError),
StateWhere,
StateWhere, // i never seen a ip neigh without the big FAILED at the end
StateParseError(strum::ParseError),
}
+25 -34
View File
@@ -1,16 +1,27 @@
use axum::{Router, response::Html, routing::get};
use axum::{
Router,
extract::Query,
http::StatusCode,
response::{Html, IntoResponse},
routing::{get, post},
};
use tokio::net::TcpListener;
mod arpparse;
mod route;
mod utils;
use route::*;
use utils::*;
use crate::{
route::DeviceQuery,
utils::{ping::ping_ip, query::get_macs_2_1, wake::wake},
};
const MACHINE_NAME: &str = "lda.lan";
async fn home() -> Html<String> {
Html(format!(
r#"
<html>
<body>
<p><a href="/home_2">Alternate UI</a></p>
<p>the machine is {}! <a href="/status">Status</a></p>
<form method="POST" action="/wake">
<button type="submit">Wake LDA</button>
@@ -31,34 +42,15 @@ async fn home() -> Html<String> {
))
}
async fn wake_handler() -> axum::response::Result<&'static str> {
match wake(MACHINE_NAME).await {
Ok(x) if x > 0 => Ok("Packet sent!"),
_ => Err("Wake failed".into()),
async fn wake_handler(
Query(DeviceQuery { name }): Query<DeviceQuery>,
) -> axum::response::Result<impl IntoResponse> {
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
Ok(x) if x > 0 => Ok((StatusCode::ACCEPTED, format!("{x} packets sent!"))),
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
}
}
async fn status() -> Html<String> {
// let formatted_ips = match get_ips(MACHINE_NAME).await {
// Ok(ips) => {
// let string: String = ips
// .iter()
// .map(|ip| format!("<tr><td>{ip}</td></tr>"))
// .collect();
// format!(
// r#"<p>the ips of {m} are:</p>
// <table>
// <tr><th>IP</th></tr>
// {string}
// </table>"#,
// m = MACHINE_NAME
// )
// }
// Err(e) => format!("<p>error getting ips: {e}</p>"),
// };
Html(status_build(MACHINE_NAME).await)
}
pub async fn status_build(machine_name: &str) -> String {
let formatted_macs = match get_macs_2_1(machine_name).await {
Ok(table) => {
@@ -101,16 +93,15 @@ pub async fn status_build(machine_name: &str) -> String {
#[cfg(target_os = "linux")]
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
use crate::route::{api_status, get_status_2, home_2_route};
color_eyre::install()?;
let app = Router::new()
.route("/", get(home))
.route("/home_2", get(home_2))
.route("/wake", axum::routing::post(wake_handler))
// .route("/wake", axum::routing::post(wake_handler))
// .route("/status", get(status))
.merge(home_2_route())
.route("/wake", post(wake_handler))
.route("/status", get(get_status_2))
.route("/api/status/{name}", get(get_status_json))
;
.merge(api_status());
let port = TcpListener::bind("0.0.0.0:12012").await?;
axum::serve(port, app.into_make_service()).await?;
+62 -12
View File
@@ -1,19 +1,46 @@
use axum::{
Json,
Json, Router,
extract::{Path, Query},
http::StatusCode,
response::{Html, IntoResponse},
http::{StatusCode, header},
response::{Html, IntoResponse, Redirect},
routing::get,
};
use crate::{MACHINE_NAME, arpparse, status_build, utils::get_macs_1};
use crate::{MACHINE_NAME, arpparse, status_build, utils::query::get_macs_1};
pub async fn home_2() -> Html<&'static str> {
async fn home_2() -> Html<&'static str> {
Html(include_str!("../static/home_2"))
}
async fn home_2_css() -> impl IntoResponse {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
include_str!("../static/assets/home_2.css"),
)
}
async fn home_2_js() -> impl IntoResponse {
(
[
(header::CONTENT_TYPE, "application/javascript"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
include_str!("../static/assets/home_2.js"),
)
}
/// all the pages related to home_2
pub fn home_2_route() -> Router {
Router::new()
.route("/home_2", get(home_2))
.route("/home_2.css", get(home_2_css))
.route("/home_2.js", get(home_2_js)) //js
}
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
pub struct DeviceQuery {
name: Option<String>,
pub name: Option<String>,
// ip: Option<IpAddr>,
// #[serde(deserialize_with = "des_opm")]
// mac: Option<MacAddr>,
@@ -35,13 +62,24 @@ pub struct StatusError {
}
// pub struct statuserror? table? and error? on status? what is the strat here
pub async fn get_status_json(p: Option<Path<NamePath>>) -> impl IntoResponse {
let name = match p {
Some(Path(NamePath { name })) => name,
_ => MACHINE_NAME.to_owned(),
};
pub async fn get_status_json(
// p: Option<Path<NamePath>>,
Query(DeviceQuery { name }): Query<DeviceQuery>,
) -> impl IntoResponse {
let name = /* p
.map(|Path(n)| n.name)
.or */(name)
.unwrap_or_else(|| MACHINE_NAME.to_owned());
match get_macs_1(&name).await {
Ok(table) => (StatusCode::OK, Json(Status { name, table })).into_response(),
Ok(table) => {
// let canonical = format!("/api/status?name={name}");
(
StatusCode::OK,
// [(header::LINK, format!("<{canonical}>; rel=\"canonical\""))],
Json(Status { name, table }),
)
.into_response()
}
Err(error) => (
StatusCode::BAD_GATEWAY,
Json(StatusError {
@@ -52,6 +90,18 @@ pub async fn get_status_json(p: Option<Path<NamePath>>) -> impl IntoResponse {
.into_response(), // holy clutch. Couldve been disasterous
}
}
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
Redirect::permanent(&format!(
"/api/status?name={name}",
name = urlencoding::encode(&name) // just for
))
}
pub fn api_status() -> Router {
Router::new()
.route("/api/status/{name}", get(status_redirect)) // api/status should be like the entire ip neigh br lan like idk like
.route("/api/status", get(get_status_json))
}
pub async fn get_status_2(q: Query<DeviceQuery>) -> Html<String> {
let name = match q {
+6 -110
View File
@@ -7,53 +7,15 @@ pub const LDA_MACS: [[u8; 6]; 2] = [
/// is it time to lookup host lda.lan for this...
pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub async fn wake(machine_name: &str) -> io::Result<u32> {
let suh = UdpSocket::bind("0.0.0.0:0").await?;
suh.set_broadcast(true)?;
let mut macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
macs.extend(*LDA_MACS_2);
let mut sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
match suh
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
.await
{
Ok(n) if n == pac.len() => sent_ok += 1,
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
Err(e) => eprintln!("send error: {e}"),
}
}
Ok(sent_ok)
}
use std::{collections::HashSet, net::IpAddr, sync::LazyLock, time::Duration};
pub mod wake;
use std::{net::IpAddr, sync::LazyLock};
use macaddr::MacAddr;
use tokio::{
io,
net::{TcpStream, ToSocketAddrs, UdpSocket},
time::timeout,
};
use crate::arpparse::{self, IpNeighLine, NUDState};
use tokio::io;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr))
.await
.is_ok()
}
pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
todo!("use icmp")
}
pub mod ping;
/// this is because i like [`IpAddr`] more than [`SocketAddr`](std::net::SocketAddr)
pub async fn get_ips(machine_name: &str) -> io::Result<Vec<IpAddr>> {
@@ -63,71 +25,5 @@ pub async fn get_ips(machine_name: &str) -> io::Result<Vec<IpAddr>> {
.collect())
}
pub async fn get_macs_1(machine_name: &str) -> io::Result<Vec<arpparse::IpNeighLine>> {
let ips = get_ips(machine_name).await?;
let futures = ips.iter().map(|ip| {
let ip = ip.to_canonical();
async move {
let o = exec_command(
"ip",
["neigh", "show", "to", &ip.to_string(), "dev", "br-lan"],
)
.await?;
if !o.status.success() {
return Err(io::Error::other(format!(
"`ip neigh` failed for {ip} (status: {st}): {err}",
st = o.status,
err = String::from_utf8_lossy(&o.stderr),
)));
};
Ok(String::from_utf8_lossy(&o.stdout)
.lines()
.map(arpparse::parse_ip_neigh_line)
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?; // async move block errs.
Ok(res
.into_iter()
.flatten() /* resolve double vec */
.flatten() /* drop parse errors */
.collect())
}
async fn exec_command<S: AsRef<std::ffi::OsStr>>(
cmd: S,
args: impl IntoIterator<Item = S>,
) -> io::Result<std::process::Output> {
let mut u = tokio::process::Command::new(cmd);
u.args(args);
u.output().await
}
pub async fn get_macs_2_1(machine_name: &str) -> io::Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip,
dev: _,
mac,
state,
}| mac.map(|mac| (ip, mac, state)),
)
.collect())
}
pub async fn get_macs_2_mac(machine_name: &str) -> io::Result<HashSet<MacAddr>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
pub mod cmd;
pub mod query;
+10
View File
@@ -0,0 +1,10 @@
use std::io;
pub(crate) async fn exec_command<S: AsRef<std::ffi::OsStr>>(
cmd: S,
args: impl IntoIterator<Item = S>,
) -> io::Result<std::process::Output> {
let mut u = tokio::process::Command::new(cmd);
u.args(args);
u.output().await
}
+12
View File
@@ -0,0 +1,12 @@
use std::time::Duration;
use tokio::{net::{TcpStream, ToSocketAddrs}, time::timeout};
pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr))
.await
.is_ok()
}
pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
todo!("use icmp")
}
+69
View File
@@ -0,0 +1,69 @@
use std::{collections::HashSet, net::IpAddr};
use macaddr::MacAddr;
use tokio::io;
use crate::{
arpparse::{self, IpNeighLine, NUDState},
utils::{cmd::exec_command, get_ips},
};
pub async fn get_macs_2_1(machine_name: &str) -> io::Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip,
dev: _,
mac,
state,
}| mac.map(|mac| (ip, mac, state)),
)
.collect())
}
pub async fn get_macs_2_mac(machine_name: &str) -> io::Result<HashSet<MacAddr>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
pub async fn get_macs_1(machine_name: &str) -> io::Result<Vec<arpparse::IpNeighLine>> {
let ips = get_ips(machine_name).await?;
let futures = ips.iter().map(|ip| {
let ip = ip.to_canonical();
async move {
let o = exec_command(
"ip",
["neigh", "show", "to", &ip.to_string(), "dev", "br-lan"],
)
.await?;
if !o.status.success() {
return Err(io::Error::other(format!(
"`ip neigh` failed for {ip} (status: {st}): {err}",
st = o.status,
err = String::from_utf8_lossy(&o.stderr),
)));
};
Ok(String::from_utf8_lossy(&o.stdout)
.lines()
.map(arpparse::parse_ip_neigh_line)
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?; // async move block errs.
Ok(res
.into_iter()
.flatten() /* resolve double vec */
.flatten() /* drop parse errors */
.collect())
}
+32
View File
@@ -0,0 +1,32 @@
use std::{io, net::IpAddr};
use tokio::net::UdpSocket;
use crate::utils::{query::get_macs_2_mac, LDA_MACS_2};
pub async fn wake(machine_name: &str) -> io::Result<u32> {
let suh = UdpSocket::bind("0.0.0.0:0").await?;
suh.set_broadcast(true)?;
let mut macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
macs.extend(*LDA_MACS_2);
let mut sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
match suh
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
.await
{
Ok(n) if n == pac.len() => sent_ok += 1,
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
Err(e) => eprintln!("send error: {e}"),
}
}
Ok(sent_ok)
}
+113
View File
@@ -0,0 +1,113 @@
:root {
color-scheme: light dark;
--bg: #0b0b0b;
--fg: #e6e6e6;
--muted: #888;
--ok: #17a34a;
--warn: #d97706;
--bad: #dc2626;
--btn: #2563eb;
--card: #111827;
}
html,
body {
margin: 0;
padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
body {
display: flex;
min-height: 100dvh;
align-items: center;
justify-content: center;
background: var(--bg);
color: var(--fg);
}
.wrap {
width: min(900px, 95vw);
display: grid;
gap: 12px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
h1 {
font-size: 18px;
margin: 0;
font-weight: 600;
}
.muted {
color: var(--muted);
font-size: 12px;
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
input[type="text"] {
flex: 1 1 240px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #2a2a2a;
background: #0f0f0f;
color: var(--fg);
outline: none;
}
button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #2a2a2a;
background: var(--btn);
color: white;
cursor: pointer;
}
button.secondary {
background: #1f2937;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pill {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
font-size: 12px;
border: 1px solid #2a2a2a;
}
.ok {
background: #052e1a;
border-color: #064e3b;
color: #86efac;
}
.warn {
background: #2b1800;
border-color: #7c2d12;
color: #fbbf24;
}
.bad {
background: #330b0b;
border-color: #7f1d1d;
color: #fca5a5;
}
.card {
border: 1px solid #2a2a2a;
border-radius: 12px;
padding: 12px;
background: var(--card);
}
#out {
min-height: 100px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
a {
color: #93c5fd;
}
+142
View File
@@ -0,0 +1,142 @@
const qs = new URLSearchParams(location.search);
const $ = (id) => document.getElementById(id);
const elName = $("name");
const elCheck = $("check");
const elWake = $("wake");
const elLog = $("log");
const elHtml = $("html");
const pill = $("status-pill");
const link = $("permalink");
function setPill(kind, text) {
pill.className = `pill ${kind}`;
pill.textContent = text;
}
function setLink(name) {
const url = new URL(location.href);
if (name) url.searchParams.set("name", name);
else url.searchParams.delete("name");
history.replaceState(null, "", url);
link.href = url.toString();
}
function getName() {
return (elName.value || "").trim();
}
function saveName(name) {
try {
localStorage.setItem("wakey:name", name);
} catch {}
}
function loadName() {
return qs.get("name") || localStorage.getItem("wakey:name") || "";
}
function rankState(s) {
return (
{
Permanent: 5,
Reachable: 5,
Stale: 4,
Delay: 3,
Probe: 3,
Incomplete: 3,
Noarp: 2,
None: 1,
Failed: 0,
}[s] ?? 0
);
}
function renderStatus(data) {
// data: { name: string, table: Array<{ ip, dev, mac, state }>} | { name, error }
const tbl = document.createElement("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>`;
tbl.appendChild(tr);
}
elHtml.innerHTML = "";
elHtml.appendChild(tbl);
if ((data.table || []).length > 0) {
const best = data.table.reduce((a, b) =>
rankState(b.state) > rankState(a.state) ? b : a
);
const r = rankState(best.state);
if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline");
} else {
setPill("warn", "unknown");
}
}
async function fetchStatus(name) {
setPill("warn", "checking…");
elLog.textContent = "GET /api/status?name=" + name;
elHtml.innerHTML = "";
try {
const r = await fetch(`/api/status?name=${encodeURIComponent(name)}`);
if (!r.ok) {
const err = await r.json().catch(() => ({}));
elLog.textContent = `status error: ${err.error || r.status}`;
setPill("bad", "error");
return;
}
const data = await r.json();
renderStatus(data);
} catch (e) {
elLog.textContent = "status error: " + e;
setPill("bad", "error");
}
}
async function sendWake(name) {
setPill("warn", "waking…");
elLog.textContent = "POST /wake?name=" + name;
try {
const r = await fetch(`/wake?name=${encodeURIComponent(name)}`, {
method: "POST",
// headers: { "X-Target-Name": name },
});
const t = await r.text();
elLog.textContent = t || "ok";
// Optional: recheck after small delay
setTimeout(() => fetchStatus(name), 600);
} catch (e) {
elLog.textContent = "wake error: " + e;
setPill("bad", "error");
}
}
elCheck.addEventListener("click", () => {
const name = getName();
if (!name) return;
saveName(name);
setLink(name);
fetchStatus(name);
});
elWake.addEventListener("click", () => {
const name = getName();
if (!name) return;
saveName(name);
setLink(name);
sendWake(name);
});
elName.addEventListener("keydown", (e) => {
if (e.key === "Enter") elCheck.click();
});
// init
const initial = loadName();
if (initial) {
elName.value = initial;
// setLink(initial); // when the permalink doing YOUR job
// auto-check on load in this A/B page
fetchStatus(initial);
} else {
setPill("warn", "unknown");
}
+4 -222
View File
@@ -4,126 +4,13 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>wakey • home_2</title>
<style>
:root {
color-scheme: light dark;
--bg: #0b0b0b;
--fg: #e6e6e6;
--muted: #888;
--ok: #17a34a;
--warn: #d97706;
--bad: #dc2626;
--btn: #2563eb;
--card: #111827;
}
html,
body {
margin: 0;
padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
body {
display: flex;
min-height: 100dvh;
align-items: center;
justify-content: center;
background: var(--bg);
color: var(--fg);
}
.wrap {
width: min(900px, 95vw);
display: grid;
gap: 12px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
h1 {
font-size: 18px;
margin: 0;
font-weight: 600;
}
.muted {
color: var(--muted);
font-size: 12px;
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
input[type="text"] {
flex: 1 1 240px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #2a2a2a;
background: #0f0f0f;
color: var(--fg);
outline: none;
}
button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #2a2a2a;
background: var(--btn);
color: white;
cursor: pointer;
}
button.secondary {
background: #1f2937;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pill {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
font-size: 12px;
border: 1px solid #2a2a2a;
}
.ok {
background: #052e1a;
border-color: #064e3b;
color: #86efac;
}
.warn {
background: #2b1800;
border-color: #7c2d12;
color: #fbbf24;
}
.bad {
background: #330b0b;
border-color: #7f1d1d;
color: #fca5a5;
}
.card {
border: 1px solid #2a2a2a;
border-radius: 12px;
padding: 12px;
background: var(--card);
}
#out {
min-height: 100px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
a {
color: #93c5fd;
}
</style>
<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_2 <span class="muted">A/B test page</span></h1>
<span id="status-pill" class="pill warn">unknown</span>
</header>
@@ -139,7 +26,7 @@
</div>
<div class="row muted" style="gap: 16px">
<span
>Uses query (?name=...) on the age old /status.<!-- and header (X-Target-Name) so either extractor
>Uses query (?name=...) to view its status.<!-- and header (X-Target-Name) so either extractor
path works. --></span
>
<a id="permalink" href="#">permalink</a>
@@ -150,110 +37,5 @@
<div id="html"></div>
</section>
</div>
<script>
const qs = new URLSearchParams(location.search);
const $ = (id) => document.getElementById(id);
const elName = $("name");
const elCheck = $("check");
const elWake = $("wake");
const elLog = $("log");
const elHtml = $("html");
const pill = $("status-pill");
const link = $("permalink");
function setPill(kind, text) {
pill.className = `pill ${kind}`;
pill.textContent = text;
}
function setLink(name) {
const url = new URL(location.href);
if (name) url.searchParams.set("name", name);
else url.searchParams.delete("name");
history.replaceState(null, "", url);
link.href = url.toString();
}
function getName() {
return (elName.value || "").trim();
}
function saveName(name) {
try {
localStorage.setItem("wakey:name", name);
} catch {}
}
function loadName() {
return qs.get("name") || localStorage.getItem("wakey:name") || "";
}
async function fetchStatus(name) {
setPill("warn", "checking…");
elLog.textContent = "GET /status?name=" + name;
elHtml.innerHTML = "";
try {
const r = await fetch(`/status?name=${encodeURIComponent(name)}`, {
// headers: { "X-Target-Name": name },
});
const t = await r.text();
elHtml.innerHTML = t;
// Heuristic: look for words in returned HTML // das. lets look for better
const low = t.toLowerCase();
if (low.includes("online")) setPill("ok", "online");
else if (low.includes("maybe")) setPill("warn", "maybe");
else if (low.includes("offline") || low.includes("failed"))
setPill("bad", "offline");
else setPill("warn", "unknown");
} catch (e) {
elLog.textContent = "status error: " + e;
setPill("bad", "error");
}
}
async function sendWake(name) {
setPill("warn", "waking…");
elLog.textContent = "POST /wake?name=" + name;
try {
const r = await fetch(`/wake?name=${encodeURIComponent(name)}`, {
method: "POST",
// headers: { "X-Target-Name": name },
});
const t = await r.text();
elLog.textContent = t || "ok";
// Optional: recheck after small delay
setTimeout(() => fetchStatus(name), 600);
} catch (e) {
elLog.textContent = "wake error: " + e;
setPill("bad", "error");
}
}
elCheck.addEventListener("click", () => {
const name = getName();
if (!name) return;
saveName(name);
setLink(name);
fetchStatus(name);
});
elWake.addEventListener("click", () => {
const name = getName();
if (!name) return;
saveName(name);
setLink(name);
sendWake(name);
});
elName.addEventListener("keydown", (e) => {
if (e.key === "Enter") elCheck.click();
});
// init
const initial = loadName();
if (initial) {
elName.value = initial;
setLink(initial);
// auto-check on load in this A/B page
fetchStatus(initial);
} else {
setPill("warn", "unknown");
}
</script>
</body>
</html>