feat: add HTML endpoints for item listing and details pages
Co-authored-by: aider (openai/andrew/openrouter/qwen/qwen3-coder) <aider@aider.chat>
This commit is contained in:
@@ -15,6 +15,7 @@ use tower_http::trace::TraceLayer;
|
||||
mod common;
|
||||
mod api;
|
||||
mod docs;
|
||||
mod pages;
|
||||
|
||||
pub use common::{ServerConfig, AppState, logging_middleware};
|
||||
|
||||
@@ -62,9 +63,10 @@ async fn run_server(
|
||||
.layer(CorsLayer::permissive())
|
||||
);
|
||||
|
||||
// Add API and documentation routes
|
||||
// Add API, documentation, and pages routes
|
||||
let app = api::add_routes(app);
|
||||
let app = docs::add_routes(app);
|
||||
let app = pages::add_routes(app);
|
||||
|
||||
// Apply state to the router after all routes are added
|
||||
let app = app.with_state(state);
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
use crate::db::{self, Item, Tag, Meta};
|
||||
use crate::modes::server::AppState;
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
response::Html,
|
||||
};
|
||||
use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListQueryParams {
|
||||
#[serde(default = "default_sort")]
|
||||
sort: String,
|
||||
#[serde(default)]
|
||||
tags: Option<String>,
|
||||
#[serde(default = "default_count")]
|
||||
count: usize,
|
||||
#[serde(default)]
|
||||
start: usize,
|
||||
}
|
||||
|
||||
fn default_sort() -> String {
|
||||
"newest".to_string()
|
||||
}
|
||||
|
||||
fn default_count() -> usize {
|
||||
1000
|
||||
}
|
||||
|
||||
pub fn add_routes(app: axum::Router<AppState>) -> axum::Router<AppState> {
|
||||
app.route("/", axum::routing::get(list_items))
|
||||
.route("/item/:id", axum::routing::get(show_item))
|
||||
}
|
||||
|
||||
async fn list_items(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ListQueryParams>,
|
||||
) -> Result<Html<String>, Html<String>> {
|
||||
let conn = state.db.lock().await;
|
||||
|
||||
let result = build_item_list(&conn, ¶ms);
|
||||
|
||||
match result {
|
||||
Ok(html) => Ok(Html(html)),
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {}</body></html>", e))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_item_list(conn: &Connection, params: &ListQueryParams) -> Result<String> {
|
||||
let tags: Vec<String> = params.tags
|
||||
.as_ref()
|
||||
.map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let items = if tags.is_empty() {
|
||||
db::query_all_items(conn)?
|
||||
} else {
|
||||
db::query_tagged_items(conn, &tags)?
|
||||
};
|
||||
|
||||
// Sort items
|
||||
let mut sorted_items = items;
|
||||
if params.sort == "newest" {
|
||||
sorted_items.sort_by(|a, b| b.id.cmp(&a.id));
|
||||
} else {
|
||||
sorted_items.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
let start = params.start;
|
||||
let end = std::cmp::min(start + params.count, sorted_items.len());
|
||||
let page_items = if start < sorted_items.len() {
|
||||
sorted_items[start..std::cmp::min(end, sorted_items.len())].to_vec()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Get tags for all items in the page
|
||||
let item_ids: Vec<i64> = page_items.iter().filter_map(|item| item.id).collect();
|
||||
let tags_map = db::get_tags_for_items(conn, &item_ids)?;
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str("<html><head><title>Items</title></head><body>");
|
||||
html.push_str("<h1>Items</h1>");
|
||||
html.push_str("<ul>");
|
||||
|
||||
for item in page_items {
|
||||
let item_id = item.id.unwrap_or(0);
|
||||
let tags_html = if let Some(tags) = tags_map.get(&item_id) {
|
||||
let tag_names: Vec<String> = tags.iter().map(|t| t.name.clone()).collect();
|
||||
if tag_names.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" [{}]", tag_names.join(", "))
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
html.push_str(&format!(
|
||||
"<li><a href=\"/item/{}\">Item #{}</a> - {}{} - <a href=\"/api/item/{}/content\">Download</a></li>",
|
||||
item_id,
|
||||
item_id,
|
||||
item.ts.format("%Y-%m-%d %H:%M:%S"),
|
||||
tags_html,
|
||||
item_id
|
||||
));
|
||||
}
|
||||
|
||||
html.push_str("</ul>");
|
||||
|
||||
// Add pagination info
|
||||
html.push_str(&format!("<p>Showing {} to {} of {} items</p>",
|
||||
start + 1,
|
||||
std::cmp::min(end, sorted_items.len()),
|
||||
sorted_items.len()));
|
||||
|
||||
html.push_str("</body></html>");
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
async fn show_item(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Html<String>, Html<String>> {
|
||||
let conn = state.db.lock().await;
|
||||
|
||||
let result = build_item_details(&conn, id);
|
||||
|
||||
match result {
|
||||
Ok(html) => Ok(Html(html)),
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {}</body></html>", e))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
|
||||
let item = match db::get_item(conn, id)? {
|
||||
Some(item) => item,
|
||||
None => return Err(anyhow::anyhow!("Item not found")),
|
||||
};
|
||||
|
||||
let tags = db::get_item_tags(conn, &item)?;
|
||||
let metas = db::get_item_meta(conn, &item)?;
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str("<html><head><title>Item Details</title></head><body>");
|
||||
html.push_str(&format!("<h1>Item #{}</h1>", id));
|
||||
html.push_str("<table>");
|
||||
html.push_str(&format!("<tr><td>ID:</td><td>{}</td></tr>", item.id.unwrap_or(0)));
|
||||
html.push_str(&format!("<tr><td>Timestamp:</td><td>{}</td></tr>", item.ts.format("%Y-%m-%d %H:%M:%S")));
|
||||
html.push_str(&format!("<tr><td>Size:</td><td>{}</td></tr>", item.size.unwrap_or(0)));
|
||||
html.push_str(&format!("<tr><td>Compression:</td><td>{}</td></tr>", item.compression));
|
||||
html.push_str("</table>");
|
||||
|
||||
// Tags section
|
||||
html.push_str("<h2>Tags</h2>");
|
||||
if tags.is_empty() {
|
||||
html.push_str("<p>No tags</p>");
|
||||
} else {
|
||||
html.push_str("<ul>");
|
||||
for tag in tags {
|
||||
html.push_str(&format!("<li>{}</li>", tag.name));
|
||||
}
|
||||
html.push_str("</ul>");
|
||||
}
|
||||
|
||||
// Metadata section
|
||||
html.push_str("<h2>Metadata</h2>");
|
||||
if metas.is_empty() {
|
||||
html.push_str("<p>No metadata</p>");
|
||||
} else {
|
||||
html.push_str("<table>");
|
||||
html.push_str("<tr><th>Name</th><th>Value</th></tr>");
|
||||
for meta in metas {
|
||||
html.push_str(&format!("<tr><td>{}</td><td>{}</td></tr>", meta.name, meta.value));
|
||||
}
|
||||
html.push_str("</table>");
|
||||
}
|
||||
|
||||
// Links
|
||||
html.push_str("<h2>Actions</h2>");
|
||||
html.push_str(&format!("<p><a href=\"/api/item/{}/content\">Download Content</a></p>", id));
|
||||
html.push_str("<p><a href=\"/\">Back to list</a></p>");
|
||||
|
||||
html.push_str("</body></html>");
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user