refactor: require OpenRouter and Foursquare API keys at startup
- Remove has_ai_extract/has_foursquare conditionals from all templates - Remove boolean fields from template structs and route handlers - Remove has_ai_extract()/has_foursquare() methods from AppState - Change API key fields from Option<String> to String in ServerConfig and AppState - Validate keys in run_server() with clear error messages - Remove runtime key checks from extraction/nearby route handlers - Update README and CLAUDE.md to reflect required configuration
This commit is contained in:
parent
98c8391670
commit
a31c91211c
22 changed files with 91 additions and 127 deletions
|
|
@ -429,7 +429,6 @@ Each extraction-enabled page uses this structure:
|
|||
|
||||
```html
|
||||
<section data-signals:_extracting="false" data-signals:_extract-error="''" data-signals:_submitting="false">
|
||||
{% if has_ai_extract %}
|
||||
<!-- Hidden file input — only JS needed (FileReader API) -->
|
||||
<input type="file" id="{id}-photo" accept="image/*" capture="environment" class="hidden"
|
||||
onchange="if(this.files[0]){const r=new FileReader();r.onload=()=>{
|
||||
|
|
@ -452,7 +451,6 @@ Each extraction-enabled page uses this structure:
|
|||
<div data-show="$_extracting" style="display:none"><!-- spinner --></div>
|
||||
<p data-show="$_extractError" data-text="$_extractError" style="display:none"></p>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main form with data-bind fields -->
|
||||
<form data-on:submit="$_submitting = true; @post(...)">
|
||||
|
|
@ -468,7 +466,7 @@ The only inline JS is the `onchange` handler for FileReader (reading photos as d
|
|||
|
||||
The cafes page uses the [Foursquare Places API](https://docs.foursquare.com/developer/reference/place-search) to search for nearby cafes. The integration lives in `infrastructure/foursquare.rs`.
|
||||
|
||||
**Configuration**: Set `BREWLOG_FOURSQUARE_API_KEY` (a Foursquare service API key). The nearby search feature is only available when this key is configured.
|
||||
**Configuration**: Set `BREWLOG_FOURSQUARE_API_KEY` (a Foursquare service API key).
|
||||
|
||||
**Search modes** via the `SearchLocation` enum:
|
||||
|
||||
|
|
|
|||
25
README.md
25
README.md
|
|
@ -20,13 +20,17 @@ B{rew}log ships as one executable. You decide whether it acts as a server or a c
|
|||
|
||||
### First-time setup
|
||||
|
||||
On first start, you must set an admin username and password via the `BREWLOG_ADMIN_USERNAME` and `BREWLOG_ADMIN_PASSWORD` environment variables:
|
||||
The server requires `BREWLOG_OPENROUTER_API_KEY` and `BREWLOG_FOURSQUARE_API_KEY` to be set. On first start, you must also set an admin username and password:
|
||||
|
||||
```bash
|
||||
BREWLOG_ADMIN_USERNAME="admin" BREWLOG_ADMIN_PASSWORD="your-secure-password" brewlog serve
|
||||
BREWLOG_ADMIN_USERNAME="admin" \
|
||||
BREWLOG_ADMIN_PASSWORD="your-secure-password" \
|
||||
BREWLOG_OPENROUTER_API_KEY="sk-or-..." \
|
||||
BREWLOG_FOURSQUARE_API_KEY="fsq3..." \
|
||||
brewlog serve
|
||||
```
|
||||
|
||||
This creates the admin user in the database. On subsequent starts, the environment variables are not required.
|
||||
This creates the admin user in the database. On subsequent starts, the admin environment variables are not required.
|
||||
|
||||
### Authentication
|
||||
|
||||
|
|
@ -154,27 +158,28 @@ All configuration is via environment variables or CLI flags. A `.env` file in th
|
|||
| `BREWLOG_URL` | Server URL for CLI commands | `http://127.0.0.1:3000` |
|
||||
| `BREWLOG_TOKEN` | API token for authenticated CLI operations | — |
|
||||
|
||||
### Optional Integrations
|
||||
### Integrations
|
||||
|
||||
| Variable | Purpose | Default |
|
||||
|----------|---------|---------|
|
||||
| `BREWLOG_OPENROUTER_API_KEY` | [OpenRouter](https://openrouter.ai/) API key — enables AI extraction | — |
|
||||
| `BREWLOG_OPENROUTER_API_KEY` | [OpenRouter](https://openrouter.ai/) API key for AI extraction | — (required) |
|
||||
| `BREWLOG_OPENROUTER_MODEL` | LLM model for AI extraction | `openrouter/free` |
|
||||
| `BREWLOG_FOURSQUARE_API_KEY` | [Foursquare](https://foursquare.com/) Places API key — enables nearby cafe search | — |
|
||||
| `BREWLOG_FOURSQUARE_API_KEY` | [Foursquare](https://foursquare.com/) Places API key for nearby cafe search | — (required) |
|
||||
|
||||
## Optional Features
|
||||
## Integrations
|
||||
|
||||
### AI Extraction
|
||||
|
||||
When `BREWLOG_OPENROUTER_API_KEY` is configured, the web UI gains the ability to extract roaster and roast details from photos or text descriptions using an LLM. This powers:
|
||||
The web UI uses an LLM via [OpenRouter](https://openrouter.ai/) to extract roaster and roast details from photos or text descriptions. `BREWLOG_OPENROUTER_API_KEY` is required. It powers:
|
||||
|
||||
- Photo extraction buttons on the roaster and roast forms
|
||||
- Text-based extraction from typed descriptions
|
||||
- The **/scan** page, which extracts both roaster and roast data from a single coffee bag label photo
|
||||
- The **Scan Bag** feature on the home page, which extracts both roaster and roast data from a single coffee bag label photo
|
||||
- The **Scan Bag** feature on the check-in page, which identifies a roast from a bag photo
|
||||
|
||||
### Nearby Cafe Search
|
||||
|
||||
When `BREWLOG_FOURSQUARE_API_KEY` is configured, the cafes page can search for nearby coffee shops via the Foursquare Places API. Searches can be made by GPS coordinates or city name.
|
||||
The check-in and cafes pages search for nearby coffee shops via the [Foursquare Places API](https://docs.foursquare.com/developer/reference/place-search). `BREWLOG_FOURSQUARE_API_KEY` is required. Searches can be made by GPS coordinates or city name.
|
||||
|
||||
## Database
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ const SESSION_COOKIE_NAME: &str = "brewlog_session";
|
|||
struct LoginTemplate {
|
||||
nav_active: &'static str,
|
||||
is_authenticated: bool,
|
||||
has_ai_extract: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +42,7 @@ pub(crate) async fn login_page(
|
|||
let template = LoginTemplate {
|
||||
nav_active: "login",
|
||||
is_authenticated: false,
|
||||
has_ai_extract: false,
|
||||
|
||||
error: None,
|
||||
};
|
||||
|
||||
|
|
@ -132,7 +131,7 @@ fn show_login_error(message: &str) -> Result<Response, StatusCode> {
|
|||
let template = LoginTemplate {
|
||||
nav_active: "login",
|
||||
is_authenticated: false,
|
||||
has_ai_extract: false,
|
||||
|
||||
error: Some(message.to_string()),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ pub(crate) async fn bags_page(
|
|||
let template = BagsTemplate {
|
||||
nav_active: "bags",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
open_bags,
|
||||
bags,
|
||||
roaster_options,
|
||||
|
|
|
|||
|
|
@ -186,7 +186,6 @@ pub(crate) async fn brews_page(
|
|||
let template = BrewsTemplate {
|
||||
nav_active: "brews",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
brews,
|
||||
bag_options,
|
||||
grinder_options,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ pub(crate) async fn cafes_page(
|
|||
let template = CafesTemplate {
|
||||
nav_active: "cafes",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
cafes,
|
||||
navigator,
|
||||
};
|
||||
|
|
@ -97,7 +96,6 @@ pub(crate) async fn cafe_page(
|
|||
let template = CafeDetailTemplate {
|
||||
nav_active: "cafes",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
cafe: cafe_view,
|
||||
};
|
||||
|
||||
|
|
@ -227,15 +225,10 @@ pub(crate) async fn nearby_cafes(
|
|||
foursquare::SearchLocation::Coordinates { lat, lng }
|
||||
};
|
||||
|
||||
let api_key = state
|
||||
.foursquare_api_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| AppError::unexpected("Foursquare API key not configured"))?;
|
||||
|
||||
let cafes = foursquare::search_nearby(
|
||||
&state.http_client,
|
||||
&state.foursquare_url,
|
||||
api_key,
|
||||
&state.foursquare_api_key,
|
||||
&location,
|
||||
q,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ pub(crate) async fn checkin_page(
|
|||
let template = CheckInTemplate {
|
||||
nav_active: "home",
|
||||
is_authenticated: true,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
has_foursquare: state.has_foursquare(),
|
||||
roast_options,
|
||||
cafe_options,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ pub(crate) async fn cups_page(
|
|||
let template = CupsTemplate {
|
||||
nav_active: "cups",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
cups,
|
||||
roast_options,
|
||||
cafe_options,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ pub(crate) async fn gear_page(
|
|||
let template = GearTemplate {
|
||||
nav_active: "gear",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
gear,
|
||||
navigator,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ pub(crate) async fn home_page(
|
|||
let template = HomeTemplate {
|
||||
nav_active: "home",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
has_foursquare: state.has_foursquare(),
|
||||
last_brew: content.last_brew,
|
||||
open_bags: content.open_bags,
|
||||
recent_events: content.recent_events,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ pub(crate) async fn roasters_page(
|
|||
let template = RoastersTemplate {
|
||||
nav_active: "roasters",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
roasters,
|
||||
navigator,
|
||||
};
|
||||
|
|
@ -101,7 +100,6 @@ pub(crate) async fn roaster_page(
|
|||
let template = RoasterDetailTemplate {
|
||||
nav_active: "roasters",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
roaster: roaster_view,
|
||||
roasts: roasts.into_iter().map(RoastView::from_list_item).collect(),
|
||||
};
|
||||
|
|
@ -192,15 +190,15 @@ pub(crate) async fn extract_roaster(
|
|||
headers: HeaderMap,
|
||||
payload: FlexiblePayload<ExtractionInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let api_key = state
|
||||
.openrouter_api_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| AppError::validation("AI extraction is not configured"))?;
|
||||
|
||||
let (input, _) = payload.into_parts();
|
||||
let result = ai::extract_roaster(&state.http_client, api_key, &state.openrouter_model, &input)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let result = ai::extract_roaster(
|
||||
&state.http_client,
|
||||
&state.openrouter_api_key,
|
||||
&state.openrouter_model,
|
||||
&input,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
use serde_json::Value;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ pub(crate) async fn roasts_page(
|
|||
let template = RoastsTemplate {
|
||||
nav_active: "roasts",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
roasts,
|
||||
roaster_options,
|
||||
navigator,
|
||||
|
|
@ -121,7 +120,6 @@ pub(crate) async fn roast_page(
|
|||
let template = RoastDetailTemplate {
|
||||
nav_active: "roasts",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
roast: RoastView::from_domain(roast, &roaster.name, &roaster.slug),
|
||||
bags: bag_views,
|
||||
};
|
||||
|
|
@ -348,15 +346,15 @@ pub(crate) async fn extract_roast_info(
|
|||
headers: HeaderMap,
|
||||
payload: FlexiblePayload<ExtractionInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let api_key = state
|
||||
.openrouter_api_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| AppError::validation("AI extraction is not configured"))?;
|
||||
|
||||
let (input, _) = payload.into_parts();
|
||||
let result = ai::extract_roast(&state.http_client, api_key, &state.openrouter_model, &input)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let result = ai::extract_roast(
|
||||
&state.http_client,
|
||||
&state.openrouter_api_key,
|
||||
&state.openrouter_model,
|
||||
&input,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
use serde_json::Value;
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ pub(crate) async fn extract_bag_scan(
|
|||
headers: HeaderMap,
|
||||
payload: FlexiblePayload<ExtractionInput>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let api_key = state
|
||||
.openrouter_api_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| AppError::validation("AI extraction is not configured"))?;
|
||||
|
||||
let (input, _) = payload.into_parts();
|
||||
let result = ai::extract_bag_scan(&state.http_client, api_key, &state.openrouter_model, &input)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let result = ai::extract_bag_scan(
|
||||
&state.http_client,
|
||||
&state.openrouter_api_key,
|
||||
&state.openrouter_model,
|
||||
&input,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
use serde_json::Value;
|
||||
|
|
@ -128,18 +128,18 @@ async fn extract_into_submission(
|
|||
state: &AppState,
|
||||
submission: &mut BagScanSubmission,
|
||||
) -> Result<(), ApiError> {
|
||||
let api_key = state
|
||||
.openrouter_api_key
|
||||
.as_deref()
|
||||
.ok_or_else(|| AppError::validation("AI extraction is not configured"))?;
|
||||
|
||||
let input = ExtractionInput {
|
||||
image: submission.image.take(),
|
||||
prompt: submission.prompt.take(),
|
||||
};
|
||||
let result = ai::extract_bag_scan(&state.http_client, api_key, &state.openrouter_model, &input)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
let result = ai::extract_bag_scan(
|
||||
&state.http_client,
|
||||
&state.openrouter_api_key,
|
||||
&state.openrouter_model,
|
||||
&input,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if let Some(name) = result.roaster.name {
|
||||
submission.roaster_name = name;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ pub(crate) async fn timeline_page(
|
|||
let template = TimelineTemplate {
|
||||
nav_active: "timeline",
|
||||
is_authenticated,
|
||||
has_ai_extract: state.has_ai_extract(),
|
||||
events: data.events,
|
||||
navigator: data.navigator,
|
||||
months: data.months,
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ pub struct ServerConfig {
|
|||
pub database_url: String,
|
||||
pub admin_password: Option<String>,
|
||||
pub admin_username: Option<String>,
|
||||
pub openrouter_api_key: Option<String>,
|
||||
pub openrouter_api_key: String,
|
||||
pub openrouter_model: String,
|
||||
pub foursquare_api_key: Option<String>,
|
||||
pub foursquare_api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -52,8 +52,8 @@ pub struct AppState {
|
|||
pub session_repo: Arc<dyn SessionRepository>,
|
||||
pub http_client: reqwest::Client,
|
||||
pub foursquare_url: String,
|
||||
pub foursquare_api_key: Option<String>,
|
||||
pub openrouter_api_key: Option<String>,
|
||||
pub foursquare_api_key: String,
|
||||
pub openrouter_api_key: String,
|
||||
pub openrouter_model: String,
|
||||
}
|
||||
|
||||
|
|
@ -73,8 +73,8 @@ impl AppState {
|
|||
session_repo: Arc<dyn SessionRepository>,
|
||||
http_client: reqwest::Client,
|
||||
foursquare_url: String,
|
||||
foursquare_api_key: Option<String>,
|
||||
openrouter_api_key: Option<String>,
|
||||
foursquare_api_key: String,
|
||||
openrouter_api_key: String,
|
||||
openrouter_model: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -96,14 +96,6 @@ impl AppState {
|
|||
openrouter_model,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_ai_extract(&self) -> bool {
|
||||
self.openrouter_api_key.is_some()
|
||||
}
|
||||
|
||||
pub fn has_foursquare(&self) -> bool {
|
||||
self.foursquare_api_key.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
|
||||
|
|
|
|||
18
src/main.rs
18
src/main.rs
|
|
@ -79,14 +79,28 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
async fn run_server(command: ServeCommand) -> Result<()> {
|
||||
let openrouter_api_key = command.openrouter_api_key.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"BREWLOG_OPENROUTER_API_KEY is required. Set this environment variable \
|
||||
to your OpenRouter API key for AI-powered extraction features."
|
||||
)
|
||||
})?;
|
||||
|
||||
let foursquare_api_key = command.foursquare_api_key.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"BREWLOG_FOURSQUARE_API_KEY is required. Set this environment variable \
|
||||
to your Foursquare API key for nearby cafe search."
|
||||
)
|
||||
})?;
|
||||
|
||||
let config = ServerConfig {
|
||||
bind_address: command.bind_address,
|
||||
database_url: command.database_url,
|
||||
admin_password: command.admin_password,
|
||||
admin_username: command.admin_username,
|
||||
openrouter_api_key: command.openrouter_api_key,
|
||||
openrouter_api_key,
|
||||
openrouter_model: command.openrouter_model,
|
||||
foursquare_api_key: command.foursquare_api_key,
|
||||
foursquare_api_key,
|
||||
};
|
||||
|
||||
serve(config).await
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use crate::domain::timeline::TimelineSortKey;
|
|||
pub struct RoastersTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub roasters: Paginated<RoasterView>,
|
||||
pub navigator: ListNavigator<RoasterSortKey>,
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ pub struct RoasterListTemplate {
|
|||
pub struct RoasterDetailTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub roaster: RoasterView,
|
||||
pub roasts: Vec<RoastView>,
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ pub struct RoasterDetailTemplate {
|
|||
pub struct RoastsTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub roasts: Paginated<RoastView>,
|
||||
pub roaster_options: Vec<RoasterOptionView>,
|
||||
pub navigator: ListNavigator<RoastSortKey>,
|
||||
|
|
@ -58,7 +58,7 @@ pub struct RoastsTemplate {
|
|||
pub struct RoastDetailTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub roast: RoastView,
|
||||
pub bags: Vec<BagView>,
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ pub struct RoastListTemplate {
|
|||
pub struct TimelineTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub events: Paginated<TimelineEventView>,
|
||||
pub navigator: ListNavigator<TimelineSortKey>,
|
||||
pub months: Vec<TimelineMonthView>,
|
||||
|
|
@ -96,7 +96,7 @@ pub struct TimelineChunkTemplate {
|
|||
pub struct BagsTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub open_bags: Vec<BagView>,
|
||||
pub bags: Paginated<BagView>,
|
||||
pub roaster_options: Vec<RoasterOptionView>,
|
||||
|
|
@ -117,7 +117,7 @@ pub struct BagListTemplate {
|
|||
pub struct GearTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub gear: Paginated<GearView>,
|
||||
pub navigator: ListNavigator<GearSortKey>,
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ pub struct RoastOptionsTemplate {
|
|||
pub struct BrewsTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub brews: Paginated<BrewView>,
|
||||
pub bag_options: Vec<BagOptionView>,
|
||||
pub grinder_options: Vec<GearOptionView>,
|
||||
|
|
@ -164,7 +164,7 @@ pub struct BrewListTemplate {
|
|||
pub struct CafesTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub cafes: Paginated<CafeView>,
|
||||
pub navigator: ListNavigator<CafeSortKey>,
|
||||
}
|
||||
|
|
@ -182,7 +182,7 @@ pub struct CafeListTemplate {
|
|||
pub struct CafeDetailTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub cafe: CafeView,
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ pub struct CafeDetailTemplate {
|
|||
pub struct CupsTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
|
||||
pub cups: Paginated<CupView>,
|
||||
pub roast_options: Vec<RoastOptionView>,
|
||||
pub cafe_options: Vec<CafeOptionView>,
|
||||
|
|
@ -211,8 +211,7 @@ pub struct CupListTemplate {
|
|||
pub struct HomeTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
pub has_foursquare: bool,
|
||||
|
||||
pub last_brew: Option<BrewView>,
|
||||
pub open_bags: Vec<BagView>,
|
||||
pub recent_events: Vec<TimelineEventView>,
|
||||
|
|
@ -224,8 +223,7 @@ pub struct HomeTemplate {
|
|||
pub struct CheckInTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub has_ai_extract: bool,
|
||||
pub has_foursquare: bool,
|
||||
|
||||
pub roast_options: Vec<RoastOptionView>,
|
||||
pub cafe_options: Vec<CafeOptionView>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{% extends "base.html" %} {% block title %}Brewlog · Check In{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% if has_foursquare %}
|
||||
<script>
|
||||
function locateUser() {
|
||||
const root = document.getElementById('checkin-root');
|
||||
|
|
@ -30,7 +29,6 @@ function locateUser() {
|
|||
);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -60,11 +58,9 @@ function locateUser() {
|
|||
data-signals:_reviewing-cafe="false"
|
||||
data-signals:_scan-waiting="false"
|
||||
data-signals:_scan-success="''"
|
||||
{% if has_foursquare %}
|
||||
data-on:location-found="$_locating = false; $_locationFound = true; $_userLat = evt.detail.lat; $_userLng = evt.detail.lng; @get('/api/v1/nearby-cafes?lat=' + evt.detail.lat + '&lng=' + evt.detail.lng + '&q=coffee', {responseOverrides: {selector: '#nearby-results', mode: 'replace'}})"
|
||||
data-on:location-error="$_locating = false; $_error = evt.detail.message"
|
||||
data-on:location-start="$_locating = true"
|
||||
{% endif %}
|
||||
>
|
||||
<header class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">Check In</h1>
|
||||
|
|
@ -151,7 +147,6 @@ function locateUser() {
|
|||
<!-- Step 1: Cafe selection -->
|
||||
<div class="mt-4" data-show="$_step === 1" style="display: none">
|
||||
<div class="rounded-lg border border-amber-300 bg-amber-100/80 p-5 shadow-sm">
|
||||
{% if has_foursquare %}
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4" data-show="!$_reviewingCafe">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -185,7 +180,6 @@ function locateUser() {
|
|||
Search by city
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -193,13 +187,10 @@ function locateUser() {
|
|||
data-show="!$_reviewingCafe"
|
||||
class="input-field w-full text-sm"
|
||||
placeholder="Search for a cafe…"
|
||||
{% if has_foursquare %}
|
||||
data-on:input__debounce.350ms="if ($_searchByCity) { $_cityName.length >= 2 && $_cafeSearch.length >= 2 && @get('/api/v1/nearby-cafes?near=' + encodeURIComponent($_cityName) + '&q=' + encodeURIComponent($_cafeSearch), {responseOverrides: {selector: '#nearby-results', mode: 'replace'}}) } else { $_locationFound && @get('/api/v1/nearby-cafes?lat=' + $_userLat + '&lng=' + $_userLng + '&q=' + encodeURIComponent($_cafeSearch), {responseOverrides: {selector: '#nearby-results', mode: 'replace'}}) }"
|
||||
{% endif %}
|
||||
/>
|
||||
|
||||
{% if has_foursquare %}
|
||||
<!-- Foursquare results (replaced by server fragment) -->
|
||||
<!-- Nearby results (replaced by server fragment) -->
|
||||
<div data-show="!$_reviewingCafe" id="nearby-results" class="hidden mt-3 max-h-60 overflow-y-auto rounded-lg border border-amber-200 bg-white"></div>
|
||||
|
||||
<!-- Review form for new cafe from Foursquare -->
|
||||
|
|
@ -245,7 +236,6 @@ function locateUser() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Saved cafes -->
|
||||
{% if !cafe_options.is_empty() %}
|
||||
|
|
@ -270,7 +260,6 @@ function locateUser() {
|
|||
<div class="rounded-lg border border-amber-300 bg-amber-100/80 p-5 shadow-sm">
|
||||
<h3 class="text-base font-semibold text-amber-700 mb-3">What are you drinking?</h3>
|
||||
|
||||
{% if has_ai_extract %}
|
||||
<div data-show="!$_scanWaiting && !$_scanSuccess" style="display: none" class="mb-4">
|
||||
<p class="text-sm text-stone-600 mb-3">Scan a bag to identify the coffee, or select from your existing roasts below.</p>
|
||||
|
||||
|
|
@ -340,9 +329,6 @@ function locateUser() {
|
|||
|
||||
<div class="border-t border-amber-200 pt-3">
|
||||
<p class="text-xs text-stone-500 mb-2">Or select an existing roast:</p>
|
||||
{% else %}
|
||||
<div>
|
||||
{% endif %}
|
||||
<select
|
||||
class="input-field w-full text-sm"
|
||||
data-on:change="$_roastId = el.value; $_roastName = el.options[el.selectedIndex].dataset.name || ''; $_roasterName = el.options[el.selectedIndex].dataset.roaster || ''; el.value && ($_step = 3)"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
data-signals:_scan-submitting="false"
|
||||
data-signals:_scan-error="''"
|
||||
>
|
||||
{% if has_ai_extract %}
|
||||
<button
|
||||
type="button"
|
||||
data-on:click="$_showScan = !$_showScan; !$_showScan && ($_scanExtracted = false, $_extracting = false, $_extractError = '', $_scanError = '')"
|
||||
|
|
@ -49,7 +48,6 @@
|
|||
</svg>
|
||||
Scan Bag
|
||||
</button>
|
||||
{% endif %}
|
||||
<a
|
||||
href="/check-in"
|
||||
class="flex items-center justify-center gap-3 rounded-lg border-2 border-amber-500 px-6 py-5 text-lg font-semibold text-amber-700 shadow-sm transition hover:bg-amber-50"
|
||||
|
|
@ -63,7 +61,6 @@
|
|||
</section>
|
||||
|
||||
<!-- Inline Scan Section (hidden by default) -->
|
||||
{% if has_ai_extract %}
|
||||
<section data-show="$_showScan" style="display: none"
|
||||
data-signals:_roaster-name="''"
|
||||
data-signals:_roaster-country="''"
|
||||
|
|
@ -211,7 +208,6 @@
|
|||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Last Brew -->
|
||||
{% if let Some(brew) = last_brew %}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{% if has_ai_extract %}
|
||||
<!-- Hidden file input — minimal JS for FileReader API -->
|
||||
<input type="file" id="roaster-photo" accept="image/*" capture="environment" class="hidden"
|
||||
onchange="if(this.files[0]){const r=new FileReader();r.onload=()=>{document.getElementById('roaster-image').value=r.result;document.getElementById('roaster-extract-form').requestSubmit()};r.readAsDataURL(this.files[0]);this.value=''}" />
|
||||
|
|
@ -80,7 +79,6 @@
|
|||
</div>
|
||||
<p data-show="$_extractError" data-text="$_extractError" style="display:none" class="mt-2 text-sm text-red-600"></p>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form
|
||||
id="roaster-form"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{% if has_ai_extract %}
|
||||
<!-- Hidden file input — minimal JS for FileReader API -->
|
||||
<input type="file" id="roast-photo" accept="image/*" capture="environment" class="hidden"
|
||||
onchange="if(this.files[0]){const r=new FileReader();r.onload=()=>{document.getElementById('roast-image').value=r.result;document.getElementById('roast-extract-form').requestSubmit()};r.readAsDataURL(this.files[0]);this.value=''}" />
|
||||
|
|
@ -94,7 +93,6 @@
|
|||
</div>
|
||||
<p data-show="$_extractError" data-text="$_extractError" style="display:none" class="mt-2 text-sm text-red-600"></p>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form
|
||||
id="roast-form"
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ pub async fn spawn_app() -> TestApp {
|
|||
token_repo,
|
||||
session_repo,
|
||||
brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(),
|
||||
None,
|
||||
String::new(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
|
@ -111,7 +111,7 @@ async fn spawn_app_inner(
|
|||
token_repo: Arc<dyn TokenRepository>,
|
||||
session_repo: Arc<dyn SessionRepository>,
|
||||
foursquare_url: String,
|
||||
foursquare_api_key: Option<String>,
|
||||
foursquare_api_key: String,
|
||||
mock_server: Option<wiremock::MockServer>,
|
||||
) -> TestApp {
|
||||
// Create application state
|
||||
|
|
@ -130,7 +130,7 @@ async fn spawn_app_inner(
|
|||
reqwest::Client::new(),
|
||||
foursquare_url,
|
||||
foursquare_api_key,
|
||||
None,
|
||||
String::new(),
|
||||
"openrouter/free".to_string(),
|
||||
);
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ pub async fn spawn_app_with_foursquare_mock() -> TestApp {
|
|||
token_repo,
|
||||
session_repo,
|
||||
foursquare_url,
|
||||
Some("test-api-key".to_string()),
|
||||
"test-api-key".to_string(),
|
||||
Some(mock_server),
|
||||
)
|
||||
.await;
|
||||
|
|
|
|||
Loading…
Reference in a new issue