use anyhow::{Context, Result};
use include_dir::{include_dir, Dir};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener as StdListener};
use tiny_http::{Header, Response, Server};
static DASHBOARD: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/dashboard-dist");
const PREFERRED_HOSTNAME: &str = "local.giffstack.com";
const PREFERRED_PORT: u16 = 51743;
pub fn run() -> Result<()> {
let listener = bind_port().context("binding loopback listener")?;
let port = listener.local_addr()?.port();
let branded = format!("http://{}:{}", PREFERRED_HOSTNAME, port);
let local = format!("http://localhost:{}", port);
println!("giff dashboard listening on:");
println!(
" → {} (preferred — branded URL via DNS to 127.0.0.1)",
branded
);
println!(
" → {} (fallback if your DNS blocks the above)",
local
);
println!();
println!("opening browser…");
println!("press Ctrl-C to stop");
let _ = open::that_detached(&branded);
let server =
Server::from_listener(listener, None).map_err(|e| anyhow::anyhow!("server: {}", e))?;
for request in server.incoming_requests() {
if let Err(e) = serve(request) {
eprintln!("giff dashboard: error serving request: {}", e);
}
}
Ok(())
}
fn bind_port() -> Result<StdListener> {
let loopback = IpAddr::V4(Ipv4Addr::LOCALHOST);
if let Ok(l) = StdListener::bind(SocketAddr::new(loopback, PREFERRED_PORT)) {
return Ok(l);
}
StdListener::bind(SocketAddr::new(loopback, 0)).context("binding ephemeral port")
}
fn serve(request: tiny_http::Request) -> Result<()> {
let url = request.url();
let path = url.split('?').next().unwrap_or(url).trim_start_matches('/');
let path = if path.is_empty() { "index.html" } else { path };
let (bytes, mime, status): (&[u8], String, u16) = match DASHBOARD.get_file(path) {
Some(file) => {
let mime = mime_guess::from_path(path)
.first_or_octet_stream()
.essence_str()
.to_string();
(file.contents(), mime, 200)
}
None => match DASHBOARD.get_file("index.html") {
Some(idx) => (idx.contents(), "text/html; charset=utf-8".into(), 200),
None => (b"404", "text/plain; charset=utf-8".into(), 404),
},
};
let header = Header::from_bytes(&b"Content-Type"[..], mime.as_bytes())
.map_err(|_| anyhow::anyhow!("invalid Content-Type header"))?;
let response = Response::from_data(bytes)
.with_status_code(status)
.with_header(header);
request.respond(response).context("sending response")?;
Ok(())
}