Small fixes & fmt

This commit is contained in:
Andras Schmelczer 2026-03-19 21:51:07 +00:00
parent 6b12e21d50
commit f32a552f46
23 changed files with 347 additions and 99 deletions

View file

@ -437,10 +437,7 @@ async fn main() -> anyhow::Result<()> {
.route("/api/features", get(routes::get_features))
.route("/api/hexagons", get(routes::get_hexagons))
.route("/api/postcodes", get(routes::get_postcodes))
.route(
"/api/postcode/{postcode}",
get(routes::get_postcode_lookup),
)
.route("/api/postcode/{postcode}", get(routes::get_postcode_lookup))
.route("/api/pois", get(routes::get_pois))
.route("/api/poi-categories", get(routes::get_poi_categories))
.route("/api/places", get(routes::get_places))
@ -478,10 +475,7 @@ async fn main() -> anyhow::Result<()> {
"/api/checkout",
post(routes::post_checkout).layer(ConcurrencyLimitLayer::new(10)),
)
.route(
"/api/stripe-webhook",
post(routes::post_stripe_webhook),
)
.route("/api/stripe-webhook", post(routes::post_stripe_webhook))
.route(
"/api/invites",
get(routes::get_invites).post(routes::post_invites),
@ -491,10 +485,7 @@ async fn main() -> anyhow::Result<()> {
.route("/s/{code}", get(routes::get_short_url))
.route("/api/telemetry", post(routes::post_telemetry))
.route("/api/reload", post(routes::post_reload))
.route(
"/pb/{*rest}",
any(routes::proxy_to_pocketbase),
)
.route("/pb/{*rest}", any(routes::proxy_to_pocketbase))
// Tile routes use a different state type — kept as closures
.route(
"/api/tiles/{z}/{x}/{y}",

View file

@ -154,6 +154,20 @@ impl Field {
}
}
fn number(name: &str) -> Self {
Self {
name: name.to_string(),
r#type: "number".to_string(),
required: None,
max_select: None,
collection_id: None,
max_size: None,
mime_types: None,
on_create: None,
on_update: None,
}
}
fn autodate(name: &str, on_create: bool, on_update: bool) -> Self {
Self {
name: name.to_string(),
@ -717,6 +731,39 @@ pub async fn ensure_collections(
ensure_autodate_fields(client, base_url, &token, "short_urls").await?;
}
if !existing.iter().any(|n| n == "ai_query_logs") {
let users_id = find_users_collection_id(client, base_url, &token).await?;
create_collection(
client,
base_url,
&token,
CreateCollection {
name: "ai_query_logs".to_string(),
r#type: "base".to_string(),
fields: vec![
Field::relation("user", &users_id),
Field::text("query", true),
Field::text("listing_type", false),
Field::text("response_filters", false),
Field::text("response_notes", false),
Field::number("tokens_used"),
Field::number("rounds"),
Field::text("model", false),
Field::autodate("created", true, false),
Field::autodate("updated", true, true),
],
list_rule: None,
view_rule: None,
create_rule: None,
update_rule: None,
delete_rule: None,
},
)
.await?;
} else {
ensure_autodate_fields(client, base_url, &token, "ai_query_logs").await?;
}
Ok(())
}
@ -869,6 +916,56 @@ async fn poll_pocketbase_counts(state: &AppState) {
}
}
/// Insert a record into the `ai_query_logs` collection.
/// Best-effort — logs warnings on failure but does not propagate errors.
#[allow(clippy::too_many_arguments)]
pub async fn log_ai_query(
state: &AppState,
user_id: &str,
query: &str,
listing_type: &str,
response_filters: &str,
response_notes: &str,
tokens_used: u64,
rounds: u64,
) {
let token = match get_superuser_token(state).await {
Ok(tk) => tk,
Err(err) => {
warn!("Failed to auth superuser for AI query log: {err}");
return;
}
};
let pb_url = state.pocketbase_url.trim_end_matches('/');
let url = format!("{pb_url}/api/collections/ai_query_logs/records");
let res = state
.http_client
.post(&url)
.header("Authorization", format!("Bearer {token}"))
.json(&serde_json::json!({
"user": user_id,
"query": query,
"listing_type": listing_type,
"response_filters": response_filters,
"response_notes": response_notes,
"tokens_used": tokens_used,
"rounds": rounds,
"model": &state.gemini_model,
}))
.send()
.await;
match res {
Ok(resp) if resp.status().is_success() => {}
Ok(resp) => {
let status = resp.status();
warn!("Failed to log AI query ({status})");
}
Err(err) => warn!("Failed to log AI query: {err}"),
}
}
async fn pb_count(
client: &reqwest::Client,
pb_url: &str,

View file

@ -12,7 +12,7 @@ use tracing::{info, warn};
use crate::auth::OptionalUser;
use crate::consts::{AI_FILTERS_MAX_TOKENS, AI_FILTERS_TEMPERATURE, AI_FILTERS_WEEKLY_TOKEN_LIMIT};
use crate::data::slugify;
use crate::pocketbase::get_superuser_token;
use crate::pocketbase::{get_superuser_token, log_ai_query};
use crate::routes::{FeatureInfo, FeaturesResponse};
use crate::state::{AppState, SharedState};
use crate::utils::gemini_chat;
@ -783,6 +783,28 @@ pub async fn post_ai_filters(
counter!("ai_tokens_total").increment(total_tokens_accumulated);
counter!("ai_requests_total", "status" => "success").increment(1);
// Log the query to PocketBase (fire-and-forget)
let filters_json = serde_json::to_string(&filters).unwrap_or_default();
let log_state = state.clone();
let log_user_id = user.id.clone();
let log_query = req.query.clone();
let log_listing_type = listing_type.to_string();
let log_notes = notes.clone();
let log_rounds = (round + 1) as u64;
tokio::spawn(async move {
log_ai_query(
&log_state,
&log_user_id,
&log_query,
&log_listing_type,
&filters_json,
&log_notes,
total_tokens_accumulated,
log_rounds,
)
.await;
});
return Ok(Json(AiFiltersResponse {
filters,
travel_time_filters,

View file

@ -22,7 +22,10 @@ static PROXY_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
.expect("Failed to build proxy HTTP client")
});
pub async fn proxy_to_pocketbase(State(shared): State<Arc<SharedState>>, req: Request) -> impl IntoResponse {
pub async fn proxy_to_pocketbase(
State(shared): State<Arc<SharedState>>,
req: Request,
) -> impl IntoResponse {
let state = shared.load_state();
let pb_url = state.pocketbase_url.trim_end_matches('/');

View file

@ -128,7 +128,9 @@ pub struct POICategoriesResponse {
groups: Vec<POICategoryGroup>,
}
pub async fn get_poi_categories(State(shared): State<Arc<SharedState>>) -> Json<POICategoriesResponse> {
pub async fn get_poi_categories(
State(shared): State<Arc<SharedState>>,
) -> Json<POICategoriesResponse> {
let state = shared.load_state();
let groups: Vec<POICategoryGroup> = state.poi_category_groups.to_vec();

View file

@ -38,7 +38,10 @@ struct PbRecord {
params: String,
}
pub async fn post_shorten(State(shared): State<Arc<SharedState>>, Json(req): Json<ShortenRequest>) -> Response {
pub async fn post_shorten(
State(shared): State<Arc<SharedState>>,
Json(req): Json<ShortenRequest>,
) -> Response {
let state = shared.load_state();
let pb_url = state.pocketbase_url.trim_end_matches('/');
@ -86,7 +89,10 @@ pub async fn post_shorten(State(shared): State<Arc<SharedState>>, Json(req): Jso
}
}
pub async fn get_short_url(State(shared): State<Arc<SharedState>>, Path(code): Path<String>) -> Response {
pub async fn get_short_url(
State(shared): State<Arc<SharedState>>,
Path(code): Path<String>,
) -> Response {
let state = shared.load_state();
if code.is_empty() || code.len() > 20 || !code.bytes().all(|b| b.is_ascii_alphanumeric()) {