move to vanilla js/html with not-quite-hot-reloading

This commit is contained in:
Alex Janka 2024-01-07 10:01:59 +11:00
parent ccb3966078
commit aab2d73f9d
3 changed files with 132 additions and 75 deletions

View file

@ -1,16 +1,11 @@
use std::path::PathBuf;
use anyhow::{Context, Result};
use include_dir::{include_dir, Dir};
use rocket::{
fairing::{Fairing, Info, Kind},
http::{ContentType, Header, Status},
outcome::IntoOutcome,
response::Responder,
route::{Handler, Outcome},
http::Header,
serde::json::Json,
Data, Request, Response, State,
Request, Response, State,
};
use teslatte::{
vehicles::{Endpoint, GetVehicleData, VehicleData},
FleetVehicleApi,
@ -18,6 +13,10 @@ use teslatte::{
use crate::{config::Config, Coords, FleetApiAuth};
use self::static_handler::UiStatic;
mod static_handler;
pub struct ServerState {
pub config: Config,
pub auth: FleetApiAuth,
@ -29,79 +28,22 @@ pub async fn launch_server(state: ServerState) {
}
fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
let fileserver: Vec<rocket::Route> = if cfg!(debug_assertions) {
rocket::fs::FileServer::from(format!(
"{}/webapp",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
))
.into()
} else {
UiStatic {}.into()
};
rocket::build()
.attach(Cors)
.manage(state)
.mount("/", UiStatic {})
.mount("/", fileserver)
.mount("/", routes![home, charge_state, flash])
}
static UI_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../client/dist");
#[derive(Clone, Copy, Debug)]
struct UiStatic {}
impl From<UiStatic> for Vec<rocket::Route> {
fn from(server: UiStatic) -> Self {
vec![rocket::Route::ranked(
None,
rocket::http::Method::Get,
"/<path..>",
server,
)]
}
}
#[rocket::async_trait]
impl Handler for UiStatic {
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
use rocket::http::uri::fmt::Path;
let path = req
.segments::<rocket::http::uri::Segments<'_, Path>>(0..)
.ok()
.and_then(|segments| segments.to_path_buf(true).ok());
match path {
Some(p) => {
if p.as_os_str() == "" {
let index = UI_DIR.get_file("index.html").map(|v| RawHtml {
data: v.contents().to_vec(),
name: PathBuf::from("index.html"),
});
index.respond_to(req).or_forward((data, Status::NotFound))
} else {
let file = UI_DIR.get_file(&p).map(|v| RawHtml {
data: v.contents().to_vec(),
name: p,
});
file.respond_to(req).or_forward((data, Status::NotFound))
}
}
None => Outcome::forward(data, Status::NotFound),
}
}
}
struct RawHtml {
data: Vec<u8>,
name: PathBuf,
}
impl<'r> Responder<'r, 'static> for RawHtml {
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'static> {
let mut response = self.data.respond_to(request)?;
if let Some(ext) = self.name.extension() {
if let Some(ct) = ContentType::from_extension(&ext.to_string_lossy()) {
response.set_header(ct);
}
}
Ok(response)
}
}
#[get("/home")]
async fn home(state: &State<ServerState>) -> Option<String> {
let coords = state.get_coords().await.ok()?;

View file

@ -0,0 +1,75 @@
use include_dir::{include_dir, Dir};
use rocket::{
http::{ContentType, Status},
outcome::IntoOutcome,
response::Responder,
route::{Handler, Outcome},
Data, Request,
};
use std::path::PathBuf;
static UI_DIR_FILES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/webapp");
#[derive(Clone, Copy, Debug)]
pub(super) struct UiStatic {}
impl From<UiStatic> for Vec<rocket::Route> {
fn from(server: UiStatic) -> Self {
vec![rocket::Route::ranked(
None,
rocket::http::Method::Get,
"/<path..>",
server,
)]
}
}
#[rocket::async_trait]
impl Handler for UiStatic {
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
use rocket::http::uri::fmt::Path;
let path = req
.segments::<rocket::http::uri::Segments<'_, Path>>(0..)
.ok()
.and_then(|segments| segments.to_path_buf(true).ok());
match path {
Some(p) => {
if p.as_os_str() == "" {
let index = UI_DIR_FILES.get_file("index.html").map(|v| RawHtml {
data: v.contents().to_vec(),
name: PathBuf::from("index.html"),
});
index.respond_to(req).or_forward((data, Status::NotFound))
} else {
let file = UI_DIR_FILES.get_file(&p).map(|v| RawHtml {
data: v.contents().to_vec(),
name: p,
});
file.respond_to(req).or_forward((data, Status::NotFound))
}
}
None => Outcome::forward(data, Status::NotFound),
}
}
}
struct RawHtml {
data: Vec<u8>,
name: PathBuf,
}
impl<'r> Responder<'r, 'static> for RawHtml {
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'static> {
let mut response = self.data.respond_to(request)?;
if let Some(ext) = self.name.extension() {
if let Some(ct) = ContentType::from_extension(&ext.to_string_lossy()) {
response.set_header(ct);
}
}
Ok(response)
}
}

40
server/webapp/index.html Normal file
View file

@ -0,0 +1,40 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Tesla Charge Control</title>
<script type="text/javascript">
const api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port;
function flash() {
fetch(api_url + "/flash", { method: "POST" })
.then((response) => console.log(response));
}
function refresh() {
fetch(api_url + "/charge-state")
.then((response) => response.json())
.then((json) => update_charge_state(json));
}
function update_charge_state(charge_state) {
var info_div = document.getElementById("info");
while (info_div.childElementCount > 0) { info_div.removeChild(info_div.firstChild) }
var arr = ["Battery " + charge_state.battery_level + "%", "Range: " + (charge_state.battery_range * 1.60934).toFixed(1) + "km", "Charging at " + charge_state.charge_amps + " amps"];
for (line in arr) {
el = document.createElement('p');
el.appendChild(document.createTextNode(arr[line]));
info_div.appendChild(el)
}
}
</script>
</head>
<body></body>
<button onclick="refresh()">refresh</button>
<div id="info"></div>
<button onclick="flash()">flash</button>
</html>