Small fixes & fmt
This commit is contained in:
parent
6b12e21d50
commit
f32a552f46
23 changed files with 347 additions and 99 deletions
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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('/');
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue