big day for the unemployed 2
we have uhhhh some shuffling around of the Shits support of multiple queries (thanks copilot) more idk
This commit is contained in:
+1
-1
@@ -4,4 +4,4 @@
|
|||||||
# advices from gpt5rustup target add armv7-unknown-linux-musleabihf
|
# advices from gpt5rustup target add armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
target = "armv7-unknown-linux-musleabihf"
|
# target = "armv7-unknown-linux-musleabihf"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
you can use block code as so:
|
||||||
|
```rust
|
||||||
|
hello_world!(println);
|
||||||
|
```
|
||||||
|
in the chat.
|
||||||
Generated
+40
-1
@@ -99,6 +99,31 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-extra"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_html_form",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-macros"
|
name = "axum-macros"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -804,6 +829,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_html_form"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"indexmap 2.11.0",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.143"
|
version = "1.0.143"
|
||||||
@@ -1141,9 +1179,10 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wakey"
|
name = "wakey"
|
||||||
version = "0.0.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"futures",
|
"futures",
|
||||||
"macaddr",
|
"macaddr",
|
||||||
|
|||||||
+2
-1
@@ -1,11 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey"
|
name = "wakey"
|
||||||
version = "0.0.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8.4", features = ["macros"] }
|
axum = { version = "0.8.4", features = ["macros"] }
|
||||||
|
axum-extra = { version = "0.10.1", features = ["query"] }
|
||||||
color-eyre = "0.6.5"
|
color-eyre = "0.6.5"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
|
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
|
||||||
|
|||||||
+21
-2
@@ -17,6 +17,7 @@ use strum::{Display, EnumString};
|
|||||||
|
|
||||||
use crate::arpparse::error::IPNeighParseError;
|
use crate::arpparse::error::IPNeighParseError;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod r#impl; // custom (de)serialization impls
|
||||||
/// ip neigh has some cool shit.
|
/// ip neigh has some cool shit.
|
||||||
/// IP
|
/// IP
|
||||||
/// dev DEV | None
|
/// dev DEV | None
|
||||||
@@ -36,22 +37,26 @@ pub struct IpNeighLine {
|
|||||||
pub state: NUDState,
|
pub state: NUDState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// serialize an [`Option<MacAddr>`]
|
||||||
pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
|
pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
|
||||||
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
|
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// deserialize an [`Option<MacAddr>`]
|
||||||
pub fn des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
|
pub fn des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
Option::<&str>::deserialize(des)?
|
Option::<&str>::deserialize(des)?
|
||||||
.map(MacAddr::from_str)
|
.map(str::parse)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(de::Error::custom)
|
.map_err(de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize)]
|
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize)]
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
pub enum NUDState {
|
pub enum NUDState {
|
||||||
/// the neighbour entry is valid forever and can
|
/// the neighbour entry is valid forever and can
|
||||||
@@ -92,6 +97,20 @@ pub enum NUDState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NUDState {
|
impl NUDState {
|
||||||
|
/// Argument form expected by `ip neigh ... nud <state>` (lowercase)
|
||||||
|
pub const fn as_ip_neigh_arg(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
NUDState::Permanent => "permanent",
|
||||||
|
NUDState::Reachable => "reachable",
|
||||||
|
NUDState::Stale => "stale",
|
||||||
|
NUDState::Delay => "delay",
|
||||||
|
NUDState::Probe => "probe",
|
||||||
|
NUDState::Incomplete => "incomplete",
|
||||||
|
NUDState::Noarp => "noarp",
|
||||||
|
NUDState::None => "none",
|
||||||
|
NUDState::Failed => "failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
/// dumb UI label
|
/// dumb UI label
|
||||||
pub fn dumber_state(&self) -> &'static str {
|
pub fn dumber_state(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use serde::{Deserialize, Deserializer, de};
|
||||||
|
|
||||||
|
use super::NUDState;
|
||||||
|
|
||||||
|
// Case-insensitive parsing for NUDState via manual Deserialize
|
||||||
|
impl<'de> Deserialize<'de> for NUDState {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: &str = <&str as Deserialize>::deserialize(deserializer)?;
|
||||||
|
s.parse().map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-5
@@ -7,12 +7,15 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
mod arpparse;
|
mod arpparse;
|
||||||
mod route;
|
mod r#static;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod route;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
route::DeviceQuery,
|
route::DeviceQuery,
|
||||||
utils::{ping::ping_ip, query::get_macs_2_1, wake::wake},
|
utils::{ping::ping_ip, query::get_macs_2_1, wake::wake},
|
||||||
};
|
};
|
||||||
|
use r#static as st;
|
||||||
|
|
||||||
const MACHINE_NAME: &str = "lda.lan";
|
const MACHINE_NAME: &str = "lda.lan";
|
||||||
|
|
||||||
@@ -110,9 +113,14 @@ async fn main() -> color_eyre::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn main() {
|
fn main() -> color_eyre::Result<()> {
|
||||||
use std::process::exit;
|
color_eyre::install()?;
|
||||||
|
|
||||||
eprintln!("OS not supported! run this on your ahh router!");
|
// use crate::arpparse::NUDState;
|
||||||
exit(1)
|
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
||||||
|
// println!("{:?}", std::net::TcpStream::connect("svuhuvshdv:331"));
|
||||||
|
// // Err(Os { code: 11001, kind: Uncategorized, message: "No such host is known." })
|
||||||
|
Err(color_eyre::eyre::eyre!(
|
||||||
|
"OS not supported! run this on your ahh router!"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-15
@@ -2,17 +2,28 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{Path, Query},
|
extract::Path,
|
||||||
http::{StatusCode, header},
|
http::{StatusCode, header},
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse, Redirect},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::Query;
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
|
||||||
use crate::{MACHINE_NAME, arpparse::{self, des_opm}, status_build, utils::query::get_macs_1};
|
// use serde_with::skip_serializing_none;
|
||||||
|
// use serde::{Deserialize, de};
|
||||||
|
// use serde_with::{OneOrMany, serde_as};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
MACHINE_NAME,
|
||||||
|
arpparse::{self, NUDState, des_opm},
|
||||||
|
st, status_build,
|
||||||
|
utils::{de_many, query::get_macs},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn home_2() -> Html<&'static str> {
|
pub async fn home_2() -> Html<&'static str> {
|
||||||
Html(include_str!("../static/home_2"))
|
Html(st::HOME_2)
|
||||||
}
|
}
|
||||||
async fn home_2_css() -> impl IntoResponse {
|
async fn home_2_css() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
@@ -20,7 +31,7 @@ async fn home_2_css() -> impl IntoResponse {
|
|||||||
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
|
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
|
||||||
(header::CACHE_CONTROL, "public, max-age=300"),
|
(header::CACHE_CONTROL, "public, max-age=300"),
|
||||||
],
|
],
|
||||||
include_str!("../static/assets/home_2.css"),
|
st::HOME_2_CSS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
async fn home_2_js() -> impl IntoResponse {
|
async fn home_2_js() -> impl IntoResponse {
|
||||||
@@ -29,7 +40,7 @@ async fn home_2_js() -> impl IntoResponse {
|
|||||||
(header::CONTENT_TYPE, "application/javascript"),
|
(header::CONTENT_TYPE, "application/javascript"),
|
||||||
(header::CACHE_CONTROL, "public, max-age=300"),
|
(header::CACHE_CONTROL, "public, max-age=300"),
|
||||||
],
|
],
|
||||||
include_str!("../static/assets/home_2.js"),
|
st::HOME_2_JS,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,42 +55,102 @@ pub fn home_2_route() -> Router {
|
|||||||
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
||||||
pub struct DeviceQuery {
|
pub struct DeviceQuery {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
ip: Option<IpAddr>,
|
// Accept single or many; ignore blanks
|
||||||
|
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||||
|
ip: Vec<IpAddr>,
|
||||||
#[serde(default, deserialize_with = "des_opm")]
|
#[serde(default, deserialize_with = "des_opm")]
|
||||||
mac: Option<MacAddr>,
|
mac: Option<MacAddr>,
|
||||||
|
/// optional interface filter (e.g., br-lan)
|
||||||
|
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||||
|
pub dev: Vec<String>,
|
||||||
|
/// optional NUD state filter; accepts any case (e.g., reachable, REACHABLE)
|
||||||
|
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||||
|
pub nud: Vec<NUDState>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
||||||
pub struct NamePath {
|
pub struct NamePath {
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, Default, serde::Serialize)]
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
name: String,
|
name: Option<String>,
|
||||||
table: Vec<arpparse::IpNeighLine>,
|
table: Vec<arpparse::IpNeighLine>,
|
||||||
|
filters: Filters,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
|
pub struct Filters {
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
ip: Vec<IpAddr>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
dev: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
nud: Vec<NUDState>,
|
||||||
|
}
|
||||||
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, serde::Serialize)]
|
#[derive(Debug, serde::Serialize)]
|
||||||
pub struct StatusError {
|
pub struct StatusError {
|
||||||
name: String,
|
name: Option<String>,
|
||||||
error: String,
|
error: String,
|
||||||
}
|
}
|
||||||
// pub struct statuserror? table? and error? on status? what is the strat here
|
|
||||||
|
|
||||||
pub async fn get_status_json(
|
pub async fn get_status_json(
|
||||||
// p: Option<Path<NamePath>>,
|
// p: Option<Path<NamePath>>,
|
||||||
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
|
Query(DeviceQuery {
|
||||||
|
name, ip, dev, nud, ..
|
||||||
|
}): Query<DeviceQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let name = /* p
|
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
|
||||||
|
if slice.is_empty() {
|
||||||
|
vec![None]
|
||||||
|
} else {
|
||||||
|
slice.iter().cloned().map(Some).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* let name = /* p
|
||||||
.map(|Path(n)| n.name)
|
.map(|Path(n)| n.name)
|
||||||
.or */(name)
|
.or */(name)
|
||||||
.unwrap_or_else(|| MACHINE_NAME.to_owned());
|
// .unwrap_or_else(|| MACHINE_NAME.to_owned())
|
||||||
match get_macs_1(&name).await {
|
; */
|
||||||
|
// ip/dev/nud already parsed; assemble options
|
||||||
|
let ips_opt = if ip.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ip.clone())
|
||||||
|
};
|
||||||
|
let dev_opts: Vec<Option<String>> = to_opts(&dev);
|
||||||
|
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
|
||||||
|
|
||||||
|
let filters = Filters { ip, dev, nud };
|
||||||
|
|
||||||
|
// Run combinations of dev/nud and merge results
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for d in &dev_opts {
|
||||||
|
for n in &nud_opts {
|
||||||
|
tasks.push(get_macs(
|
||||||
|
name.as_deref(),
|
||||||
|
ips_opt.as_deref(),
|
||||||
|
d.as_deref(),
|
||||||
|
*n,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match futures::future::try_join_all(tasks)
|
||||||
|
.await
|
||||||
|
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
|
||||||
|
{
|
||||||
Ok(table) => {
|
Ok(table) => {
|
||||||
// let canonical = format!("/api/status?name={name}");
|
// let canonical = format!("/api/status?name={name}");
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
// [(header::LINK, format!("<{canonical}>; rel=\"canonical\""))],
|
// [(header::LINK, format!("<{canonical}>; rel=\"canonical\""))],
|
||||||
Json(Status { name, table }),
|
Json(Status {
|
||||||
|
name,
|
||||||
|
table,
|
||||||
|
filters,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub const HOME_2: &str = include_str!("../static/home_2");
|
||||||
|
pub const HOME_2_CSS: &str = include_str!("../static/assets/home_2.css");
|
||||||
|
pub const HOME_2_JS: &str = include_str!("../static/assets/home_2.js");
|
||||||
@@ -27,3 +27,46 @@ pub async fn get_ips(machine_name: &str) -> io::Result<Vec<IpAddr>> {
|
|||||||
|
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
// no custom ip deserializer needed when using axum_extra::extract::Query
|
||||||
|
// but we add a generic one to ignore blanks and accept OneOrMany
|
||||||
|
pub mod de_many {
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::de;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum OneOrMany<T> {
|
||||||
|
One(T),
|
||||||
|
Many(Vec<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
T: std::str::FromStr,
|
||||||
|
T::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
match raw {
|
||||||
|
OneOrMany::One(s) => {
|
||||||
|
let t = s.trim();
|
||||||
|
if !t.is_empty() {
|
||||||
|
out.push(t.parse().map_err(de::Error::custom)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OneOrMany::Many(vs) => {
|
||||||
|
for s in vs {
|
||||||
|
let t = s.trim();
|
||||||
|
if t.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(t.parse().map_err(de::Error::custom)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
enum cuh<T> {
|
||||||
|
DNSError(io::Error),
|
||||||
|
ToolUseError,
|
||||||
|
Other(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<T> From<io::Error> for cuh<T> {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
todo!()
|
||||||
|
// if matches!(value.kind(), ) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
-3
@@ -62,12 +62,79 @@ pub async fn get_macs_1(machine_name: &str) -> io::Result<Vec<arpparse::IpNeighL
|
|||||||
let res = futures::future::try_join_all(futures).await?; // async move block errs.
|
let res = futures::future::try_join_all(futures).await?; // async move block errs.
|
||||||
Ok(res
|
Ok(res
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
||||||
.flatten() /* resolve double vec */
|
.flatten() /* resolve double vec */
|
||||||
// .flatten() /* drop parse errors (flat_map cleared) */
|
// .flatten() /* drop parse errors (flat_map cleared) */
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_macs(machine_name: Option<&str>, ips: Option<impl IntoIterator<Item = IpAddr>>, dev: Option<&str>, state: Option<NUDState>) -> io::Result<Vec<IpNeighLine>> {
|
pub async fn get_macs(
|
||||||
todo!()
|
machine_name: Option<&str>,
|
||||||
|
ips: Option<&[IpAddr]>,
|
||||||
|
dev: Option<&str>,
|
||||||
|
state: Option<NUDState>,
|
||||||
|
) -> io::Result<Vec<IpNeighLine>> {
|
||||||
|
// Collect IPs early (before any await) to avoid holding generics across await points
|
||||||
|
let ip_list: Option<Vec<IpAddr>> = ips.map(|slice| {
|
||||||
|
slice
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|ip| ip.to_canonical())
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve by machine name if no IPs provided but we have a name
|
||||||
|
let ip_list = match (ip_list, machine_name) {
|
||||||
|
(Some(list), _) => list,
|
||||||
|
(None, Some(name)) => get_ips(name).await?.into_iter().collect(),
|
||||||
|
(None, None) => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to convert NUDState to the string expected by `ip neigh`
|
||||||
|
let nud_arg = state.map(NUDState::as_ip_neigh_arg);
|
||||||
|
// let nud_arg = Rc::new(state.map(|s| s.to_string().to_lowercase()));
|
||||||
|
// Build a closure to run one `ip neigh` invocation and parse results
|
||||||
|
let run_one = |to_ip: Option<IpAddr>| async move {
|
||||||
|
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
|
||||||
|
if let Some(ip) = to_ip {
|
||||||
|
args.push("to".into());
|
||||||
|
args.push(ip.to_string());
|
||||||
|
}
|
||||||
|
if let Some(d) = dev {
|
||||||
|
args.push("dev".into());
|
||||||
|
args.push(d.to_string());
|
||||||
|
}
|
||||||
|
if let Some(nud) = nud_arg {
|
||||||
|
args.push("nud".into());
|
||||||
|
args.push(nud.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let o = exec_command("ip", args.iter().map(String::as_str).collect::<Vec<_>>()).await?; // hope to rustc that it knows how to unfuck ts
|
||||||
|
if !o.status.success() {
|
||||||
|
return Err(io::Error::other(format!(
|
||||||
|
"`ip neigh` failed{ctx} (status: {st}): {err}",
|
||||||
|
ctx = to_ip.map(|ip| format!(" for {ip}")).unwrap_or_default(),
|
||||||
|
st = o.status,
|
||||||
|
err = String::from_utf8_lossy(&o.stderr),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let lines = String::from_utf8_lossy(&o.stdout);
|
||||||
|
// Parse lines and, if a specific dev filter was used, stamp that dev onto rows
|
||||||
|
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
|
||||||
|
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
|
||||||
|
parsed.map(IpNeighLine::with_dev(d)).collect()
|
||||||
|
} else {
|
||||||
|
parsed.collect()
|
||||||
|
};
|
||||||
|
Ok::<Vec<IpNeighLine>, io::Error>(rows)
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we have specific IPs, query each; otherwise query the whole table once
|
||||||
|
if !ip_list.is_empty() {
|
||||||
|
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
|
||||||
|
let res = futures::future::try_join_all(futures).await?;
|
||||||
|
Ok(res.into_iter().flatten().collect())
|
||||||
|
} else {
|
||||||
|
run_one(None).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -111,3 +111,18 @@ pre {
|
|||||||
a {
|
a {
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#html {
|
||||||
|
overflow-y: auto;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
#html::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|||||||
+43
-8
@@ -50,7 +50,18 @@ function rankState(s) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {{ name: String, table: Array<{ ip, dev, mac, state }>} | { name: String, error }} data from /api/status */
|
function buildStatusUrl(name) {
|
||||||
|
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"]) {
|
||||||
|
const vals = qs.getAll(k);
|
||||||
|
for (const v of vals) u.searchParams.append(k, v);
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {{ name: String, table: Array<{ ip, dev, mac, state }>, filters: {ip, dev, nud}} | { name: String, error }} data from /api/status */
|
||||||
function renderStatus(data) {
|
function renderStatus(data) {
|
||||||
const tbl = document.createElement("table");
|
const tbl = document.createElement("table");
|
||||||
tbl.innerHTML = `<tr><th>IP</th><th>MAC</th><th>State</th><th>IF</th></tr>`;
|
tbl.innerHTML = `<tr><th>IP</th><th>MAC</th><th>State</th><th>IF</th></tr>`;
|
||||||
@@ -62,6 +73,22 @@ function renderStatus(data) {
|
|||||||
tbl.appendChild(tr);
|
tbl.appendChild(tr);
|
||||||
}
|
}
|
||||||
elHtml.innerHTML = "";
|
elHtml.innerHTML = "";
|
||||||
|
// Optional: show applied filters
|
||||||
|
if (data.filters) {
|
||||||
|
const parts = [];
|
||||||
|
if (Array.isArray(data.filters.ip) && data.filters.ip.length)
|
||||||
|
parts.push(`ip=[${data.filters.ip.join(", ")}]`);
|
||||||
|
if (Array.isArray(data.filters.dev) && data.filters.dev.length)
|
||||||
|
parts.push(`dev=[${data.filters.dev.join(", ")}]`);
|
||||||
|
if (Array.isArray(data.filters.nud) && data.filters.nud.length)
|
||||||
|
parts.push(`nud=[${data.filters.nud.join(", ")}]`);
|
||||||
|
if (parts.length) {
|
||||||
|
const info = document.createElement("div");
|
||||||
|
info.className = "filters";
|
||||||
|
info.textContent = `Filters: ${parts.join("; ")}`;
|
||||||
|
elHtml.appendChild(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
elHtml.appendChild(tbl);
|
elHtml.appendChild(tbl);
|
||||||
|
|
||||||
if ((data.table || []).length > 0) {
|
if ((data.table || []).length > 0) {
|
||||||
@@ -79,13 +106,21 @@ function renderStatus(data) {
|
|||||||
|
|
||||||
async function fetchStatus(name) {
|
async function fetchStatus(name) {
|
||||||
setPill("warn", "checking…");
|
setPill("warn", "checking…");
|
||||||
elLog.textContent = "GET /api/status?name=" + name;
|
const u = buildStatusUrl(name);
|
||||||
|
elLog.textContent = "GET " + u.pathname + u.search;
|
||||||
elHtml.innerHTML = "";
|
elHtml.innerHTML = "";
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/status?name=${encodeURIComponent(name)}`);
|
const r = await fetch(u);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const err = await r.json().catch((_) => (elHtml.textContent = r.text));
|
let msg = String(r.status);
|
||||||
elLog.textContent = `status error: ${err.error || r.status}`;
|
try {
|
||||||
|
const err = await r.clone().json();
|
||||||
|
msg = err.error || JSON.stringify(err);
|
||||||
|
} catch {
|
||||||
|
msg = await r.text();
|
||||||
|
}
|
||||||
|
elLog.textContent = `status error: ${msg}`;
|
||||||
|
// elHtml.textContent = msg;
|
||||||
setPill("bad", "error");
|
setPill("bad", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -107,8 +142,8 @@ async function sendWake(name) {
|
|||||||
});
|
});
|
||||||
const t = await r.text();
|
const t = await r.text();
|
||||||
elLog.textContent = t || "ok";
|
elLog.textContent = t || "ok";
|
||||||
// Optional: recheck after small delay
|
// Optional: recheck after medium delay
|
||||||
setTimeout(() => fetchStatus(name), 600);
|
setTimeout(() => fetchStatus(name), 1500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
elLog.textContent = "wake error: " + e;
|
elLog.textContent = "wake error: " + e;
|
||||||
setPill("bad", "error");
|
setPill("bad", "error");
|
||||||
@@ -137,7 +172,7 @@ elName.addEventListener("keydown", (e) => {
|
|||||||
const initial = loadName();
|
const initial = loadName();
|
||||||
if (initial) {
|
if (initial) {
|
||||||
elName.value = initial;
|
elName.value = initial;
|
||||||
// setLink(initial); // when the permalink doing YOUR job
|
setLink(initial); // when the permalink doing YOUR job
|
||||||
// auto-check on load in this A/B page
|
// auto-check on load in this A/B page
|
||||||
fetchStatus(initial);
|
fetchStatus(initial);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+2
-1
@@ -26,7 +26,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row muted" style="gap: 16px">
|
<div class="row muted" style="gap: 16px">
|
||||||
<span
|
<span
|
||||||
>Uses query (?name=...) to view its status.<!-- and header (X-Target-Name) so either extractor
|
>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
|
||||||
path works. --></span
|
path works. --></span
|
||||||
>
|
>
|
||||||
<a id="permalink" href="#">permalink</a>
|
<a id="permalink" href="#">permalink</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user