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 common;
|
||||||
mod api;
|
mod api;
|
||||||
mod docs;
|
mod docs;
|
||||||
|
mod pages;
|
||||||
|
|
||||||
pub use common::{ServerConfig, AppState, logging_middleware};
|
pub use common::{ServerConfig, AppState, logging_middleware};
|
||||||
|
|
||||||
@@ -62,9 +63,10 @@ async fn run_server(
|
|||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add API and documentation routes
|
// Add API, documentation, and pages routes
|
||||||
let app = api::add_routes(app);
|
let app = api::add_routes(app);
|
||||||
let app = docs::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
|
// Apply state to the router after all routes are added
|
||||||
let app = app.with_state(state);
|
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