mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 02:23:34 -06:00
refactor: extract theme toggle styles to CSS and improve timeout handling
This commit is contained in:
Generated
-15
@@ -238,7 +238,6 @@ dependencies = [
|
|||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"num-format",
|
"num-format",
|
||||||
"once_cell",
|
|
||||||
"poise",
|
"poise",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"rapidhash",
|
"rapidhash",
|
||||||
@@ -1284,12 +1283,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "http-range-header"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -3544,20 +3537,12 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
|
||||||
"http-range-header",
|
|
||||||
"httpdate",
|
|
||||||
"iri-string",
|
"iri-string",
|
||||||
"mime",
|
|
||||||
"mime_guess",
|
|
||||||
"percent-encoding",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
|||||||
+1
-2
@@ -46,10 +46,9 @@ tracing = "0.1.41"
|
|||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
governor = "0.10.1"
|
governor = "0.10.1"
|
||||||
once_cell = "1.21.3"
|
|
||||||
serde_path_to_error = "0.1.17"
|
serde_path_to_error = "0.1.17"
|
||||||
num-format = "0.4.4"
|
num-format = "0.4.4"
|
||||||
tower-http = { version = "0.6.0", features = ["fs", "cors", "trace", "timeout"] }
|
tower-http = { version = "0.6.0", features = ["cors", "trace", "timeout"] }
|
||||||
rust-embed = { version = "8.0", features = ["include-exclude"], optional = true }
|
rust-embed = { version = "8.0", features = ["include-exclude"], optional = true }
|
||||||
mime_guess = { version = "2.0", optional = true }
|
mime_guess = { version = "2.0", optional = true }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ impl DateRange {
|
|||||||
/// Get the number of weeks between start and end dates
|
/// Get the number of weeks between start and end dates
|
||||||
pub fn weeks_duration(&self) -> u32 {
|
pub fn weeks_duration(&self) -> u32 {
|
||||||
let duration = self.end.signed_duration_since(self.start);
|
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
|
/// Check if a specific date falls within this range
|
||||||
|
|||||||
+2
-1
@@ -168,8 +168,9 @@ impl SearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the maximum number of results to return
|
/// 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 {
|
pub fn max_results(mut self, max_results: i32) -> Self {
|
||||||
self.max_results = max_results;
|
self.max_results = max_results.clamp(1, 500);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-17
@@ -7,13 +7,12 @@ use cookie::Cookie;
|
|||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use governor::state::InMemoryState;
|
use governor::state::InMemoryState;
|
||||||
use governor::{Quota, RateLimiter};
|
use governor::{Quota, RateLimiter};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rand::distr::{Alphanumeric, SampleString};
|
use rand::distr::{Alphanumeric, SampleString};
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::{Mutex, Notify};
|
use tokio::sync::{Mutex, Notify};
|
||||||
use tracing::{debug, info, trace};
|
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,
|
// 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.
|
// 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>,
|
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.
|
/// Represents an active anonymous session within the Banner API.
|
||||||
/// Identified by multiple persistent cookies, as well as a client-generated "unique session ID".
|
/// Identified by multiple persistent cookies, as well as a client-generated "unique session ID".
|
||||||
@@ -63,16 +62,16 @@ pub fn nonce() -> String {
|
|||||||
|
|
||||||
impl BannerSession {
|
impl BannerSession {
|
||||||
/// Creates a new session
|
/// 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();
|
let now = Instant::now();
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
created_at: now,
|
created_at: now,
|
||||||
last_activity: None,
|
last_activity: None,
|
||||||
unique_session_id: unique_session_id.to_string(),
|
unique_session_id: unique_session_id.to_string(),
|
||||||
jsessionid: jsessionid.to_string(),
|
jsessionid: jsessionid.to_string(),
|
||||||
ssb_cookie: ssb_cookie.to_string(),
|
ssb_cookie: ssb_cookie.to_string(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the unique session ID
|
/// Returns the unique session ID
|
||||||
@@ -124,47 +123,46 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_session_returns_ok() {
|
fn test_new_session_creates_session() {
|
||||||
let session = BannerSession::new("sess-1", "JSID123", "SSB456");
|
let session = BannerSession::new("sess-1", "JSID123", "SSB456");
|
||||||
assert!(session.is_ok());
|
assert_eq!(session.id(), "sess-1");
|
||||||
assert_eq!(session.unwrap().id(), "sess-1");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fresh_session_not_expired() {
|
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());
|
assert!(!session.is_expired());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fresh_session_not_been_used() {
|
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());
|
assert!(!session.been_used());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_touch_marks_used() {
|
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();
|
session.touch();
|
||||||
assert!(session.been_used());
|
assert!(session.been_used());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_touched_session_not_expired() {
|
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();
|
session.touch();
|
||||||
assert!(!session.is_expired());
|
assert!(!session.is_expired());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cookie_format() {
|
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");
|
assert_eq!(session.cookie(), "JSESSIONID=JSID123; SSB_COOKIE=SSB456");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_id_returns_unique_session_id() {
|
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");
|
assert_eq!(session.id(), "my-unique-id");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +452,7 @@ impl SessionPool {
|
|||||||
self.select_term(&term.to_string(), &unique_session_id, &cookie_header)
|
self.select_term(&term.to_string(), &unique_session_id, &cookie_header)
|
||||||
.await?;
|
.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.
|
/// Retrieves a list of terms from the Banner API.
|
||||||
|
|||||||
+1
-1
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
/// Returns a browser-like user agent string.
|
/// Returns a browser-like user agent string.
|
||||||
pub fn user_agent() -> &'static str {
|
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
@@ -5,58 +5,24 @@ use crate::data::models::TargetType;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::fmt;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Errors that can occur during job parsing
|
/// Errors that can occur during job parsing
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum JobParseError {
|
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),
|
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
|
/// Errors that can occur during job processing
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub enum JobError {
|
pub enum JobError {
|
||||||
Recoverable(anyhow::Error), // API failures, network issues
|
#[error("Recoverable error: {0}")]
|
||||||
Unrecoverable(anyhow::Error), // Parse errors, corrupted data
|
Recoverable(#[source] anyhow::Error),
|
||||||
}
|
#[error("Unrecoverable error: {0}")]
|
||||||
|
Unrecoverable(#[source] anyhow::Error),
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Common trait interface for all job types
|
/// Common trait interface for all job types
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@
|
|||||||
//! at compile time using rust-embed.
|
//! at compile time using rust-embed.
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rapidhash::v3::rapidhash_v3;
|
use rapidhash::v3::rapidhash_v3;
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
/// Embedded web assets from the dist directory
|
/// Embedded web assets from the dist directory
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
@@ -65,7 +65,7 @@ impl AssetMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Global cache for asset metadata to avoid repeated calculations
|
/// 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
|
/// Get cached asset metadata for a file path, caching on-demand
|
||||||
/// Returns AssetMetadata containing MIME type and RapidHash hash
|
/// Returns AssetMetadata containing MIME type and RapidHash hash
|
||||||
|
|||||||
@@ -20,6 +20,26 @@
|
|||||||
animation: pulse 2s ease-in-out infinite;
|
animation: pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme toggle button */
|
||||||
|
.theme-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 7px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--gray-11);
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--gray-4);
|
||||||
|
}
|
||||||
|
|
||||||
/* Screen reader only text */
|
/* Screen reader only text */
|
||||||
.sr-only {
|
.sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from "next-themes";
|
|
||||||
import { Button } from "@radix-ui/themes";
|
import { Button } from "@radix-ui/themes";
|
||||||
import { Sun, Moon, Monitor } from "lucide-react";
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
@@ -28,31 +28,7 @@ export function ThemeToggle() {
|
|||||||
}, [nextTheme]);
|
}, [nextTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button variant="ghost" size="3" onClick={() => setTheme(nextTheme)} className="theme-toggle">
|
||||||
variant="ghost"
|
|
||||||
size="3"
|
|
||||||
onClick={() => setTheme(nextTheme)}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "none",
|
|
||||||
margin: "4px",
|
|
||||||
padding: "7px",
|
|
||||||
borderRadius: "6px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: "var(--gray-11)",
|
|
||||||
transition: "background-color 0.2s, color 0.2s",
|
|
||||||
transform: "scale(1.25)",
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = "var(--gray-4)";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = "transparent";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
{icon}
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { BannerApiClient } from "./api";
|
import { BannerApiClient } from "./api";
|
||||||
|
|
||||||
// Mock fetch
|
// Mock fetch
|
||||||
@@ -31,12 +31,12 @@ describe("BannerApiClient", () => {
|
|||||||
|
|
||||||
it("should fetch status data", async () => {
|
it("should fetch status data", async () => {
|
||||||
const mockStatus = {
|
const mockStatus = {
|
||||||
status: "active",
|
status: "active" as const,
|
||||||
version: "0.3.4",
|
version: "0.3.4",
|
||||||
commit: "abc1234",
|
commit: "abc1234",
|
||||||
services: {
|
services: {
|
||||||
web: { name: "web", status: "active" },
|
web: { name: "web", status: "active" as const },
|
||||||
database: { name: "database", status: "connected" },
|
database: { name: "database", status: "connected" as const },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+29
-12
@@ -1,21 +1,21 @@
|
|||||||
|
import { Card, Flex, Skeleton, Text, Tooltip } from "@radix-ui/themes";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { client, type StatusResponse, type Status } from "../lib/api";
|
|
||||||
import { Card, Flex, Text, Tooltip, Skeleton } from "@radix-ui/themes";
|
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
Activity,
|
||||||
XCircle,
|
|
||||||
Clock,
|
|
||||||
Bot,
|
Bot,
|
||||||
|
CheckCircle,
|
||||||
|
Circle,
|
||||||
|
Clock,
|
||||||
Globe,
|
Globe,
|
||||||
Hourglass,
|
Hourglass,
|
||||||
Activity,
|
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Circle,
|
|
||||||
WifiOff,
|
WifiOff,
|
||||||
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import TimeAgo from "react-timeago";
|
import TimeAgo from "react-timeago";
|
||||||
import { ThemeToggle } from "../components/ThemeToggle";
|
import { ThemeToggle } from "../components/ThemeToggle";
|
||||||
|
import { type Status, type StatusResponse, client } from "../lib/api";
|
||||||
import "../App.css";
|
import "../App.css";
|
||||||
|
|
||||||
const REFRESH_INTERVAL = import.meta.env.DEV ? 3000 : 30000;
|
const REFRESH_INTERVAL = import.meta.env.DEV ? 3000 : 30000;
|
||||||
@@ -190,20 +190,29 @@ function App() {
|
|||||||
const shouldShowLastFetch = hasResponse || hasError || hasTimeout;
|
const shouldShowLastFetch = hasResponse || hasError || hasTimeout;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeoutId: NodeJS.Timeout;
|
let timeoutId: NodeJS.Timeout | null = null;
|
||||||
|
let requestTimeoutId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Create a timeout promise
|
// Create a timeout promise with cleanup tracking
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error("Request timeout")), REQUEST_TIMEOUT);
|
requestTimeoutId = setTimeout(() => {
|
||||||
|
reject(new Error("Request timeout"));
|
||||||
|
}, REQUEST_TIMEOUT);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Race between the API call and timeout
|
// Race between the API call and timeout
|
||||||
const statusData = await Promise.race([client.getStatus(), timeoutPromise]);
|
const statusData = await Promise.race([client.getStatus(), timeoutPromise]);
|
||||||
|
|
||||||
|
// Clear the timeout if the request succeeded
|
||||||
|
if (requestTimeoutId) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
requestTimeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
const responseTime = endTime - startTime;
|
const responseTime = endTime - startTime;
|
||||||
|
|
||||||
@@ -214,9 +223,14 @@ function App() {
|
|||||||
lastFetch: new Date(),
|
lastFetch: new Date(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Clear the timeout on error as well
|
||||||
|
if (requestTimeoutId) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
requestTimeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = err instanceof Error ? err.message : "Failed to fetch data";
|
const errorMessage = err instanceof Error ? err.message : "Failed to fetch data";
|
||||||
|
|
||||||
// Check if it's a timeout error
|
|
||||||
if (errorMessage === "Request timeout") {
|
if (errorMessage === "Request timeout") {
|
||||||
setState({
|
setState({
|
||||||
mode: "timeout",
|
mode: "timeout",
|
||||||
@@ -241,6 +255,9 @@ function App() {
|
|||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
if (requestTimeoutId) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user