diff --git a/src/application/routes/account.rs b/src/application/routes/account.rs index ce3e1c1..a4abb57 100644 --- a/src/application/routes/account.rs +++ b/src/application/routes/account.rs @@ -73,6 +73,7 @@ fn format_date(dt: DateTime) -> String { struct AccountTemplate { nav_active: &'static str, is_authenticated: bool, + version_info: &'static crate::VersionInfo, ai_usage: Option, passkeys: Vec, tokens: Vec, @@ -137,6 +138,7 @@ pub(crate) async fn account_page( let template = AccountTemplate { nav_active: "account", is_authenticated: true, + version_info: &crate::VERSION_INFO, ai_usage, passkeys, tokens, diff --git a/src/application/routes/add.rs b/src/application/routes/add.rs index fb4e765..1e80758 100644 --- a/src/application/routes/add.rs +++ b/src/application/routes/add.rs @@ -46,6 +46,7 @@ pub(crate) async fn add_page( let template = AddTemplate { nav_active: "data", is_authenticated, + version_info: &crate::VERSION_INFO, active_type: query.entity_type, roaster_options, roast_options, diff --git a/src/application/routes/auth.rs b/src/application/routes/auth.rs index f8bb4f0..3eec5d4 100644 --- a/src/application/routes/auth.rs +++ b/src/application/routes/auth.rs @@ -21,6 +21,7 @@ pub struct LoginQuery { struct LoginTemplate { nav_active: &'static str, is_authenticated: bool, + version_info: &'static crate::VersionInfo, } #[tracing::instrument(skip(state, cookies))] @@ -38,6 +39,7 @@ pub(crate) async fn login_page( let template = LoginTemplate { nav_active: "login", is_authenticated: false, + version_info: &crate::VERSION_INFO, }; render_html(template).map(IntoResponse::into_response) diff --git a/src/application/routes/checkin.rs b/src/application/routes/checkin.rs index c0023cd..b741fb7 100644 --- a/src/application/routes/checkin.rs +++ b/src/application/routes/checkin.rs @@ -32,6 +32,7 @@ pub(crate) async fn checkin_page( let template = CheckInTemplate { nav_active: "checkin", is_authenticated: true, + version_info: &crate::VERSION_INFO, roast_options, cafe_options, }; diff --git a/src/application/routes/data.rs b/src/application/routes/data.rs index 7191036..4d93668 100644 --- a/src/application/routes/data.rs +++ b/src/application/routes/data.rs @@ -95,6 +95,7 @@ pub(crate) async fn data_page( let template = DataTemplate { nav_active: "data", is_authenticated, + version_info: &crate::VERSION_INFO, active_type: entity_type, tabs, content, diff --git a/src/application/routes/home.rs b/src/application/routes/home.rs index df0f851..cb5904b 100644 --- a/src/application/routes/home.rs +++ b/src/application/routes/home.rs @@ -31,6 +31,7 @@ pub(crate) async fn home_page( let template = HomeTemplate { nav_active: "home", is_authenticated, + version_info: &crate::VERSION_INFO, recent_brews: content.recent_brews, open_bags: content.open_bags, recent_events: content.recent_events, diff --git a/src/application/routes/mod.rs b/src/application/routes/mod.rs index 9b56c81..5134c83 100644 --- a/src/application/routes/mod.rs +++ b/src/application/routes/mod.rs @@ -130,6 +130,11 @@ pub fn app_router(state: AppState) -> axum::Router { .route("/timeline", get(timeline::timeline_page)) .route("/styles.css", get(styles)) .route("/webauthn.js", get(webauthn_js)) + .route("/components/photo-capture.js", get(photo_capture_js)) + .route( + "/components/searchable-select.js", + get(searchable_select_js), + ) .route("/favicon.ico", get(favicon)) .nest("/api/v1", api_routes) .nest("/api/v1/webauthn", webauthn_routes) @@ -155,6 +160,20 @@ async fn webauthn_js() -> impl IntoResponse { ) } +async fn photo_capture_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript; charset=utf-8")], + include_str!("../../../templates/components/photo-capture.js"), + ) +} + +async fn searchable_select_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript; charset=utf-8")], + include_str!("../../../templates/components/searchable-select.js"), + ) +} + async fn favicon() -> impl IntoResponse { ( [("content-type", "image/x-icon")], diff --git a/src/application/routes/timeline.rs b/src/application/routes/timeline.rs index 5a8985b..5a2fb4e 100644 --- a/src/application/routes/timeline.rs +++ b/src/application/routes/timeline.rs @@ -93,6 +93,7 @@ pub(crate) async fn timeline_page( let template = TimelineTemplate { nav_active: "timeline", is_authenticated, + version_info: &crate::VERSION_INFO, events: data.events, navigator: data.navigator, months: data.months, diff --git a/src/application/routes/webauthn.rs b/src/application/routes/webauthn.rs index 5763213..2339c9a 100644 --- a/src/application/routes/webauthn.rs +++ b/src/application/routes/webauthn.rs @@ -29,6 +29,7 @@ const SESSION_COOKIE_NAME: &str = "brewlog_session"; struct RegisterTemplate { nav_active: &'static str, is_authenticated: bool, + version_info: &'static crate::VersionInfo, token: String, } @@ -37,6 +38,7 @@ struct RegisterTemplate { struct CliCallbackTemplate { nav_active: &'static str, is_authenticated: bool, + version_info: &'static crate::VersionInfo, token: Option, error: Option, } @@ -116,6 +118,7 @@ pub(crate) async fn register_page( let template = RegisterTemplate { nav_active: "", is_authenticated: false, + version_info: &crate::VERSION_INFO, token, }; @@ -505,6 +508,7 @@ pub(crate) async fn cli_callback_page() -> Result { let template = CliCallbackTemplate { nav_active: "", is_authenticated: false, + version_info: &crate::VERSION_INFO, token: None, error: None, }; diff --git a/src/lib.rs b/src/lib.rs index de226b8..6ac662d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,13 @@ pub mod application; pub mod domain; pub mod infrastructure; pub mod presentation; + +pub struct VersionInfo { + pub version: &'static str, + pub commit: &'static str, +} + +pub const VERSION_INFO: VersionInfo = VersionInfo { + version: env!("CARGO_PKG_VERSION"), + commit: env!("GIT_HASH"), +}; diff --git a/src/presentation/web/templates.rs b/src/presentation/web/templates.rs index 8ed3bec..d0464b8 100644 --- a/src/presentation/web/templates.rs +++ b/src/presentation/web/templates.rs @@ -35,6 +35,7 @@ pub struct RoastListTemplate { pub struct TimelineTemplate { pub nav_active: &'static str, pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, pub events: Paginated, pub navigator: ListNavigator, @@ -101,6 +102,7 @@ pub struct CupListTemplate { pub struct HomeTemplate { pub nav_active: &'static str, pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, pub recent_brews: Vec, pub open_bags: Vec, @@ -113,6 +115,7 @@ pub struct HomeTemplate { pub struct CheckInTemplate { pub nav_active: &'static str, pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, pub roast_options: Vec, pub cafe_options: Vec, @@ -129,6 +132,7 @@ pub struct NearbyCafesFragment { pub struct DataTemplate { pub nav_active: &'static str, pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, pub active_type: String, pub tabs: Vec, pub content: String, @@ -145,6 +149,7 @@ pub struct DataTab { pub struct AddTemplate { pub nav_active: &'static str, pub is_authenticated: bool, + pub version_info: &'static crate::VersionInfo, pub active_type: String, pub roaster_options: Vec, pub roast_options: Vec, diff --git a/src/presentation/web/views/bags.rs b/src/presentation/web/views/bags.rs index bdafcb1..615d87e 100644 --- a/src/presentation/web/views/bags.rs +++ b/src/presentation/web/views/bags.rs @@ -42,16 +42,23 @@ impl BagView { pub struct BagOptionView { pub id: String, pub label: String, + pub roast_name: String, + pub roaster_name: String, + pub remaining: String, } impl From for BagOptionView { fn from(bag: BagWithRoast) -> Self { + let remaining = format!("{:.0}g", bag.bag.remaining); Self { id: bag.bag.id.to_string(), label: format!( - "{} - {} ({:.0}g remaining)", - bag.roaster_name, bag.roast_name, bag.bag.remaining + "{} - {} ({} remaining)", + bag.roaster_name, bag.roast_name, remaining ), + roast_name: bag.roast_name, + roaster_name: bag.roaster_name, + remaining, } } } diff --git a/src/presentation/web/views/brews.rs b/src/presentation/web/views/brews.rs index 0eda295..ccf5a4e 100644 --- a/src/presentation/web/views/brews.rs +++ b/src/presentation/web/views/brews.rs @@ -87,6 +87,7 @@ impl Default for BrewDefaultsView { fn default() -> Self { Self { bag_id: String::new(), + grinder_id: String::new(), brewer_id: String::new(), filter_paper_id: String::new(), @@ -102,6 +103,7 @@ impl From for BrewDefaultsView { fn from(brew: Brew) -> Self { Self { bag_id: brew.bag_id.to_string(), + grinder_id: brew.grinder_id.to_string(), brewer_id: brew.brewer_id.to_string(), filter_paper_id: brew diff --git a/src/presentation/web/views/timeline.rs b/src/presentation/web/views/timeline.rs index a73dc7b..3666246 100644 --- a/src/presentation/web/views/timeline.rs +++ b/src/presentation/web/views/timeline.rs @@ -7,6 +7,20 @@ fn is_blank(value: &str) -> bool { value.is_empty() || value == "\u{2014}" } +/// Rounds "lat,lon" coordinates to 3 decimal places for display. +fn round_coords(coords: &str) -> String { + let parts: Vec<&str> = coords.split(',').collect(); + if parts.len() == 2 + && let (Ok(lat), Ok(lon)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + ) + { + return format!("{lat:.3}, {lon:.3}"); + } + coords.to_string() +} + #[derive(Clone)] pub struct TimelineEventDetailView { pub label: String, @@ -189,13 +203,19 @@ impl TimelineEventView { "position" => { let trimmed = detail.value.trim(); if !trimmed.is_empty() { - let display = trimmed + let coords = trimmed .strip_prefix("https://www.google.com/maps?q=") .unwrap_or(trimmed); + let display = round_coords(coords); + let link = if trimmed.starts_with("http") { + trimmed.to_string() + } else { + format!("https://www.google.com/maps?q={coords}") + }; mapped.push(TimelineEventDetailView { label: detail.label, - value: display.to_string(), - link: Some(trimmed.to_string()), + value: display, + link: Some(link), }); } } diff --git a/templates/account.html b/templates/account.html index 8b017e9..0c951ce 100644 --- a/templates/account.html +++ b/templates/account.html @@ -1,19 +1,19 @@ {% extends "base.html" %} {% import "partials/icons.html" as icons %} -{% block title %}Brewlog · Account{% endblock %} +{% block title %}Brewlog · Admin{% endblock %} {% block head %} {% endblock %} {% block content %}
-

Passkeys

+

Passkeys

{% for passkey in passkeys %} -
+
- {% call icons::key("inline h-4 w-4 mr-1 text-amber-600") %}{{ passkey.name }} + {% call icons::key("inline h-4 w-4 mr-1 text-accent") %}{{ passkey.name }}
{% if passkeys.len() > 1 %} @@ -72,7 +72,7 @@ Cancel
-