- Add /v1/feed API endpoint with handler and tests - Remove health endpoint rate limiting (behind firewall, caused spurious 429s) - Add dashboard feed panel with list, row, empty state, and loading skeleton - Update home page to show feed instead of redirecting to skeptic - Improve API key auth middleware and DTO create/query params - Add OpenAPI conceptual guide (api-intro.md) with semaglutide examples - Add FindMyHealth application scaffolding (vision, architecture, prototypes) - Add FindMyHealth designer/writer and Aphoria founder-CEO agents - Update roadmap with current progress Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
91 lines
2.9 KiB
Rust
91 lines
2.9 KiB
Rust
//! Handler for the `/v1/feed` endpoint (newest-first assertion browsing).
|
|
|
|
use axum::{extract::State, Json};
|
|
use tracing::{instrument, warn};
|
|
|
|
use crate::{
|
|
dto::{FeedParams, QueryResponse},
|
|
error::Result,
|
|
extractors::QsQuery,
|
|
state::AppState,
|
|
};
|
|
|
|
use stemedb_query::Query;
|
|
|
|
use super::query::assertion_to_dto_with_warning;
|
|
|
|
/// Browse all assertions in newest-first order with pagination.
|
|
///
|
|
/// Returns assertions sorted by timestamp descending, useful for
|
|
/// "what was just written?" dashboards and dev workflows. No lens
|
|
/// resolution is applied — this is a raw chronological feed.
|
|
///
|
|
/// # Pagination
|
|
///
|
|
/// - `limit`: Max results per page (default 50, max 500)
|
|
/// - `offset`: Number of results to skip (default 0)
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/v1/feed",
|
|
params(
|
|
("limit" = Option<usize>, Query, description = "Max results (default 50, max 500)"),
|
|
("offset" = Option<usize>, Query, description = "Pagination offset (default 0)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Feed results", body = QueryResponse),
|
|
(status = 500, description = "Internal server error", body = crate::dto::ErrorResponse)
|
|
),
|
|
tag = "query"
|
|
)]
|
|
#[instrument(skip(state), fields(limit = params.limit, offset = params.offset))]
|
|
pub async fn feed(
|
|
State(state): State<AppState>,
|
|
QsQuery(params): QsQuery<FeedParams>,
|
|
) -> Result<Json<QueryResponse>> {
|
|
metrics::counter!("stemedb_queries_total", "endpoint" => "feed").increment(1);
|
|
let query_start = std::time::Instant::now();
|
|
|
|
// Fetch all assertions (no subject filter)
|
|
let query = Query::builder().limit(usize::MAX).build();
|
|
let query_engine = state.query_engine();
|
|
let result = query_engine.execute(&query).await?;
|
|
|
|
let mut assertions = result.assertions;
|
|
|
|
if assertions.len() > 10_000 {
|
|
warn!(
|
|
count = assertions.len(),
|
|
"Feed scanning large assertion set; consider adding index-backed pagination"
|
|
);
|
|
}
|
|
|
|
// Sort by timestamp descending (newest first)
|
|
assertions.sort_unstable_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
|
|
|
let total_count = assertions.len();
|
|
let limit = params.clamped_limit();
|
|
let offset = params.offset;
|
|
let has_more = offset + limit < total_count;
|
|
|
|
// Apply offset + limit pagination
|
|
let page: Vec<_> = assertions.into_iter().skip(offset).take(limit).collect();
|
|
|
|
// Convert to DTOs (no source enrichment for speed)
|
|
let assertion_responses = page
|
|
.into_iter()
|
|
.map(|a| assertion_to_dto_with_warning(a, None))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
metrics::histogram!("stemedb_query_latency_seconds", "endpoint" => "feed")
|
|
.record(query_start.elapsed().as_secs_f64());
|
|
|
|
Ok(Json(QueryResponse {
|
|
assertions: assertion_responses,
|
|
total_count,
|
|
has_more,
|
|
conflict_score: None,
|
|
resolution_confidence: None,
|
|
changes_since: None,
|
|
}))
|
|
}
|