repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

2.8 KB · 81 lines · Rust Raw History
 1use axum::{
 2    extract::State,
 3    http::{header, HeaderMap, StatusCode},
 4    response::{IntoResponse, Response},
 5    routing::get,
 6    Router,
 7};
 8
 9use crate::AppState;
10
11pub fn router() -> Router<AppState> {
12    Router::new()
13        .route("/favicon.ico", get(favicon))
14        .route("/robots.txt", get(robots))
15        .route("/sitemap.xml", get(sitemap))
16}
17
18/// The Paper Ledger mark: a rising figures line over the accountant's double
19/// underline, ink-on-paper. Matches the topbar brand in `base.html`.
20async fn favicon() -> Response {
21    let mut h = HeaderMap::new();
22    h.insert(header::CONTENT_TYPE, "image/svg+xml".parse().unwrap());
23    let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
24<rect width="64" height="64" rx="14" fill="#211f1a"/>
25<polyline points="14,38 26,28 37,33 52,16" fill="none" stroke="#f0ece1" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
26<circle cx="52" cy="16" r="4" fill="#f0ece1"/>
27<line x1="14" y1="49" x2="50" y2="49" stroke="#f0ece1" stroke-width="4.2" stroke-linecap="round"/>
28<line x1="14" y1="56" x2="50" y2="56" stroke="#f0ece1" stroke-width="4.2" stroke-linecap="round"/>
29</svg>"##;
30    (StatusCode::OK, h, svg).into_response()
31}
32
33async fn robots() -> Response {
34    let mut h = HeaderMap::new();
35    h.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap());
36    // Keep crawlers off the JSON API and the SSE stream: both are
37    // valueless to index and /stream holds a connection open per hit.
38    (
39        StatusCode::OK,
40        h,
41        "User-agent: *\nAllow: /\nDisallow: /api/\nDisallow: /stream\n",
42    )
43        .into_response()
44}
45
46async fn sitemap(State(state): State<AppState>) -> Response {
47    let mut h = HeaderMap::new();
48    h.insert(header::CONTENT_TYPE, "application/xml".parse().unwrap());
49    let base = if state.config.base_url.is_empty() {
50        "/".to_string()
51    } else {
52        format!("{}/", state.config.base_url.trim_end_matches('/'))
53    };
54    let now = chrono::Utc::now().format("%Y-%m-%d");
55
56    // Static pages plus one entry per known symbol.
57    let tickers: Vec<String> = sqlx::query_scalar("SELECT ticker FROM symbols ORDER BY ticker")
58        .fetch_all(&state.pool)
59        .await
60        .unwrap_or_default();
61
62    let mut urls = String::new();
63    for page in ["", "search"] {
64        urls.push_str(&format!(
65            "  <url><loc>{base}{page}</loc><lastmod>{now}</lastmod></url>\n"
66        ));
67    }
68    for t in tickers {
69        let enc = urlencoding::encode(&t);
70        urls.push_str(&format!(
71            "  <url><loc>{base}s/{enc}</loc><lastmod>{now}</lastmod></url>\n"
72        ));
73    }
74
75    let body = format!(
76        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
77         <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n{urls}</urlset>\n"
78    );
79    (StatusCode::OK, h, body).into_response()
80}