refactor: extract theme toggle styles to CSS and improve timeout handling

This commit is contained in:
2026-01-28 19:47:24 -06:00
parent 7cc8267c2e
commit fa2fc45aa9
12 changed files with 88 additions and 126 deletions
+1 -1
View File
@@ -298,7 +298,7 @@ impl DateRange {
/// Get the number of weeks between start and end dates
pub fn weeks_duration(&self) -> u32 {
let duration = self.end.signed_duration_since(self.start);
duration.num_weeks() as u32
duration.num_weeks().max(0) as u32
}
/// Check if a specific date falls within this range
+2 -1
View File
@@ -168,8 +168,9 @@ impl SearchQuery {
}
/// Sets the maximum number of results to return
/// Clamped to a maximum of 500 to prevent excessive API load
pub fn max_results(mut self, max_results: i32) -> Self {
self.max_results = max_results;
self.max_results = max_results.clamp(1, 500);
self
}
+15 -17
View File
@@ -7,13 +7,12 @@ use cookie::Cookie;
use dashmap::DashMap;
use governor::state::InMemoryState;
use governor::{Quota, RateLimiter};
use once_cell::sync::Lazy;
use rand::distr::{Alphanumeric, SampleString};
use reqwest_middleware::ClientWithMiddleware;
use std::collections::{HashMap, VecDeque};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, Notify};
use tracing::{debug, info, trace};
@@ -23,9 +22,9 @@ const SESSION_EXPIRY: Duration = Duration::from_secs(25 * 60); // 25 minutes
// A global rate limiter to ensure we only try to create one new session every 10 seconds,
// preventing us from overwhelming the server with session creation requests.
static SESSION_CREATION_RATE_LIMITER: Lazy<
static SESSION_CREATION_RATE_LIMITER: LazyLock<
RateLimiter<governor::state::direct::NotKeyed, InMemoryState, governor::clock::DefaultClock>,
> = Lazy::new(|| RateLimiter::direct(Quota::with_period(Duration::from_secs(10)).unwrap()));
> = LazyLock::new(|| RateLimiter::direct(Quota::with_period(Duration::from_secs(10)).unwrap()));
/// Represents an active anonymous session within the Banner API.
/// Identified by multiple persistent cookies, as well as a client-generated "unique session ID".
@@ -63,16 +62,16 @@ pub fn nonce() -> String {
impl BannerSession {
/// Creates a new session
pub fn new(unique_session_id: &str, jsessionid: &str, ssb_cookie: &str) -> Result<Self> {
pub fn new(unique_session_id: &str, jsessionid: &str, ssb_cookie: &str) -> Self {
let now = Instant::now();
Ok(Self {
Self {
created_at: now,
last_activity: None,
unique_session_id: unique_session_id.to_string(),
jsessionid: jsessionid.to_string(),
ssb_cookie: ssb_cookie.to_string(),
})
}
}
/// Returns the unique session ID
@@ -124,47 +123,46 @@ mod tests {
use super::*;
#[test]
fn test_new_session_returns_ok() {
fn test_new_session_creates_session() {
let session = BannerSession::new("sess-1", "JSID123", "SSB456");
assert!(session.is_ok());
assert_eq!(session.unwrap().id(), "sess-1");
assert_eq!(session.id(), "sess-1");
}
#[test]
fn test_fresh_session_not_expired() {
let session = BannerSession::new("sess-1", "JSID123", "SSB456").unwrap();
let session = BannerSession::new("sess-1", "JSID123", "SSB456");
assert!(!session.is_expired());
}
#[test]
fn test_fresh_session_not_been_used() {
let session = BannerSession::new("sess-1", "JSID123", "SSB456").unwrap();
let session = BannerSession::new("sess-1", "JSID123", "SSB456");
assert!(!session.been_used());
}
#[test]
fn test_touch_marks_used() {
let mut session = BannerSession::new("sess-1", "JSID123", "SSB456").unwrap();
let mut session = BannerSession::new("sess-1", "JSID123", "SSB456");
session.touch();
assert!(session.been_used());
}
#[test]
fn test_touched_session_not_expired() {
let mut session = BannerSession::new("sess-1", "JSID123", "SSB456").unwrap();
let mut session = BannerSession::new("sess-1", "JSID123", "SSB456");
session.touch();
assert!(!session.is_expired());
}
#[test]
fn test_cookie_format() {
let session = BannerSession::new("sess-1", "JSID123", "SSB456").unwrap();
let session = BannerSession::new("sess-1", "JSID123", "SSB456");
assert_eq!(session.cookie(), "JSESSIONID=JSID123; SSB_COOKIE=SSB456");
}
#[test]
fn test_id_returns_unique_session_id() {
let session = BannerSession::new("my-unique-id", "JSID123", "SSB456").unwrap();
let session = BannerSession::new("my-unique-id", "JSID123", "SSB456");
assert_eq!(session.id(), "my-unique-id");
}
@@ -454,7 +452,7 @@ impl SessionPool {
self.select_term(&term.to_string(), &unique_session_id, &cookie_header)
.await?;
BannerSession::new(&unique_session_id, jsessionid, ssb_cookie)
Ok(BannerSession::new(&unique_session_id, jsessionid, ssb_cookie))
}
/// Retrieves a list of terms from the Banner API.
+1 -1
View File
@@ -2,5 +2,5 @@
/// Returns a browser-like user agent string.
pub fn user_agent() -> &'static str {
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
+10 -44
View File
@@ -5,58 +5,24 @@ use crate::data::models::TargetType;
use crate::error::Result;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::fmt;
use thiserror::Error;
/// Errors that can occur during job parsing
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum JobParseError {
InvalidJson(serde_json::Error),
#[error("Invalid JSON in job payload: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("Unsupported target type: {0:?}")]
UnsupportedTargetType(TargetType),
}
impl fmt::Display for JobParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JobParseError::InvalidJson(e) => write!(f, "Invalid JSON in job payload: {}", e),
JobParseError::UnsupportedTargetType(t) => {
write!(f, "Unsupported target type: {:?}", t)
}
}
}
}
impl std::error::Error for JobParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
JobParseError::InvalidJson(e) => Some(e),
_ => None,
}
}
}
/// Errors that can occur during job processing
#[derive(Debug)]
#[derive(Debug, Error)]
pub enum JobError {
Recoverable(anyhow::Error), // API failures, network issues
Unrecoverable(anyhow::Error), // Parse errors, corrupted data
}
impl fmt::Display for JobError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JobError::Recoverable(e) => write!(f, "Recoverable error: {}", e),
JobError::Unrecoverable(e) => write!(f, "Unrecoverable error: {}", e),
}
}
}
impl std::error::Error for JobError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
JobError::Recoverable(e) => e.source(),
JobError::Unrecoverable(e) => e.source(),
}
}
#[error("Recoverable error: {0}")]
Recoverable(#[source] anyhow::Error),
#[error("Unrecoverable error: {0}")]
Unrecoverable(#[source] anyhow::Error),
}
/// Common trait interface for all job types
+2 -2
View File
@@ -4,10 +4,10 @@
//! at compile time using rust-embed.
use dashmap::DashMap;
use once_cell::sync::Lazy;
use rapidhash::v3::rapidhash_v3;
use rust_embed::RustEmbed;
use std::fmt;
use std::sync::LazyLock;
/// Embedded web assets from the dist directory
#[derive(RustEmbed)]
@@ -65,7 +65,7 @@ impl AssetMetadata {
}
/// Global cache for asset metadata to avoid repeated calculations
static ASSET_CACHE: Lazy<DashMap<String, AssetMetadata>> = Lazy::new(DashMap::new);
static ASSET_CACHE: LazyLock<DashMap<String, AssetMetadata>> = LazyLock::new(DashMap::new);
/// Get cached asset metadata for a file path, caching on-demand
/// Returns AssetMetadata containing MIME type and RapidHash hash