phase 4 ready for review
This commit is contained in:
+80
-3
@@ -31,7 +31,9 @@ struct StaticAssets;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LandingsQuery {
|
||||
pub month: Option<String>,
|
||||
pub month: Option<String>, // Kept for backward compatibility or single-month quick lookups
|
||||
pub month_from: Option<String>,
|
||||
pub month_to: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub gear: Option<String>,
|
||||
pub zone: Option<String>,
|
||||
@@ -88,6 +90,8 @@ pub fn build_router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/healthz", get(healthz))
|
||||
.route("/api/species", get(get_species))
|
||||
.route("/api/zones", get(get_zones))
|
||||
.route("/api/gear", get(get_gear))
|
||||
.route("/api/landings", get(get_landings))
|
||||
.route("/api/summary", get(get_summary))
|
||||
.route("/api/export.parquet", get(export_parquet))
|
||||
@@ -139,11 +143,25 @@ async fn get_landings(
|
||||
let mut args: Vec<Box<dyn duckdb::ToSql>> = Vec::new();
|
||||
let mut idx = 1;
|
||||
|
||||
// Single month filter (legacy)
|
||||
if let Some(ref month) = params.month {
|
||||
sql.push_str(&format!(" AND month = ${idx}"));
|
||||
args.push(Box::new(month.clone()));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// Range filter (new)
|
||||
if let Some(ref month_from) = params.month_from {
|
||||
sql.push_str(&format!(" AND month >= ${idx}"));
|
||||
args.push(Box::new(month_from.clone()));
|
||||
idx += 1;
|
||||
}
|
||||
if let Some(ref month_to) = params.month_to {
|
||||
sql.push_str(&format!(" AND month <= ${idx}"));
|
||||
args.push(Box::new(month_to.clone()));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if let Some(ref species) = params.species {
|
||||
sql.push_str(&format!(" AND species_code = ${idx}"));
|
||||
args.push(Box::new(species.clone()));
|
||||
@@ -403,11 +421,26 @@ async fn export_parquet(State(state): State<AppState>) -> ApiResult<Response> {
|
||||
|
||||
async fn static_handler(uri: axum::http::Uri) -> Response {
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
let asset = StaticAssets::get(path).or_else(|| StaticAssets::get("index.html"));
|
||||
|
||||
// Strip "static/" prefix if present
|
||||
let asset_path = if let Some(stripped) = path.strip_prefix("static/") {
|
||||
stripped
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
// If empty path (e.g., visiting "/"), serve index.html directly
|
||||
let asset_path = if asset_path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
asset_path
|
||||
};
|
||||
|
||||
let asset = StaticAssets::get(asset_path).or_else(|| StaticAssets::get("index.html"));
|
||||
|
||||
match asset {
|
||||
Some(file) => {
|
||||
let mime = mime_guess::from_path(path)
|
||||
let mime = mime_guess::from_path(asset_path)
|
||||
.first_or_octet_stream()
|
||||
.as_ref()
|
||||
.to_string();
|
||||
@@ -425,6 +458,50 @@ async fn static_handler(uri: axum::http::Uri) -> Response {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_zones(State(state): State<AppState>) -> ApiResult<Json<Vec<SpeciesDto>>> {
|
||||
let conn = state.conn.clone();
|
||||
let rows = tokio::task::spawn_blocking(move || -> db::Result<Vec<SpeciesDto>> {
|
||||
let conn = conn.blocking_lock();
|
||||
let mut stmt = conn.prepare("SELECT code, label FROM zone ORDER BY code")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(SpeciesDto {
|
||||
code: row.get(0)?,
|
||||
label: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
Ok(rows.collect::<std::result::Result<Vec<_>, duckdb::Error>>()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Task join error: {e}"),
|
||||
})??;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
async fn get_gear(State(state): State<AppState>) -> ApiResult<Json<Vec<SpeciesDto>>> {
|
||||
let conn = state.conn.clone();
|
||||
let rows = tokio::task::spawn_blocking(move || -> db::Result<Vec<SpeciesDto>> {
|
||||
let conn = conn.blocking_lock();
|
||||
let mut stmt = conn.prepare("SELECT code, label FROM gear ORDER BY code")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(SpeciesDto {
|
||||
code: row.get(0)?,
|
||||
label: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
Ok(rows.collect::<std::result::Result<Vec<_>, duckdb::Error>>()?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Task join error: {e}"),
|
||||
})??;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user