add load time and page titles
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 15m56s
All checks were successful
Create and publish a Docker image / build-and-push-image (push) Successful in 15m56s
This commit is contained in:
parent
8fd41b1090
commit
95bef54eed
6 changed files with 32 additions and 177 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
use actix_web::{get, web::{Data, self}, HttpResponse, Responder};
|
use actix_web::{get, web::{Data, self}, HttpResponse, Responder};
|
||||||
use std::{cmp::Ordering, sync::Arc};
|
use std::{cmp::Ordering, time::Instant, sync::Arc};
|
||||||
use libseptastic::{route::RouteType, stop_schedule::Trip};
|
use libseptastic::{route::RouteType, stop_schedule::Trip};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
@ -9,6 +9,8 @@ use crate::database;
|
||||||
|
|
||||||
#[get("/routes")]
|
#[get("/routes")]
|
||||||
async fn get_routes_html(state: Data<Arc<AppState>>) -> impl Responder {
|
async fn get_routes_html(state: Data<Arc<AppState>>) -> impl Responder {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
|
||||||
let mut all_routes = database::get_all_routes(&mut state.database.begin().await.unwrap()).await.unwrap();
|
let mut all_routes = database::get_all_routes(&mut state.database.begin().await.unwrap()).await.unwrap();
|
||||||
all_routes.sort_by(|x, y| {
|
all_routes.sort_by(|x, y| {
|
||||||
|
|
@ -21,15 +23,20 @@ async fn get_routes_html(state: Data<Arc<AppState>>) -> impl Responder {
|
||||||
return if y.id > x.id { Ordering::Less } else { Ordering::Greater };
|
return if y.id > x.id { Ordering::Less } else { Ordering::Greater };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
HttpResponse::Ok().body(crate::templates::ContentTemplate {
|
||||||
page_title: None,
|
page_title: Some(String::from("SEPTASTIC | Routes")),
|
||||||
page_desc: None,
|
page_desc: Some(String::from("All SEPTA routes.")),
|
||||||
content: crate::templates::RoutesTemplate {
|
content: crate::templates::RoutesTemplate {
|
||||||
rr_routes: all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect(),
|
rr_routes,
|
||||||
subway_routes: all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::SubwayElevated).collect(),
|
subway_routes,
|
||||||
trolley_routes: all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect(),
|
trolley_routes,
|
||||||
bus_routes: all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect(),
|
bus_routes,
|
||||||
}
|
},
|
||||||
|
load_time_ms: Some(start_time.elapsed().as_millis())
|
||||||
}.render().unwrap())
|
}.render().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +65,7 @@ pub struct RouteResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<RouteResponse> {
|
async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<RouteResponse> {
|
||||||
|
|
||||||
let route = get_route_by_id(route_id.clone(), state.clone()).await?;
|
let route = get_route_by_id(route_id.clone(), state.clone()).await?;
|
||||||
let directions = database::get_direction_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
let directions = database::get_direction_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
||||||
let mut trips = database::get_schedule_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
let mut trips = database::get_schedule_by_route_id(route_id.clone(), &mut state.database.begin().await?).await?;
|
||||||
|
|
@ -73,6 +81,8 @@ async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyho
|
||||||
|
|
||||||
#[get("/route/{route_id}")]
|
#[get("/route/{route_id}")]
|
||||||
async fn get_route(state: Data<Arc<AppState>>, info: web::Query<MyQueryParams>, path: web::Path<String>) -> impl Responder {
|
async fn get_route(state: Data<Arc<AppState>>, info: web::Query<MyQueryParams>, path: web::Path<String>) -> impl Responder {
|
||||||
|
|
||||||
|
let start_time = Instant::now();
|
||||||
let mut filters: Option<Vec<i64>> = None;
|
let mut filters: Option<Vec<i64>> = None;
|
||||||
if let Some (stops_v) = info.stops.clone() {
|
if let Some (stops_v) = info.stops.clone() {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
@ -84,17 +94,17 @@ async fn get_route(state: Data<Arc<AppState>>, info: web::Query<MyQueryParams>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let route_id = path.into_inner();
|
let route_id = path.into_inner();
|
||||||
let route_info_r = get_route_info(route_id, state).await;
|
let route_info_r = get_route_info(route_id.clone(), state).await;
|
||||||
if let Ok(route_info) = route_info_r {
|
if let Ok(route_info) = route_info_r {
|
||||||
HttpResponse::Ok().body(crate::templates::ContentTemplate {
|
HttpResponse::Ok().body(crate::templates::ContentTemplate {
|
||||||
page_title: None,
|
page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())),
|
||||||
page_desc: None,
|
page_desc: Some(format!("Schedule information for {}", route_id.clone())),
|
||||||
content: crate::templates::RouteTemplate {
|
content: crate::templates::RouteTemplate {
|
||||||
route: route_info.route,
|
route: route_info.route,
|
||||||
directions: route_info.directions.clone(),
|
|
||||||
timetables: crate::templates::build_timetables(route_info.directions.as_slice(), route_info.schedule.as_slice()),
|
timetables: crate::templates::build_timetables(route_info.directions.as_slice(), route_info.schedule.as_slice()),
|
||||||
filter_stops: filters.clone()
|
filter_stops: filters.clone()
|
||||||
}
|
},
|
||||||
|
load_time_ms: Some(start_time.elapsed().as_millis())
|
||||||
}.render().unwrap())
|
}.render().unwrap())
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::InternalServerError().body("Error")
|
HttpResponse::InternalServerError().body("Error")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use libseptastic::{direction::CardinalDirection, route::RouteType};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{Postgres, Transaction};
|
use sqlx::{Postgres, Transaction};
|
||||||
use libseptastic::{stop_schedule::{Trip, TripTracking, StopSchedule}};
|
use libseptastic::{stop_schedule::{Trip, TripTracking, StopSchedule}};
|
||||||
|
|
||||||
|
|
@ -177,142 +175,3 @@ pub async fn get_schedule_by_route_id(
|
||||||
|
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize,Clone)]
|
|
||||||
pub struct NTALive {
|
|
||||||
delay: i64,
|
|
||||||
cancelled: bool,
|
|
||||||
next_stop: Option<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize)]
|
|
||||||
pub struct LiveData {
|
|
||||||
route_id: String,
|
|
||||||
service_id: String,
|
|
||||||
trip_id: String,
|
|
||||||
trip_headsign: String,
|
|
||||||
direction_id: i64,
|
|
||||||
block_id: String,
|
|
||||||
start_time: String,
|
|
||||||
end_time: String,
|
|
||||||
delay: i64,
|
|
||||||
status: String,
|
|
||||||
lat: Option<String>,
|
|
||||||
lon: Option<String>,
|
|
||||||
heading: Option<String>,
|
|
||||||
next_stop_id: Option<i64>,
|
|
||||||
next_stop_name: Option<String>,
|
|
||||||
next_stop_sequence: Option<i64>,
|
|
||||||
seat_availability: String,
|
|
||||||
vehicle_id: String,
|
|
||||||
timestamp: i64
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize)]
|
|
||||||
pub struct NTAEntry {
|
|
||||||
route_id: String,
|
|
||||||
route_type: RouteType,
|
|
||||||
route_name: String,
|
|
||||||
color_hex: String,
|
|
||||||
trip_id: String,
|
|
||||||
arrival_time: i64,
|
|
||||||
direction: CardinalDirection,
|
|
||||||
direction_destination: String,
|
|
||||||
live: Option<NTALive>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize)]
|
|
||||||
pub struct NTAResult {
|
|
||||||
station_name: String,
|
|
||||||
arrivals: Vec<NTAEntry>
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_nta_by_stop_id(
|
|
||||||
ids: Vec<i64>,
|
|
||||||
start_time: chrono::DateTime<chrono::Utc>,
|
|
||||||
end_time: chrono::DateTime<chrono::Utc>,
|
|
||||||
transaction: &mut Transaction<'_, Postgres>,
|
|
||||||
) -> ::anyhow::Result<NTAResult> {
|
|
||||||
let local_start = start_time.with_timezone(&chrono_tz::America::New_York);
|
|
||||||
let local_end = end_time.with_timezone(&chrono_tz::America::New_York);
|
|
||||||
let local_midnight = chrono::Utc::now().with_timezone(&chrono_tz::America::New_York).date().and_hms(0,0,0);
|
|
||||||
let start_secs = local_start.signed_duration_since(local_midnight).num_seconds();
|
|
||||||
let end_secs = local_end.signed_duration_since(local_midnight).num_seconds();
|
|
||||||
|
|
||||||
let schedule_day = chrono::Utc::now().with_timezone(&chrono_tz::America::New_York);
|
|
||||||
let schedule_day_str = schedule_day.format("%Y%m%d").to_string();
|
|
||||||
|
|
||||||
let name_row = sqlx::query!("SELECT name FROM septa_stops WHERE id = $1", ids[0]).fetch_one(&mut **transaction).await?;
|
|
||||||
|
|
||||||
let stop_name = name_row.name;
|
|
||||||
|
|
||||||
let rows: Vec<(String, RouteType, String, String, i64, CardinalDirection, String, String,)> = sqlx::query_as(
|
|
||||||
r#"SELECT
|
|
||||||
septa_stop_schedules.route_id,
|
|
||||||
route_type as "route_type: libseptastic::route::RouteType",
|
|
||||||
septa_routes.color_hex,
|
|
||||||
trip_id,
|
|
||||||
arrival_time,
|
|
||||||
septa_directions.direction as "direction: libseptastic::direction::CardinalDirection",
|
|
||||||
septa_directions.direction_destination,
|
|
||||||
septa_routes.name
|
|
||||||
FROM
|
|
||||||
septa_stop_schedules
|
|
||||||
INNER JOIN septa_directions
|
|
||||||
ON
|
|
||||||
septa_directions.direction_id = septa_stop_schedules.direction_id
|
|
||||||
AND
|
|
||||||
septa_directions.route_id = septa_stop_schedules.route_id
|
|
||||||
INNER JOIN septa_stops
|
|
||||||
ON septa_stops.id = septa_stop_schedules.stop_id
|
|
||||||
INNER JOIN septa_routes
|
|
||||||
ON septa_routes.id = septa_stop_schedules.route_id
|
|
||||||
WHERE
|
|
||||||
(septa_stops.id = $1 OR septa_stops.id = $2)
|
|
||||||
AND
|
|
||||||
service_id IN (SELECT service_id FROM septa_schedule_days WHERE date = $5)
|
|
||||||
AND
|
|
||||||
septa_stop_schedules.arrival_time > $3
|
|
||||||
AND
|
|
||||||
septa_stop_schedules.arrival_time < $4
|
|
||||||
ORDER BY arrival_time
|
|
||||||
;"#)
|
|
||||||
.bind(&ids[0])
|
|
||||||
.bind(&ids.get(1).unwrap_or(&0))
|
|
||||||
.bind(&start_secs)
|
|
||||||
.bind(&end_secs)
|
|
||||||
.bind(&schedule_day_str)
|
|
||||||
.fetch_all(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut ntas: Vec<NTAEntry> = Vec::new();
|
|
||||||
let mut live_map: HashMap<String, NTALive> = HashMap::new();
|
|
||||||
|
|
||||||
let lives: Vec<LiveData> = reqwest::get("https://www3.septa.org/api/v2/trips/?route_id=AIR,CHW,LAN,NOR,TRE,WIL,WAR,MED,PAO,FOX,WTR,CYN").await?.json().await?;
|
|
||||||
|
|
||||||
for live in lives {
|
|
||||||
live_map.insert(live.route_id, NTALive { delay: live.delay, cancelled: live.status == "CANCELLED", next_stop: live.next_stop_name });
|
|
||||||
}
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
ntas.push(NTAEntry {
|
|
||||||
route_id: row.0.clone(),
|
|
||||||
route_type: row.1,
|
|
||||||
color_hex: row.2,
|
|
||||||
trip_id: row.3,
|
|
||||||
arrival_time: row.4,
|
|
||||||
direction: row.5,
|
|
||||||
direction_destination: row.6,
|
|
||||||
route_name: row.7,
|
|
||||||
live: match live_map.get(&row.0) {
|
|
||||||
Some(x) => Some(x.clone()),
|
|
||||||
None => None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(NTAResult{
|
|
||||||
station_name: stop_name,
|
|
||||||
arrivals: ntas
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use actix_web::{get, web::{self, Data}, App, HttpResponse, HttpServer, Responder};
|
use actix_web::{get, web::Data, App, HttpResponse, HttpServer, Responder};
|
||||||
use chrono::TimeDelta;
|
|
||||||
use database::get_nta_by_stop_id;
|
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use log::*;
|
use log::*;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
|
@ -23,22 +21,11 @@ async fn get_index() -> impl Responder {
|
||||||
HttpResponse::Ok().body(templates::ContentTemplate {
|
HttpResponse::Ok().body(templates::ContentTemplate {
|
||||||
page_title: None,
|
page_title: None,
|
||||||
page_desc: None,
|
page_desc: None,
|
||||||
content: templates::IndexTemplate {}
|
content: templates::IndexTemplate {},
|
||||||
|
load_time_ms: None
|
||||||
}.render().unwrap())
|
}.render().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/stop/{stop_id}/nta")]
|
|
||||||
async fn api_get_nta(state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
|
||||||
let route_id = path.into_inner().split(',') .map(|s| s.parse::<i64>())
|
|
||||||
.collect::<Result<Vec<i64>, _>>().unwrap();
|
|
||||||
let route_r = get_nta_by_stop_id(route_id, chrono::Utc::now(), chrono::Utc::now() + TimeDelta::minutes(30), &mut state.database.begin().await.unwrap()).await;
|
|
||||||
if let Ok(route) = route_r {
|
|
||||||
HttpResponse::Ok().json(route)
|
|
||||||
} else {
|
|
||||||
HttpResponse::InternalServerError().body(format!("Error {:?}", route_r.err()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> ::anyhow::Result<()> {
|
async fn main() -> ::anyhow::Result<()> {
|
||||||
env_logger::init_from_env(Env::default().default_filter_or("septastic_api=info"));
|
env_logger::init_from_env(Env::default().default_filter_or("septastic_api=info"));
|
||||||
|
|
@ -47,8 +34,6 @@ async fn main() -> ::anyhow::Result<()> {
|
||||||
let version: &str = option_env!("CARGO_PKG_VERSION").expect("Expected package version");
|
let version: &str = option_env!("CARGO_PKG_VERSION").expect("Expected package version");
|
||||||
info!("Starting SEPTASTIC Server v{} (commit: {})", version, "NONE");
|
info!("Starting SEPTASTIC Server v{} (commit: {})", version, "NONE");
|
||||||
|
|
||||||
info!("Connecting to postgres database");
|
|
||||||
|
|
||||||
let connection_string =
|
let connection_string =
|
||||||
std::env::var("DB_CONNSTR").expect("Expected database connection string");
|
std::env::var("DB_CONNSTR").expect("Expected database connection string");
|
||||||
|
|
||||||
|
|
@ -65,12 +50,10 @@ async fn main() -> ::anyhow::Result<()> {
|
||||||
trip_tracking_service: tt_service
|
trip_tracking_service: tt_service
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(actix_cors::Cors::permissive())
|
.wrap(actix_cors::Cors::permissive())
|
||||||
.app_data(Data::new(state.clone()))
|
.app_data(Data::new(state.clone()))
|
||||||
.service(api_get_nta)
|
|
||||||
.service(controllers::route::api_get_route)
|
.service(controllers::route::api_get_route)
|
||||||
.service(controllers::route::api_get_schedule)
|
.service(controllers::route::api_get_schedule)
|
||||||
.service(controllers::route::get_route)
|
.service(controllers::route::get_route)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use log::{error, info};
|
use log::error;
|
||||||
use serde::{Serialize, Deserialize, Deserializer};
|
use serde::{Serialize, Deserialize, Deserializer};
|
||||||
use libseptastic::stop_schedule::{LiveTrip, TripTracking};
|
use libseptastic::stop_schedule::{LiveTrip, TripTracking};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ pub struct ContentTemplate<T: askama::Template> {
|
||||||
pub content: T,
|
pub content: T,
|
||||||
pub page_title: Option<String>,
|
pub page_title: Option<String>,
|
||||||
pub page_desc: Option<String>,
|
pub page_desc: Option<String>,
|
||||||
|
pub load_time_ms: Option<u128>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
#[template(path = "route.html")]
|
#[template(path = "route.html")]
|
||||||
pub struct RouteTemplate {
|
pub struct RouteTemplate {
|
||||||
pub route: libseptastic::route::Route,
|
pub route: libseptastic::route::Route,
|
||||||
pub directions: Vec<libseptastic::direction::Direction>,
|
|
||||||
pub timetables: Vec<TimetableDirection>,
|
pub timetables: Vec<TimetableDirection>,
|
||||||
pub filter_stops: Option<Vec<i64>>
|
pub filter_stops: Option<Vec<i64>>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@
|
||||||
<p style="margin-bottom: 0px;margin-top: 0px;">
|
<p style="margin-bottom: 0px;margin-top: 0px;">
|
||||||
<small>Copyright © <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2025</small>
|
<small>Copyright © <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2025</small>
|
||||||
</p>
|
</p>
|
||||||
|
{% if let Some(load_time) = load_time_ms %}
|
||||||
|
<p style="marin-top: 5px; color: #555555;"><small><i>Data loaded in {{ load_time }}ms</i><small></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue