This commit is contained in:
2025-05-23 09:20:08 -04:00
parent ed98140d22
commit 6ad2afe6dd
16 changed files with 5180 additions and 177 deletions

1
.env
View File

@@ -57,6 +57,7 @@ ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=changeme ADMIN_PASSWORD=changeme
TEMPLATE_DIR=../templates TEMPLATE_DIR=../templates
METAR_INTERVAL=300
MAILPIT_WEB_PORT=8025 MAILPIT_WEB_PORT=8025
MAILPIT_SMTP_PORT=1025 MAILPIT_SMTP_PORT=1025

1
.gitignore vendored
View File

@@ -8,7 +8,6 @@
node_modules node_modules
target/ target/
dist/ dist/
Cargo.lock
ssl/ ssl/
.DS_Store .DS_Store

475
adsb/Cargo.lock generated Normal file
View File

@@ -0,0 +1,475 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adsb"
version = "0.1.0"
dependencies = [
"clap",
"ctrlc",
"env_logger",
"log",
"rusb",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cc"
version = "1.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "ctrlc"
version = "3.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
dependencies = [
"nix",
"windows-sys",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "jiff"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libusb1-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rusb"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4"
dependencies = [
"libc",
"libusb1-sys",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

4484
api/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,9 @@ use rand_chacha::ChaCha20Rng;
mod auth; mod auth;
mod email_token; mod email_token;
mod model;
mod routes; mod routes;
mod session; mod session;
mod model;
pub use auth::*; pub use auth::*;
pub use routes::init_routes; pub use routes::init_routes;

View File

@@ -80,53 +80,53 @@ impl Default for AirportQuery {
} }
} }
impl AirportQuery { // impl AirportQuery {
pub fn builder() -> AirportQueryBuilder { // pub fn builder() -> AirportQueryBuilder {
AirportQueryBuilder::new() // AirportQueryBuilder::new()
} // }
} // }
pub struct AirportQueryBuilder { // pub struct AirportQueryBuilder {
inner: AirportQuery, // inner: AirportQuery,
} // }
//
impl AirportQueryBuilder { // impl AirportQueryBuilder {
/// start the builder // /// start the builder
pub fn new() -> Self { // pub fn new() -> Self {
AirportQueryBuilder { // AirportQueryBuilder {
inner: AirportQuery::default(), // inner: AirportQuery::default(),
} // }
} // }
//
pub fn page(mut self, page: u32) -> Self { // pub fn page(mut self, page: u32) -> Self {
self.inner.page = Some(page); // self.inner.page = Some(page);
self // self
} // }
//
pub fn limit(mut self, limit: u32) -> Self { // pub fn limit(mut self, limit: u32) -> Self {
self.inner.limit = Some(limit); // self.inner.limit = Some(limit);
self // self
} // }
//
pub fn icaos<T: Into<String>>(mut self, v: T) -> Self { // pub fn icaos<T: Into<String>>(mut self, v: T) -> Self {
self.inner.icaos = Some(v.into()); // self.inner.icaos = Some(v.into());
self // self
} // }
//
pub fn iatas<T: Into<String>>(mut self, v: T) -> Self { // pub fn iatas<T: Into<String>>(mut self, v: T) -> Self {
self.inner.iatas = Some(v.into()); // self.inner.iatas = Some(v.into());
self // self
} // }
//
pub fn metars(mut self, v: bool) -> Self { // pub fn metars(mut self, v: bool) -> Self {
self.inner.metars = Some(v); // self.inner.metars = Some(v);
self // self
} // }
//
pub fn build(self) -> AirportQuery { // pub fn build(self) -> AirportQuery {
self.inner // self.inner
} // }
} // }
#[derive(Debug, Deserialize, ToSchema)] #[derive(Debug, Deserialize, ToSchema)]
pub struct Bounds { pub struct Bounds {

View File

@@ -83,7 +83,8 @@ impl HttpClient {
if response.status() != 200 { if response.status() != 200 {
return Err(Error::new( return Err(Error::new(
response.status().as_u16(), response.status().as_u16(),
format!("Request returned status {}", response.status()))); format!("Request returned status {}", response.status()),
));
} }
Ok(response) Ok(response)

View File

@@ -35,7 +35,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Arc::new(HttpClient::default()?); let client = Arc::new(HttpClient::default()?);
let scheduler_client = client.clone(); let scheduler_client = client.clone();
scheduler::update_metars(scheduler_client, 600); let interval = env::var("METAR_INTERVAL")
.unwrap_or("300".to_string())
.parse::<u64>()
.unwrap_or(300);
scheduler::update_metars(scheduler_client, interval);
// Initialize admin user // Initialize admin user
let admin_username = env::var("ADMIN_USERNAME"); let admin_username = env::var("ADMIN_USERNAME");

View File

@@ -4,6 +4,8 @@ use crate::http_client::HttpClient;
use crate::metars::MetarCheck; use crate::metars::MetarCheck;
use crate::{db, error::ApiResult}; use crate::{db, error::ApiResult};
use chrono::{DateTime, Datelike, NaiveDate, Utc}; use chrono::{DateTime, Datelike, NaiveDate, Utc};
use flate2::read::GzDecoder;
use reqwest::header::ETAG;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
@@ -11,8 +13,6 @@ use std::fmt::Display;
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
use std::str::FromStr; use std::str::FromStr;
use std::sync::OnceLock; use std::sync::OnceLock;
use flate2::read::GzDecoder;
use reqwest::header::ETAG;
use utoipa::ToSchema; use utoipa::ToSchema;
static TIME_OFFSET: OnceLock<i64> = OnceLock::new(); static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
@@ -921,12 +921,17 @@ impl Metar {
), ),
) )
})?; })?;
let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) { let candidate_date = match candidate_date.and_hms_opt(observation_hour, observation_minute, 0) {
Some(date) => date, Some(date) => date,
None => return Err(Error::new( None => {
500, return Err(Error::new(
format!("Invalid time for time '{}': hour {}, minute {}", 500,
observation_time, observation_hour, observation_minute))) format!(
"Invalid time for time '{}': hour {}, minute {}",
observation_time, observation_hour, observation_minute
),
));
}
}; };
let obs_datetime = if candidate_date > current_time { let obs_datetime = if candidate_date > current_time {
@@ -956,9 +961,11 @@ impl Metar {
Ok(obs_datetime.format("%Y-%m-%dT%H:%M:00Z").to_string()) Ok(obs_datetime.format("%Y-%m-%dT%H:%M:00Z").to_string())
} }
pub async fn get_cached_remote_metars(client: &HttpClient, etag: Option<String>) -> ApiResult<(Vec<Self>, String)> { pub async fn get_cached_remote_metars(
let base_url = env::var("AVIATION_WEATHER_URL") client: &HttpClient,
.expect("AVIATION_WEATHER_URL must be set"); etag: Option<String>,
) -> ApiResult<(Vec<Self>, String)> {
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
let url = format!("{}/data/cache/metars.cache.csv.gz", base_url); let url = format!("{}/data/cache/metars.cache.csv.gz", base_url);
match client.get(&url, etag.clone()).await { match client.get(&url, etag.clone()).await {
@@ -991,8 +998,8 @@ impl Metar {
Some(etag) => Ok((output, etag)), Some(etag) => Ok((output, etag)),
None => match etag { None => match etag {
Some(etag) => Ok((output, etag)), Some(etag) => Ok((output, etag)),
None => Ok((output, String::new())) None => Ok((output, String::new())),
} },
} }
} }
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
@@ -1000,8 +1007,7 @@ impl Metar {
} }
pub async fn get_remote_metars(client: &HttpClient, icaos: &Vec<String>) -> ApiResult<Vec<Self>> { pub async fn get_remote_metars(client: &HttpClient, icaos: &Vec<String>) -> ApiResult<Vec<Self>> {
let base_url = env::var("AVIATION_WEATHER_URL") let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
.expect("AVIATION_WEATHER_URL must be set");
// Query the remote API for the missing METAR data 10 at a time // Query the remote API for the missing METAR data 10 at a time
let icao_chunks = icaos let icao_chunks = icaos
.chunks(10) .chunks(10)
@@ -1014,22 +1020,20 @@ impl Metar {
base_url, icao_chunk base_url, icao_chunk
); );
let mut m = match client.get(&url, None).await { let mut m = match client.get(&url, None).await {
Ok(r) => { Ok(r) => match r.text().await {
match r.text().await { Ok(r) => {
Ok(r) => { let metar_chunk = r
let metar_chunk = r .trim()
.trim() .split("\n")
.split("\n") .filter(|m| !m.trim().is_empty())
.filter(|m| !m.trim().is_empty()) .collect();
.collect(); match Self::parse_multiple(&metar_chunk) {
match Self::parse_multiple(&metar_chunk) { Ok(m) => m,
Ok(m) => m, Err(err) => return Err(err),
Err(err) => return Err(err),
}
} }
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
} }
} Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
},
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
metars.append(&mut m); metars.append(&mut m);

View File

@@ -1,4 +1,5 @@
use crate::AppState; use crate::AppState;
use crate::account::Auth;
use crate::metars::Metar; use crate::metars::Metar;
use actix_web::{HttpRequest, HttpResponse, get, put, web}; use actix_web::{HttpRequest, HttpResponse, get, put, web};
use log::error; use log::error;
@@ -6,7 +7,6 @@ use serde::Deserialize;
use utoipa::{IntoParams, ToSchema}; use utoipa::{IntoParams, ToSchema};
use utoipa_actix_web::scope; use utoipa_actix_web::scope;
use utoipa_actix_web::service_config::ServiceConfig; use utoipa_actix_web::service_config::ServiceConfig;
use crate::account::Auth;
#[derive(Debug, Deserialize, ToSchema, IntoParams)] #[derive(Debug, Deserialize, ToSchema, IntoParams)]
#[into_params(parameter_in = Query)] #[into_params(parameter_in = Query)]
@@ -85,37 +85,10 @@ async fn refresh_metars(data: web::Data<AppState>, req: HttpRequest, _auth: Auth
HttpResponse::Ok().json(metars) HttpResponse::Ok().json(metars)
} }
#[utoipa::path(
tag = "metar",
responses(
(status = 200, description = "Successful Response", body = Metar),
(status = 404, description = "Not Found"),
),
)]
#[get("/{icao}")]
async fn find(icao: web::Path<String>) -> HttpResponse {
let icao = vec![icao.to_uppercase()];
let metar = match Metar::get_all_distinct(&icao).await {
Ok(metars) => {
if metars.len() == 1 {
metars[0].clone()
} else {
return HttpResponse::NotFound().finish()
}
},
Err(err) => {
error!("{}", err);
return err.to_http_response();
}
};
HttpResponse::Ok().json(metar)
}
pub fn init_routes(config: &mut ServiceConfig) { pub fn init_routes(config: &mut ServiceConfig) {
config.service( config.service(
scope::scope("/metars") scope::scope("/metars")
.service(find_all) .service(find_all)
.service(refresh_metars) .service(refresh_metars),
.service(find)
); );
} }

View File

@@ -22,7 +22,7 @@ pub fn update_metars(client: Arc<HttpClient>, seconds: u64) {
// Run the update // Run the update
match Metar::update_metars(&client, etag.clone()).await { match Metar::update_metars(&client, etag.clone()).await {
Ok(new_etag) => etag = Some(new_etag), Ok(new_etag) => etag = Some(new_etag),
Err(err) => log::error!("METAR update failed: {}", err) Err(err) => log::error!("METAR update failed: {}", err),
} }
let elapsed = start_monotonic.elapsed(); let elapsed = start_monotonic.elapsed();

View File

@@ -21,7 +21,10 @@ pub struct SystemInfo {
async fn info() -> HttpResponse { async fn info() -> HttpResponse {
let healthy = true; let healthy = true;
let version = env!("CARGO_PKG_VERSION"); let version = env!("CARGO_PKG_VERSION");
let info = SystemInfo { version: version.to_string(), healthy }; let info = SystemInfo {
version: version.to_string(),
healthy,
};
HttpResponse::Ok().json(info) HttpResponse::Ok().json(info)
} }

183
ui/package-lock.json generated
View File

@@ -65,15 +65,15 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.26.2", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.25.9", "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.0.0" "picocolors": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -197,9 +197,9 @@
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.25.9", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -207,9 +207,9 @@
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -227,27 +227,27 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.26.9", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.26.9", "@babel/template": "^7.27.1",
"@babel/types": "^7.26.9" "@babel/types": "^7.27.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.26.9", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.26.9" "@babel/types": "^7.27.1"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -289,27 +289,24 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.9", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"license": "MIT", "license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.26.9", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.26.2", "@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.26.9", "@babel/parser": "^7.27.2",
"@babel/types": "^7.26.9" "@babel/types": "^7.27.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -345,14 +342,14 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.26.9", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.25.9" "@babel/helper-validator-identifier": "^7.27.1"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1571,12 +1568,6 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/d3": { "node_modules/@types/d3": {
"version": "7.4.3", "version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
@@ -4405,15 +4396,13 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.5.0", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
"integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==", "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1", "cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0"
"turbo-stream": "2.4.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@@ -4493,12 +4482,6 @@
"pify": "^2.3.0" "pify": "^2.3.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -4845,6 +4828,51 @@
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4877,12 +4905,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -5089,15 +5111,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.1", "version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rollup": "^4.30.1" "rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -5160,6 +5185,34 @@
} }
} }
}, },
"node_modules/vite/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -107,6 +107,7 @@ function App() {
]} ]}
scrollWheelZoom={true} scrollWheelZoom={true}
zoomControl={false} zoomControl={false}
markerZoomAnimation={false}
> >
<AirportDrawer airport={airport} setAirport={setAirport} /> <AirportDrawer airport={airport} setAirport={setAirport} />
<LayersControl> <LayersControl>

View File

@@ -22,8 +22,8 @@ export default function AirportLayer({
const debouncedLoad = useRef( const debouncedLoad = useRef(
debounce(async (map: any) => { debounce(async (map: any) => {
const bounds = map.getBounds(); const bounds = map.getBounds();
const ne = bounds.getNorthEast() const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest() const sw = bounds.getSouthWest();
lastBoundsRef.current = { ne, sw }; lastBoundsRef.current = { ne, sw };
try { try {

View File

@@ -202,7 +202,12 @@ export function HeaderModal({ type, toggle, login, register }: HeaderModalProps)
} }
})} })}
> >
<TextInput label='Username' placeholder='Your username' required {...loginForm.getInputProps('username')} /> <TextInput
label='Username'
placeholder='Your username'
required
{...loginForm.getInputProps('username')}
/>
<PasswordInput <PasswordInput
label='Password' label='Password'
placeholder='Your password' placeholder='Your password'