Add widescreen setting and universal time

This commit is contained in:
Nicholas Orlowsky 2025-09-25 22:08:59 -04:00
parent 775d6c2899
commit ce912d4b85
No known key found for this signature in database
GPG key ID: A9F3BA4C0AA7A70B
7 changed files with 295 additions and 64 deletions

148
api/Cargo.lock generated
View file

@ -67,7 +67,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"base64", "base64 0.22.1",
"bitflags 2.9.1", "bitflags 2.9.1",
"brotli", "brotli",
"bytes", "bytes",
@ -158,6 +158,23 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "3.0.1" version = "3.0.1"
@ -238,6 +255,41 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -574,6 +626,12 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -753,6 +811,16 @@ dependencies = [
"phf", "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]] [[package]]
name = "clap" name = "clap"
version = "4.5.47" version = "4.5.47"
@ -849,7 +917,14 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [ dependencies = [
"aes-gcm",
"base64 0.20.0",
"hkdf",
"hmac",
"percent-encoding", "percent-encoding",
"rand 0.8.5",
"sha2",
"subtle",
"time", "time",
"version_check", "version_check",
] ]
@ -925,9 +1000,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core 0.6.4",
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
@ -1374,6 +1459,16 @@ dependencies = [
"wasi 0.14.2+wasi-0.2.4", "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]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@ -1628,7 +1723,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [ dependencies = [
"base64", "base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1795,6 +1890,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.13" version = "0.1.13"
@ -2169,6 +2273,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.73" version = "0.10.73"
@ -2356,6 +2466,18 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.11.1" version = "1.11.1"
@ -2528,7 +2650,7 @@ version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [ dependencies = [
"base64", "base64 0.22.1",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel", "futures-channel",
@ -2656,7 +2778,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.9.4", "linux-raw-sys 0.9.4",
"windows-sys 0.59.0", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@ -2757,6 +2879,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-files", "actix-files",
"actix-session",
"actix-web", "actix-web",
"anyhow", "anyhow",
"askama", "askama",
@ -2764,6 +2887,7 @@ dependencies = [
"chrono-tz", "chrono-tz",
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures-util",
"libseptastic", "libseptastic",
"log", "log",
"reqwest", "reqwest",
@ -2974,7 +3098,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [ dependencies = [
"async-io 1.13.0", "async-io 1.13.0",
"async-std", "async-std",
"base64", "base64 0.22.1",
"bytes", "bytes",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
@ -3049,7 +3173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64 0.22.1",
"bitflags 2.9.1", "bitflags 2.9.1",
"byteorder", "byteorder",
"bytes", "bytes",
@ -3091,7 +3215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64", "base64 0.22.1",
"bitflags 2.9.1", "bitflags 2.9.1",
"byteorder", "byteorder",
"crc", "crc",
@ -3554,6 +3678,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View file

@ -20,3 +20,5 @@ chrono-tz = "0.10.4"
actix-cors = "0.7.1" actix-cors = "0.7.1"
reqwest = { version = "0.12.22", features = [ "json", "blocking" ] } reqwest = { version = "0.12.22", features = [ "json", "blocking" ] }
sqlx-cli = "0.8.6" sqlx-cli = "0.8.6"
futures-util = "0.3.31"
actix-session = { version = "0.11.0", features = ["cookie-session"] }

View file

@ -22,10 +22,13 @@ body {
.body { .body {
background-color: #ffffff; background-color: #ffffff;
margin: 10px auto; margin: 10px auto;
max-width: 750px;
width: 95%; width: 95%;
} }
.body-small {
max-width: 750px;
}
a { a {
text-decoration: none; text-decoration: none;
color: #114488; color: #114488;

View file

@ -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 std::{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};
@ -8,27 +9,33 @@ use crate::AppState;
use crate::database; use crate::database;
#[get("/routes")] #[get("/routes")]
async fn get_routes_html(state: Data<Arc<AppState>>) -> impl Responder { async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
let start_time = Instant::now(); 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 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 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 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 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 { Ok(crate::templates::ContentTemplate {
page_title: Some(String::from("SEPTASTIC | Routes")), page_title: Some(String::from("SEPTASTIC | Routes")),
page_desc: Some(String::from("All SEPTA routes.")), page_desc: Some(String::from("All SEPTA routes.")),
content: crate::templates::RoutesTemplate { widescreen: false,
rr_routes, content: crate::templates::RoutesTemplate {
subway_routes, rr_routes,
trolley_routes, subway_routes,
bus_routes, trolley_routes,
}, bus_routes,
load_time_ms: Some(start_time.elapsed().as_millis()) },
}.render().unwrap()) load_time_ms: Some(start_time.elapsed().as_millis())
})
}
}).await
} }
#[get("/routes.json")] #[get("/routes.json")]
@ -37,7 +44,7 @@ async fn get_routes_json(state: Data<Arc<AppState>>) -> impl Responder {
HttpResponse::Ok().json(all_routes) HttpResponse::Ok().json(all_routes)
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct RouteQueryParams { pub struct RouteQueryParams {
#[serde(default)] // Optional: handle missing parameters with a default value #[serde(default)] // Optional: handle missing parameters with a default value
stops: Option<String>, stops: Option<String>,
@ -67,39 +74,43 @@ 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<RouteQueryParams>, path: web::Path<String>) -> impl Responder { async fn get_route(state: Data<Arc<AppState>>, req: HttpRequest, info: web::Query<RouteQueryParams>, path: web::Path<String>) -> impl Responder {
let start_time = Instant::now(); crate::perform_action(req, move || {
let pathx = path.clone();
let infox = info.clone();
let statex = state.clone();
async move {
let mut filters: Option<Vec<i64>> = None;
if let Some (stops_v) = infox.stops.clone() {
let mut items = Vec::new();
let mut filters: Option<Vec<i64>> = None; for sid in stops_v.split(",") {
if let Some (stops_v) = info.stops.clone() { items.push(sid.parse::<i64>().unwrap());
let mut items = Vec::new(); }
filters = Some(items);
for sid in stops_v.split(",") {
items.push(sid.parse::<i64>().unwrap());
} }
filters = Some(items);
}
let route_id = path.into_inner(); let route_id = pathx;
let route_info_r = get_route_info(route_id.clone(), state).await; let route_info_r = get_route_info(route_id.clone(), statex.clone()).await;
let load_time_ms = Some(start_time.elapsed().as_millis());
if let Ok(route_info) = route_info_r { if let Ok(route_info) = route_info_r {
let timetables = crate::templates::build_timetables(route_info.directions, route_info.schedule); let timetables = crate::templates::build_timetables(route_info.directions, route_info.schedule);
HttpResponse::Ok().body(crate::templates::ContentTemplate { Ok(crate::templates::ContentTemplate {
page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())), widescreen: false,
page_desc: Some(format!("Schedule information for {}", route_id.clone())), page_title: Some(format!("SEPTASTIC | Schedules for {}", route_id.clone())),
content: crate::templates::RouteTemplate { page_desc: Some(format!("Schedule information for {}", route_id.clone())),
route: route_info.route, content: crate::templates::RouteTemplate {
timetables, route: route_info.route,
filter_stops: filters.clone() timetables,
}, filter_stops: filters.clone()
load_time_ms },
}.render().unwrap()) load_time_ms: None
} else { })
HttpResponse::InternalServerError().body("Error") } else {
} Err(anyhow!("test"))
}
}}).await
} }
#[get("/route/{route_id}.json")] #[get("/route/{route_id}.json")]

View file

@ -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 env_logger::Env;
use log::*; use log::*;
use dotenv::dotenv; use dotenv::dotenv;
use serde::Deserialize;
use services::trip_tracking::{self}; use services::trip_tracking::{self};
use std::sync::Arc; use templates::ContentTemplate;
use std::{sync::Arc, time::Instant};
use askama::Template; use askama::Template;
mod database; mod database;
mod services; mod services;
mod controllers; mod controllers;
mod templates; mod templates;
mod middleware;
pub struct AppState { pub struct AppState {
database: ::sqlx::postgres::PgPool, database: ::sqlx::postgres::PgPool,
trip_tracking_service: services::trip_tracking::TripTrackingService trip_tracking_service: services::trip_tracking::TripTrackingService
} }
#[derive(Deserialize)]
struct LocalStateQuery {
pub widescreen: Option<bool>
}
pub async fn perform_action<F, Fut, T >(req: HttpRequest, func: F) -> impl Responder
where T: Template,
F: Fn() -> Fut,
Fut: Future<Output = anyhow::Result<ContentTemplate<T>>> + '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::<LocalStateQuery>::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("/")] #[get("/")]
async fn get_index() -> impl Responder { async fn get_index(req: HttpRequest) -> impl Responder {
HttpResponse::Ok().body(templates::ContentTemplate { perform_action(req, move || async {
Ok(templates::ContentTemplate {
page_title: None, page_title: None,
page_desc: None, page_desc: None,
content: templates::IndexTemplate {}, content: templates::IndexTemplate {},
load_time_ms: None load_time_ms: None,
}.render().unwrap()) widescreen: false
})
}).await
} }
#[actix_web::main] #[actix_web::main]
@ -53,6 +99,7 @@ async fn main() -> ::anyhow::Result<()> {
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(actix_cors::Cors::permissive()) .wrap(actix_cors::Cors::permissive())
.wrap(actix_web::middleware::from_fn(middleware::local_state::local_state))
.app_data(Data::new(state.clone())) .app_data(Data::new(state.clone()))
.service(controllers::route::api_get_route) .service(controllers::route::api_get_route)
.service(controllers::route::api_get_schedule) .service(controllers::route::api_get_schedule)

View file

@ -9,7 +9,8 @@ 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> pub load_time_ms: Option<u128>,
pub widescreen: bool
} }
#[derive(askama::Template)] #[derive(askama::Template)]
@ -127,6 +128,20 @@ pub fn build_timetables(
} }
mod filters { mod filters {
pub fn format_load_time(
nanos: &u128,
_: &dyn askama::Values,
) -> askama::Result<String> {
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( pub fn format_time(
seconds_since_midnight: &i64, seconds_since_midnight: &i64,
_: &dyn askama::Values, _: &dyn askama::Values,

View file

@ -18,6 +18,13 @@
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico"> <link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<noscript>
<style>
.js-only {
display: none;
}
</style>
</noscript>
<style> <style>
.silverliner-svg { .silverliner-svg {
display: block; display: block;
@ -26,7 +33,11 @@
} }
</style> </style>
<body> <body>
{% if widescreen %}
<div class="body"> <div class="body">
{% else %}
<div class="body body-small">
{% endif %}
<div style="background-color: #ff0000; color: #ffffff; font-size: .7em; padding: 5px; margin-bottom: 10px; margin-top: 10px;"> <div style="background-color: #ff0000; color: #ffffff; font-size: .7em; padding: 5px; margin-bottom: 10px; margin-top: 10px;">
This website is not run by SEPTA. Data may be inaccurate. This website is not run by SEPTA. Data may be inaccurate.
</div> </div>
@ -54,10 +65,18 @@
<small>Copyright &#169; <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2025</small> <small>Copyright &#169; <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2025</small>
</p> </p>
{% if let Some(load_time) = load_time_ms %} {% 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> <p style="marin-top: 5px; color: #555555;"><small><i>Data loaded in {{ *load_time | format_load_time }}</i></small></p>
{% endif %}
</div>
<div>
{% if widescreen %}
<a href="?widescreen=false"><small>[ disable widescreen ]</small></a>
{% else %}
<a href="?widescreen=true"><small>[ enable widescreen ]</small></a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<noscript><p style="margin-top: 10px;"><small>[!] You do not have JavaScript enabled. Some features will be missing.</small></p></noscript>
</footer> </footer>
</div> </div>