From ce912d4b8503510aa3acf0754ccde32c5f9959df Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Thu, 25 Sep 2025 22:08:59 -0400 Subject: [PATCH] Add widescreen setting and universal time --- api/Cargo.lock | 148 +++++++++++++++++++++++++++++++++-- api/Cargo.toml | 2 + api/assets/style.css | 5 +- api/src/controllers/route.rs | 107 +++++++++++++------------ api/src/main.rs | 59 ++++++++++++-- api/src/templates.rs | 17 +++- api/templates/layout.html | 21 ++++- 7 files changed, 295 insertions(+), 64 deletions(-) diff --git a/api/Cargo.lock b/api/Cargo.lock index bf07f29..7428f95 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -67,7 +67,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64", + "base64 0.22.1", "bitflags 2.9.1", "brotli", "bytes", @@ -158,6 +158,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400c27fd4cdbe0082b7bbd29ac44a3070cbda1b2114138dc106ba39fe2f90dff" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more 2.0.1", + "rand 0.9.1", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -238,6 +255,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -574,6 +626,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.22.1" @@ -753,6 +811,16 @@ dependencies = [ "phf", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.47" @@ -849,7 +917,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", "time", "version_check", ] @@ -925,9 +1000,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "der" version = "0.7.10" @@ -1374,6 +1459,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1628,7 +1723,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1795,6 +1890,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -2169,6 +2273,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.73" @@ -2356,6 +2466,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -2528,7 +2650,7 @@ version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -2656,7 +2778,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2757,6 +2879,7 @@ version = "0.1.0" dependencies = [ "actix-cors", "actix-files", + "actix-session", "actix-web", "anyhow", "askama", @@ -2764,6 +2887,7 @@ dependencies = [ "chrono-tz", "dotenv", "env_logger", + "futures-util", "libseptastic", "log", "reqwest", @@ -2974,7 +3098,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "async-io 1.13.0", "async-std", - "base64", + "base64 0.22.1", "bytes", "crc", "crossbeam-queue", @@ -3049,7 +3173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags 2.9.1", "byteorder", "bytes", @@ -3091,7 +3215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags 2.9.1", "byteorder", "crc", @@ -3554,6 +3678,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/api/Cargo.toml b/api/Cargo.toml index 0225533..9094583 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -20,3 +20,5 @@ chrono-tz = "0.10.4" actix-cors = "0.7.1" reqwest = { version = "0.12.22", features = [ "json", "blocking" ] } sqlx-cli = "0.8.6" +futures-util = "0.3.31" +actix-session = { version = "0.11.0", features = ["cookie-session"] } diff --git a/api/assets/style.css b/api/assets/style.css index 403b61c..951d28e 100644 --- a/api/assets/style.css +++ b/api/assets/style.css @@ -22,10 +22,13 @@ body { .body { background-color: #ffffff; margin: 10px auto; - max-width: 750px; width: 95%; } +.body-small { + max-width: 750px; +} + a { text-decoration: none; color: #114488; diff --git a/api/src/controllers/route.rs b/api/src/controllers/route.rs index 265c652..2bd6ae8 100644 --- a/api/src/controllers/route.rs +++ b/api/src/controllers/route.rs @@ -1,4 +1,5 @@ -use actix_web::{get, web::{Data, self}, HttpResponse, Responder}; +use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder}; +use anyhow::anyhow; use std::{time::Instant, sync::Arc}; use libseptastic::{route::RouteType, stop_schedule::Trip}; use serde::{Serialize, Deserialize}; @@ -8,27 +9,33 @@ use crate::AppState; use crate::database; #[get("/routes")] -async fn get_routes_html(state: Data>) -> impl Responder { - let start_time = Instant::now(); +async fn get_routes_html(req: HttpRequest, state: Data>) -> impl Responder { + crate::perform_action(req, move || { + let statex = state.clone(); + async move { + let start_time = Instant::now(); - let all_routes = database::get_all_routes(&mut state.database.begin().await.unwrap()).await.unwrap(); + let all_routes = database::get_all_routes(&mut statex.database.begin().await?).await?; - let rr_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect(); - let subway_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::SubwayElevated).collect(); - let trolley_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect(); - let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect(); + let rr_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect(); + let subway_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::SubwayElevated).collect(); + let trolley_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect(); + let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect(); - HttpResponse::Ok().body(crate::templates::ContentTemplate { - page_title: Some(String::from("SEPTASTIC | Routes")), - page_desc: Some(String::from("All SEPTA routes.")), - content: crate::templates::RoutesTemplate { - rr_routes, - subway_routes, - trolley_routes, - bus_routes, - }, - load_time_ms: Some(start_time.elapsed().as_millis()) - }.render().unwrap()) + Ok(crate::templates::ContentTemplate { + page_title: Some(String::from("SEPTASTIC | Routes")), + page_desc: Some(String::from("All SEPTA routes.")), + widescreen: false, + content: crate::templates::RoutesTemplate { + rr_routes, + subway_routes, + trolley_routes, + bus_routes, + }, + load_time_ms: Some(start_time.elapsed().as_millis()) + }) + } + }).await } #[get("/routes.json")] @@ -37,7 +44,7 @@ async fn get_routes_json(state: Data>) -> impl Responder { HttpResponse::Ok().json(all_routes) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct RouteQueryParams { #[serde(default)] // Optional: handle missing parameters with a default value stops: Option, @@ -67,39 +74,43 @@ async fn get_route_info(route_id: String, state: Data>) -> ::anyho } #[get("/route/{route_id}")] -async fn get_route(state: Data>, info: web::Query, path: web::Path) -> impl Responder { - let start_time = Instant::now(); +async fn get_route(state: Data>, req: HttpRequest, info: web::Query, path: web::Path) -> impl Responder { + crate::perform_action(req, move || { + let pathx = path.clone(); + let infox = info.clone(); + let statex = state.clone(); + async move { + let mut filters: Option> = None; + if let Some (stops_v) = infox.stops.clone() { + let mut items = Vec::new(); - let mut filters: Option> = None; - if let Some (stops_v) = info.stops.clone() { - let mut items = Vec::new(); - - for sid in stops_v.split(",") { - items.push(sid.parse::().unwrap()); + for sid in stops_v.split(",") { + items.push(sid.parse::().unwrap()); + } + filters = Some(items); } - filters = Some(items); - } - let route_id = path.into_inner(); - let route_info_r = get_route_info(route_id.clone(), state).await; - let load_time_ms = Some(start_time.elapsed().as_millis()); + let route_id = pathx; + let route_info_r = get_route_info(route_id.clone(), statex.clone()).await; - if let Ok(route_info) = route_info_r { - let timetables = crate::templates::build_timetables(route_info.directions, route_info.schedule); + if let Ok(route_info) = route_info_r { + let timetables = crate::templates::build_timetables(route_info.directions, route_info.schedule); - HttpResponse::Ok().body(crate::templates::ContentTemplate { - page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())), - page_desc: Some(format!("Schedule information for {}", route_id.clone())), - content: crate::templates::RouteTemplate { - route: route_info.route, - timetables, - filter_stops: filters.clone() - }, - load_time_ms - }.render().unwrap()) - } else { - HttpResponse::InternalServerError().body("Error") - } + Ok(crate::templates::ContentTemplate { + widescreen: false, + page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())), + page_desc: Some(format!("Schedule information for {}", route_id.clone())), + content: crate::templates::RouteTemplate { + route: route_info.route, + timetables, + filter_stops: filters.clone() + }, + load_time_ms: None + }) + } else { + Err(anyhow!("test")) + } + }}).await } #[get("/route/{route_id}.json")] diff --git a/api/src/main.rs b/api/src/main.rs index 1082ead..a3f6082 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -1,29 +1,75 @@ -use actix_web::{get, web::Data, App, HttpResponse, HttpServer, Responder}; +use actix_web::{cookie::Cookie, get, web::Data, App, HttpRequest, HttpResponse, HttpServer, Responder}; use env_logger::Env; use log::*; use dotenv::dotenv; +use serde::Deserialize; use services::trip_tracking::{self}; -use std::sync::Arc; +use templates::ContentTemplate; +use std::{sync::Arc, time::Instant}; use askama::Template; mod database; mod services; mod controllers; mod templates; +mod middleware; pub struct AppState { database: ::sqlx::postgres::PgPool, trip_tracking_service: services::trip_tracking::TripTrackingService } +#[derive(Deserialize)] +struct LocalStateQuery { + pub widescreen: Option +} + +pub async fn perform_action(req: HttpRequest, func: F) -> impl Responder +where T: Template, + F: Fn() -> Fut, + Fut: Future>> + 'static { + + let start_time = Instant::now(); + let mut enable_widescreen = false; + if let Some(widescreen_set) = req.cookie("widescreen") { + enable_widescreen = widescreen_set.value() == "true"; + } + + let query_params = actix_web::web::Query::::from_query(req.query_string()).unwrap(); + + if let Some(set_widescreen) = query_params.widescreen { + enable_widescreen = set_widescreen; + } + + let x = func().await; + + match x { + Ok(mut y) => { + y.widescreen = enable_widescreen; + y.load_time_ms = Some(start_time.elapsed().as_nanos()); + let mut cookie = Cookie::new("widescreen", y.widescreen.to_string()); + cookie.set_path("/"); + HttpResponse::Ok() + .cookie(cookie) + .body(y.render().unwrap()) + }, + Err(_) => { + HttpResponse::InternalServerError().body("Error") + } + } +} + #[get("/")] -async fn get_index() -> impl Responder { - HttpResponse::Ok().body(templates::ContentTemplate { +async fn get_index(req: HttpRequest) -> impl Responder { + perform_action(req, move || async { + Ok(templates::ContentTemplate { page_title: None, page_desc: None, content: templates::IndexTemplate {}, - load_time_ms: None - }.render().unwrap()) + load_time_ms: None, + widescreen: false + }) + }).await } #[actix_web::main] @@ -53,6 +99,7 @@ async fn main() -> ::anyhow::Result<()> { HttpServer::new(move || { App::new() .wrap(actix_cors::Cors::permissive()) + .wrap(actix_web::middleware::from_fn(middleware::local_state::local_state)) .app_data(Data::new(state.clone())) .service(controllers::route::api_get_route) .service(controllers::route::api_get_schedule) diff --git a/api/src/templates.rs b/api/src/templates.rs index e185504..eb39747 100644 --- a/api/src/templates.rs +++ b/api/src/templates.rs @@ -9,7 +9,8 @@ pub struct ContentTemplate { pub content: T, pub page_title: Option, pub page_desc: Option, - pub load_time_ms: Option + pub load_time_ms: Option, + pub widescreen: bool } #[derive(askama::Template)] @@ -127,6 +128,20 @@ pub fn build_timetables( } mod filters { + pub fn format_load_time( + nanos: &u128, + _: &dyn askama::Values, + ) -> askama::Result { + if *nanos >= 1000000000 { + return Ok(format!("{}s", (nanos/1000000000))); + } else if *nanos >= 1000000 { + return Ok(format!("{}ms", nanos/1000000)); + } if *nanos >= 1000 { + return Ok(format!("{}us", nanos/1000)); + } else { + return Ok(format!("{}ns", nanos)); + } + } pub fn format_time( seconds_since_midnight: &i64, _: &dyn askama::Values, diff --git a/api/templates/layout.html b/api/templates/layout.html index 33854a0..2b74790 100644 --- a/api/templates/layout.html +++ b/api/templates/layout.html @@ -18,6 +18,13 @@ + + {% if widescreen %}
+ {% else %} +
+ {% endif %}
This website is not run by SEPTA. Data may be inaccurate.
@@ -54,10 +65,18 @@ Copyright © Nicholas Orlowsky 2025

{% if let Some(load_time) = load_time_ms %} -

Data loaded in {{ load_time }}ms

+

Data loaded in {{ *load_time | format_load_time }}

+ {% endif %} +
+
+ {% if widescreen %} + [ disable widescreen ] + {% else %} + [ enable widescreen ] {% endif %}
+