diff --git a/faze-server/src/routes/dto.rs b/faze-server/src/routes/dto.rs index 5309012..0254529 100644 --- a/faze-server/src/routes/dto.rs +++ b/faze-server/src/routes/dto.rs @@ -22,6 +22,8 @@ pub struct ListTracesQuery { pub struct ListLogsQuery { /// Filter by service name pub service: Option, + /// Return only logs correlated with this trace (ignores other filters) + pub trace_id: Option, /// Maximum number of results pub limit: Option, } @@ -81,3 +83,83 @@ impl From<&faze::Trace> for TraceInfo { } } } + +/// Response for the aggregate stats endpoint +#[derive(Debug, Serialize)] +pub struct StatsResponse { + /// Total number of stored spans. + pub spans: i64, + /// Total number of stored logs. + pub logs: i64, + /// Total number of stored metrics. + pub metrics: i64, + /// Global trace aggregates. + pub traces: TraceStatsDto, + /// Per-service trace counts, most active first. + pub services: Vec, + /// Time-bucketed trace activity, oldest bucket first. + pub activity: Vec, +} + +/// Global trace aggregates. +#[derive(Debug, Serialize)] +pub struct TraceStatsDto { + /// Total number of distinct traces. + pub total: i64, + /// Number of traces containing at least one error span. + pub errors: i64, + /// Mean trace duration in milliseconds. + pub avg_duration_ms: f64, +} + +impl From for TraceStatsDto { + fn from(stats: faze::storage::TraceStats) -> Self { + Self { + total: stats.total, + errors: stats.errors, + avg_duration_ms: stats.avg_duration_ms, + } + } +} + +/// Per-service trace counts. +#[derive(Debug, Serialize)] +pub struct ServiceStatDto { + /// Service name. + pub name: String, + /// Number of traces attributed to the service. + pub trace_count: i64, + /// Number of those traces containing at least one error span. + pub error_count: i64, +} + +impl From for ServiceStatDto { + fn from(stat: faze::storage::ServiceStat) -> Self { + Self { + name: stat.name, + trace_count: stat.trace_count, + error_count: stat.error_count, + } + } +} + +/// One time bucket of trace activity. +#[derive(Debug, Serialize)] +pub struct TimeBucketDto { + /// Bucket start as nanoseconds since the Unix epoch. + pub bucket_start_unix_nano: i64, + /// Number of traces starting in this bucket. + pub total: i64, + /// Number of those traces containing at least one error span. + pub errors: i64, +} + +impl From for TimeBucketDto { + fn from(bucket: faze::storage::TraceTimeBucket) -> Self { + Self { + bucket_start_unix_nano: bucket.bucket_start_unix_nano, + total: bucket.total, + errors: bucket.errors, + } + } +} diff --git a/faze-server/src/routes/handlers.rs b/faze-server/src/routes/handlers.rs index 46b49bb..0ccf2e3 100644 --- a/faze-server/src/routes/handlers.rs +++ b/faze-server/src/routes/handlers.rs @@ -1,7 +1,9 @@ //! The API route handlers. use super::AppState; -use super::dto::{ListLogsQuery, ListMetricsQuery, ListTracesQuery, TraceInfo, TraceListResponse}; +use super::dto::{ + ListLogsQuery, ListMetricsQuery, ListTracesQuery, StatsResponse, TraceInfo, TraceListResponse, +}; use super::error::ApiError; use axum::{ Json, @@ -74,6 +76,11 @@ pub async fn list_logs( ) -> Result { info!("GET /api/logs - params: {:?}", params); + if let Some(trace_id) = params.trace_id.as_deref() { + let logs = state.storage.get_logs_by_trace_id(trace_id)?; + return Ok(Json(logs)); + } + let limit = params.limit.unwrap_or(100).min(1000); let logs = state .storage @@ -114,6 +121,34 @@ pub async fn list_metrics( }))) } +/// Number of buckets returned by the stats activity timeline. +const ACTIVITY_BUCKETS: usize = 30; + +/// GET /api/stats - Aggregate statistics across all stored telemetry +pub async fn get_stats(State(state): State) -> Result { + info!("GET /api/stats"); + + let storage = &state.storage; + let response = StatsResponse { + spans: storage.count_spans()?, + logs: storage.count_logs()?, + metrics: storage.count_metrics()?, + traces: storage.trace_stats()?.into(), + services: storage + .service_stats()? + .into_iter() + .map(Into::into) + .collect(), + activity: storage + .trace_time_buckets(ACTIVITY_BUCKETS)? + .into_iter() + .map(Into::into) + .collect(), + }; + + Ok(Json(response)) +} + /// GET /health - Health check endpoint pub async fn health_check() -> impl IntoResponse { Json(serde_json::json!({ @@ -386,6 +421,56 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + #[tokio::test] + async fn test_get_stats_empty() { + let storage = Storage::new_in_memory().unwrap(); + let state = AppState { + storage: Arc::new(storage), + }; + + let response = get_stats(State(state)).await.into_response(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn test_get_stats_with_data() { + let storage = Storage::new_in_memory().unwrap(); + let ok_span = Span::new( + "span1".to_string(), + "trace1".to_string(), + None, + "ok operation".to_string(), + SpanKind::Server, + 1_000_000_000, + 2_000_000_000, + Attributes::new(), + Status::ok(), + Some("service-a".to_string()), + ); + let error_span = Span::new( + "span2".to_string(), + "trace2".to_string(), + None, + "failing operation".to_string(), + SpanKind::Server, + 3_000_000_000, + 4_000_000_000, + Attributes::new(), + Status::error("boom"), + Some("service-b".to_string()), + ); + storage.insert_spans(&[ok_span, error_span]).unwrap(); + + let state = AppState { + storage: Arc::new(storage), + }; + + let response = get_stats(State(state)).await.into_response(); + + assert_eq!(response.status(), StatusCode::OK); + } + #[tokio::test] async fn test_list_logs_empty() { let storage = Storage::new_in_memory().unwrap(); @@ -395,6 +480,39 @@ mod tests { let query = ListLogsQuery { service: None, + trace_id: None, + limit: None, + }; + + let response = list_logs(State(state), Query(query)).await.into_response(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn test_list_logs_by_trace_id() { + use faze::models::{Log, SeverityLevel}; + + let storage = Storage::new_in_memory().unwrap(); + let log = Log::new( + 1_000_000_000, + SeverityLevel::Info, + None, + "correlated log".to_string(), + Attributes::new(), + Some("trace1".to_string()), + Some("span1".to_string()), + Some("test-service".to_string()), + ); + storage.insert_log(&log).unwrap(); + + let state = AppState { + storage: Arc::new(storage), + }; + + let query = ListLogsQuery { + service: None, + trace_id: Some("trace1".to_string()), limit: None, }; diff --git a/faze-server/src/routes/mod.rs b/faze-server/src/routes/mod.rs index 3cca251..17637e1 100644 --- a/faze-server/src/routes/mod.rs +++ b/faze-server/src/routes/mod.rs @@ -5,7 +5,8 @@ mod error; mod handlers; pub use handlers::{ - get_project_info, get_trace, health_check, list_logs, list_metrics, list_services, list_traces, + get_project_info, get_stats, get_trace, health_check, list_logs, list_metrics, list_services, + list_traces, }; use faze::Storage; diff --git a/faze-server/src/server.rs b/faze-server/src/server.rs index 55ca196..5ef35ab 100644 --- a/faze-server/src/server.rs +++ b/faze-server/src/server.rs @@ -1,6 +1,6 @@ use crate::{ routes::{ - AppState, get_project_info, get_trace, health_check, list_logs, list_metrics, + AppState, get_project_info, get_stats, get_trace, health_check, list_logs, list_metrics, list_services, list_traces, }, ui, @@ -42,6 +42,7 @@ impl ApiServer { .route("/api/logs", get(list_logs)) .route("/api/services", get(list_services)) .route("/api/metrics", get(list_metrics)) + .route("/api/stats", get(get_stats)) .layer(CorsLayer::permissive()) .with_state(state) .fallback(ui::fallback_service()) @@ -72,6 +73,7 @@ impl ApiServer { " List services: http://localhost:{}/api/services", self.port ); + info!(" Stats: http://localhost:{}/api/stats", self.port); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app).await?; @@ -193,4 +195,37 @@ mod tests { assert_eq!(response.status(), StatusCode::NOT_FOUND); } + + #[tokio::test] + async fn test_stats_endpoint() { + let storage = Storage::new_in_memory().unwrap(); + let span = Span::new( + "span1".to_string(), + "trace1".to_string(), + None, + "test".to_string(), + SpanKind::Server, + 1_000_000_000, + 2_000_000_000, + Attributes::new(), + Status::ok(), + Some("test-service".to_string()), + ); + storage.insert_span(&span).unwrap(); + + let server = ApiServer::new(storage, 0); + let app = server.build_router(); + + let response = app + .oneshot( + Request::builder() + .uri("/api/stats") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } } diff --git a/faze/src/storage/logs.rs b/faze/src/storage/logs.rs index 35ca0f7..2f44ba2 100644 --- a/faze/src/storage/logs.rs +++ b/faze/src/storage/logs.rs @@ -63,6 +63,25 @@ impl Storage { Ok(logs) } + /// Get all logs correlated with a trace, oldest first + #[allow(clippy::significant_drop_tightening)] + pub fn get_logs_by_trace_id(&self, trace_id: &str) -> Result> { + let conn = self.lock()?; + let mut stmt = conn.prepare( + "SELECT time_unix_nano, severity_level, severity_text, body, + attributes, trace_id, span_id, service_name + FROM logs + WHERE trace_id = ?1 + ORDER BY time_unix_nano", + )?; + + let logs = stmt + .query_map([trace_id], log_from_row)? + .collect::>>()?; + + Ok(logs) + } + /// Get count of logs pub fn count_logs(&self) -> Result { self.count_rows("logs") @@ -94,4 +113,38 @@ mod tests { assert_eq!(logs.len(), 1); assert_eq!(logs[0].body, "Test log"); } + + #[test] + fn test_get_logs_by_trace_id() { + let storage = Storage::new_in_memory().unwrap(); + let make_log = |time: i64, body: &str, trace_id: Option<&str>| { + Log::new( + time, + SeverityLevel::Info, + None, + body.to_string(), + Attributes::new(), + trace_id.map(String::from), + None, + Some("test-service".to_string()), + ) + }; + + storage + .insert_logs(&[ + make_log(3_000, "late", Some("trace1")), + make_log(1_000, "early", Some("trace1")), + make_log(2_000, "other trace", Some("trace2")), + make_log(4_000, "uncorrelated", None), + ]) + .unwrap(); + + let logs = storage.get_logs_by_trace_id("trace1").unwrap(); + + assert_eq!(logs.len(), 2); + assert_eq!(logs[0].body, "early"); + assert_eq!(logs[1].body, "late"); + + assert!(storage.get_logs_by_trace_id("missing").unwrap().is_empty()); + } } diff --git a/faze/src/storage/mod.rs b/faze/src/storage/mod.rs index e8df75a..29102e2 100644 --- a/faze/src/storage/mod.rs +++ b/faze/src/storage/mod.rs @@ -6,6 +6,7 @@ mod metrics; mod rows; mod schema; mod spans; +mod stats; use rusqlite::Connection; use std::path::Path; @@ -14,6 +15,7 @@ use thiserror::Error; pub use db_path::{detect_project_root, get_data_dir, get_project_db_path}; use schema::init_schema; +pub use stats::{ServiceStat, TraceStats, TraceTimeBucket}; /// Errors returned by [`Storage`] operations. #[derive(Debug, Error)] diff --git a/faze/src/storage/stats.rs b/faze/src/storage/stats.rs new file mode 100644 index 0000000..cf53412 --- /dev/null +++ b/faze/src/storage/stats.rs @@ -0,0 +1,313 @@ +//! Aggregate statistics over traces: global totals, per-service counts, +//! and time-bucketed activity. + +use super::{Result, Storage}; +use rusqlite::{Result as SqliteResult, params}; + +/// Per-trace rollup shared by every stats query: one row per trace with its +/// start time, duration, error flag, and a representative service name. +const TRACE_ROLLUP: &str = "SELECT trace_id, + MIN(start_time_unix_nano) AS st, + MAX(end_time_unix_nano) - MIN(start_time_unix_nano) AS dur, + MAX(CASE WHEN json_extract(status, '$.code') = 'ERROR' THEN 1 ELSE 0 END) AS has_err, + MIN(service_name) AS service_name + FROM spans + GROUP BY trace_id"; + +/// Global trace aggregates. +#[derive(Debug, Clone, PartialEq)] +pub struct TraceStats { + /// Total number of distinct traces. + pub total: i64, + /// Number of traces containing at least one error span. + pub errors: i64, + /// Mean trace duration in milliseconds. + pub avg_duration_ms: f64, +} + +/// Per-service trace counts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServiceStat { + /// Service name. + pub name: String, + /// Number of traces attributed to the service. + pub trace_count: i64, + /// Number of those traces containing at least one error span. + pub error_count: i64, +} + +/// One time bucket of trace activity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TraceTimeBucket { + /// Bucket start as nanoseconds since the Unix epoch. + pub bucket_start_unix_nano: i64, + /// Number of traces starting in this bucket. + pub total: i64, + /// Number of those traces containing at least one error span. + pub errors: i64, +} + +impl Storage { + /// Global trace count, error count, and average duration. + #[allow(clippy::significant_drop_tightening)] + pub fn trace_stats(&self) -> Result { + let conn = self.lock()?; + let stats = conn.query_row( + &format!( + "SELECT COUNT(*), + COALESCE(SUM(has_err), 0), + COALESCE(AVG(dur / 1000000.0), 0.0) + FROM ({TRACE_ROLLUP})" + ), + [], + |row| { + Ok(TraceStats { + total: row.get(0)?, + errors: row.get(1)?, + avg_duration_ms: row.get(2)?, + }) + }, + )?; + Ok(stats) + } + + /// Trace and error counts per service, most active first. + #[allow(clippy::significant_drop_tightening)] + pub fn service_stats(&self) -> Result> { + let conn = self.lock()?; + let mut stmt = conn.prepare(&format!( + "SELECT service_name, COUNT(*), COALESCE(SUM(has_err), 0) + FROM ({TRACE_ROLLUP}) + WHERE service_name IS NOT NULL + GROUP BY service_name + ORDER BY COUNT(*) DESC, service_name" + ))?; + + let stats = stmt + .query_map([], |row| { + Ok(ServiceStat { + name: row.get(0)?, + trace_count: row.get(1)?, + error_count: row.get(2)?, + }) + })? + .collect::>>()?; + + Ok(stats) + } + + /// Trace activity grouped into `bucket_count` equal time buckets spanning + /// the full range of trace start times. Buckets without traces are + /// zero-filled; an empty database yields an empty `Vec`. + #[allow(clippy::significant_drop_tightening)] + pub fn trace_time_buckets(&self, bucket_count: usize) -> Result> { + if bucket_count == 0 { + return Ok(Vec::new()); + } + + let conn = self.lock()?; + let range: (Option, Option) = conn.query_row( + &format!("SELECT MIN(st), MAX(st) FROM ({TRACE_ROLLUP})"), + [], + |row| Ok((row.get(0)?, row.get(1)?)), + )?; + + let (Some(min_start), Some(max_start)) = range else { + return Ok(Vec::new()); + }; + + let bucket_count = i64::try_from(bucket_count).unwrap_or(i64::MAX); + // Width chosen so the latest trace still lands in the last bucket. + let width = (max_start - min_start) / bucket_count + 1; + + let mut buckets: Vec = (0..bucket_count) + .map(|i| TraceTimeBucket { + bucket_start_unix_nano: min_start + i * width, + total: 0, + errors: 0, + }) + .collect(); + + let mut stmt = conn.prepare(&format!( + "SELECT (st - ?1) / ?2 AS bucket, COUNT(*), COALESCE(SUM(has_err), 0) + FROM ({TRACE_ROLLUP}) + GROUP BY bucket" + ))?; + + let rows = stmt.query_map(params![min_start, width], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, i64>(1)?, + row.get::<_, i64>(2)?, + )) + })?; + + for row in rows { + let (index, total, errors) = row?; + if let Ok(index) = usize::try_from(index) + && let Some(bucket) = buckets.get_mut(index) + { + bucket.total = total; + bucket.errors = errors; + } + } + + Ok(buckets) + } +} + +#[cfg(test)] +#[allow(clippy::float_cmp)] +mod tests { + use super::*; + use crate::models::{Attributes, Span, SpanKind, Status}; + + fn create_span( + span_id: &str, + trace_id: &str, + start: i64, + end: i64, + status: Status, + service: &str, + ) -> Span { + Span::new( + span_id.to_string(), + trace_id.to_string(), + None, + "test-operation".to_string(), + SpanKind::Server, + start, + end, + Attributes::new(), + status, + Some(service.to_string()), + ) + } + + #[test] + fn test_trace_stats_empty() { + let storage = Storage::new_in_memory().unwrap(); + let stats = storage.trace_stats().unwrap(); + + assert_eq!(stats.total, 0); + assert_eq!(stats.errors, 0); + assert_eq!(stats.avg_duration_ms, 0.0); + } + + #[test] + fn test_trace_stats_counts_and_avg() { + let storage = Storage::new_in_memory().unwrap(); + // trace1: two spans, 100ms total, ok + storage + .insert_spans(&[ + create_span( + "s1", + "trace1", + 1_000_000_000, + 1_050_000_000, + Status::ok(), + "svc-a", + ), + create_span( + "s2", + "trace1", + 1_020_000_000, + 1_100_000_000, + Status::ok(), + "svc-a", + ), + ]) + .unwrap(); + // trace2: one span, 300ms, error + storage + .insert_spans(&[create_span( + "s3", + "trace2", + 2_000_000_000, + 2_300_000_000, + Status::error("boom"), + "svc-b", + )]) + .unwrap(); + + let stats = storage.trace_stats().unwrap(); + + assert_eq!(stats.total, 2); + assert_eq!(stats.errors, 1); + assert_eq!(stats.avg_duration_ms, 200.0); + } + + #[test] + fn test_service_stats() { + let storage = Storage::new_in_memory().unwrap(); + storage + .insert_spans(&[ + create_span("s1", "trace1", 1_000, 2_000, Status::ok(), "svc-a"), + create_span("s2", "trace2", 1_000, 2_000, Status::error("x"), "svc-a"), + create_span("s3", "trace3", 1_000, 2_000, Status::ok(), "svc-b"), + ]) + .unwrap(); + + let stats = storage.service_stats().unwrap(); + + assert_eq!(stats.len(), 2); + assert_eq!(stats[0].name, "svc-a"); + assert_eq!(stats[0].trace_count, 2); + assert_eq!(stats[0].error_count, 1); + assert_eq!(stats[1].name, "svc-b"); + assert_eq!(stats[1].trace_count, 1); + assert_eq!(stats[1].error_count, 0); + } + + #[test] + fn test_trace_time_buckets_empty() { + let storage = Storage::new_in_memory().unwrap(); + let buckets = storage.trace_time_buckets(30).unwrap(); + assert!(buckets.is_empty()); + } + + #[test] + fn test_trace_time_buckets_distribution() { + let storage = Storage::new_in_memory().unwrap(); + // Two traces at the start of the range, one error at the end. + storage + .insert_spans(&[ + create_span("s1", "trace1", 0, 1_000, Status::ok(), "svc"), + create_span("s2", "trace2", 100, 1_000, Status::ok(), "svc"), + create_span("s3", "trace3", 10_000, 11_000, Status::error("x"), "svc"), + ]) + .unwrap(); + + let buckets = storage.trace_time_buckets(10).unwrap(); + + assert_eq!(buckets.len(), 10); + assert_eq!(buckets.iter().map(|b| b.total).sum::(), 3); + assert_eq!(buckets.iter().map(|b| b.errors).sum::(), 1); + assert_eq!(buckets[0].bucket_start_unix_nano, 0); + // First bucket holds the two early traces, last non-empty one the error. + assert_eq!(buckets[0].total, 2); + assert_eq!(buckets.last().unwrap().errors + buckets[8].errors, 1); + } + + #[test] + fn test_trace_time_buckets_single_trace() { + let storage = Storage::new_in_memory().unwrap(); + storage + .insert_spans(&[create_span( + "s1", + "trace1", + 5_000, + 6_000, + Status::ok(), + "svc", + )]) + .unwrap(); + + let buckets = storage.trace_time_buckets(5).unwrap(); + + assert_eq!(buckets.len(), 5); + assert_eq!(buckets[0].bucket_start_unix_nano, 5_000); + assert_eq!(buckets[0].total, 1); + assert_eq!(buckets.iter().map(|b| b.total).sum::(), 1); + } +} diff --git a/ui/bun.lock b/ui/bun.lock index 50202b9..0f7f77d 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -10,19 +10,14 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@shadcn/ui": "^0.0.4", "@tailwindcss/vite": "^4.1.17", - "@tanstack/react-form": "^1.27.0", "@tanstack/react-query": "^5.90.11", "@tanstack/react-router": "^1.139.10", - "@tanstack/react-table": "^8.21.3", - "@tremor/react": "^3.18.7", "@vitejs/plugin-react": "^5.1.1", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "framer-motion": "^12.23.24", "lucide-react": "^0.555.0", "nuqs": "^2.8.2", "react": "^19.2.0", @@ -30,7 +25,6 @@ "recharts": "2.15.4", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", - "zod": "^4.1.13", }, "devDependencies": { "@tanstack/router-plugin": "^1.139.10", @@ -40,7 +34,6 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react-swc": "^4.2.2", "@vitest/ui": "^4.0.15", "globals": "^16.5.0", "happy-dom": "^20.0.11", @@ -221,14 +214,10 @@ "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], - "@floating-ui/react": ["@floating-ui/react@0.19.2", "", { "dependencies": { "@floating-ui/react-dom": "^1.3.0", "aria-hidden": "^1.1.3", "tabbable": "^6.0.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@headlessui/react": ["@headlessui/react@2.2.0", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.17.1", "@react-aria/interactions": "^3.21.3", "@tanstack/react-virtual": "^3.8.1" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ=="], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -317,20 +306,6 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@react-aria/focus": ["@react-aria/focus@3.21.2", "", { "dependencies": { "@react-aria/interactions": "^3.25.6", "@react-aria/utils": "^3.31.0", "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ=="], - - "@react-aria/interactions": ["@react-aria/interactions@3.25.6", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.31.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw=="], - - "@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="], - - "@react-aria/utils": ["@react-aria/utils@3.31.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.10.8", "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig=="], - - "@react-stately/flags": ["@react-stately/flags@3.1.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="], - - "@react-stately/utils": ["@react-stately/utils@3.10.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g=="], - - "@react-types/shared": ["@react-types/shared@3.32.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="], @@ -361,38 +336,8 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], - "@shadcn/ui": ["@shadcn/ui@0.0.4", "", { "dependencies": { "chalk": "5.2.0", "commander": "^10.0.0", "execa": "^7.0.0", "fs-extra": "^11.1.0", "node-fetch": "^3.3.0", "ora": "^6.1.2", "prompts": "^2.4.2", "zod": "^3.20.2" }, "bin": { "ui": "dist/index.js" } }, "sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - "@swc/core": ["@swc/core@1.15.3", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.3", "@swc/core-darwin-x64": "1.15.3", "@swc/core-linux-arm-gnueabihf": "1.15.3", "@swc/core-linux-arm64-gnu": "1.15.3", "@swc/core-linux-arm64-musl": "1.15.3", "@swc/core-linux-x64-gnu": "1.15.3", "@swc/core-linux-x64-musl": "1.15.3", "@swc/core-win32-arm64-msvc": "1.15.3", "@swc/core-win32-ia32-msvc": "1.15.3", "@swc/core-win32-x64-msvc": "1.15.3" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q=="], - - "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ=="], - - "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A=="], - - "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg=="], - - "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw=="], - - "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g=="], - - "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.3", "", { "os": "linux", "cpu": "x64" }, "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A=="], - - "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.3", "", { "os": "linux", "cpu": "x64" }, "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug=="], - - "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA=="], - - "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw=="], - - "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.3", "", { "os": "win32", "cpu": "x64" }, "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog=="], - - "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], - - "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - - "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="], @@ -423,28 +368,16 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="], - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], - - "@tanstack/form-core": ["@tanstack/form-core@1.27.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.5", "@tanstack/pacer": "^0.15.3", "@tanstack/store": "^0.7.7" } }, "sha512-QFEhg9/VcrwtpbcN7Qpl8JVVfEm2UJ+dzfDFGGMYub2J9jsgrp2HmaY7LSLlnkpTJlCIDxQiWDkiOFYQtK6yzw=="], - "@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - "@tanstack/pacer": ["@tanstack/pacer@0.15.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.2", "@tanstack/store": "^0.7.5" } }, "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="], - "@tanstack/react-form": ["@tanstack/react-form@1.27.0", "", { "dependencies": { "@tanstack/form-core": "1.27.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7MBOtvjlUwkGpvA9TIOs3YdLoyfJWZYtxuAQIdkLDZ9HLrRaRbxWQIZ2H6sRVA35sPvx6uiQMunGHOPKip5AZA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="], "@tanstack/react-router": ["@tanstack/react-router@1.139.10", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.10", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-SVEH2n38XPtQSbW3BgKpK8G1GdLzbsmo3B/epfjuRk2XlYkSFjj7P8DIoHKOgkaCh3T0hKh/CEEj+D130YmGUw=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], - "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], - - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], - "@tanstack/router-core": ["@tanstack/router-core@1.139.10", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-gougqlYumNOn98d2ZhyoRJTNT8RvFip97z6T2T3/JTPrErwOsKaIA2FwlkfLJmJY1JQtUuF38IREJdfQrTJiqg=="], "@tanstack/router-generator": ["@tanstack/router-generator@1.139.10", "", { "dependencies": { "@tanstack/router-core": "1.139.10", "@tanstack/router-utils": "1.139.0", "@tanstack/virtual-file-routes": "1.139.0", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-Uo0xmz6w1Ayv1AMyWLsT0ngXmjB8yAKv5khOaci/ZxAZNyvz3t84jqI7XXlG9fwtDRdTF4G/qBmXlPEmPk6Wfg=="], @@ -455,10 +388,6 @@ "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], - "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.139.0", "", {}, "sha512-9PImF1d1tovTUIpjFVa0W7Fwj/MHif7BaaczgJJfbv3sDt1Gh+oW9W9uCw9M3ndEJynnp5ZD/TTs0RGubH5ssg=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -469,8 +398,6 @@ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], - "@tremor/react": ["@tremor/react@3.18.7", "", { "dependencies": { "@floating-ui/react": "^0.19.2", "@headlessui/react": "2.2.0", "date-fns": "^3.6.0", "react-day-picker": "^8.10.1", "react-transition-state": "^2.1.2", "recharts": "^2.13.3", "tailwind-merge": "^2.5.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": ">=16.6.0" } }, "sha512-nmqvf/1m0GB4LXc7v2ftdfSLoZhy5WLrhV6HNf0SOriE6/l8WkYeWuhQq8QsBjRi94mUIKLJ/VC3/Y/pj6VubQ=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -539,8 +466,6 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="], - "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.2.2", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.47", "@swc/core": "^1.13.5" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA=="], - "@vitest/expect": ["@vitest/expect@4.0.15", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.15", "@vitest/utils": "4.0.15", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w=="], "@vitest/mocker": ["@vitest/mocker@4.0.15", "", { "dependencies": { "@vitest/spy": "4.0.15", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ=="], @@ -591,24 +516,18 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="], - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -617,18 +536,12 @@ "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], - "chalk": ["chalk@5.2.0", "", {}, "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], @@ -639,8 +552,6 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -679,12 +590,8 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], - "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], @@ -693,8 +600,6 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -755,8 +660,6 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - "execa": ["execa@7.2.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA=="], - "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -769,8 +672,6 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -787,12 +688,6 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="], - - "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -805,8 +700,6 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -835,12 +728,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -849,8 +738,6 @@ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], @@ -859,16 +746,10 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - - "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - "isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -891,12 +772,8 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], @@ -929,8 +806,6 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "log-symbols": ["log-symbols@5.1.0", "", { "dependencies": { "chalk": "^5.0.0", "is-unicode-supported": "^1.1.0" } }, "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], @@ -945,22 +820,14 @@ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], - "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], - - "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -969,28 +836,18 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], - "nuqs": ["nuqs@2.8.2", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-KMb6gmUJaLVRw+SbKUmBTo0IWLGU2s1Z4Iz/N64+EIDcu6Iw51CuppgKmxZR2EW3iXaOz5LF4avGKD2wq45eqg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ora": ["ora@6.3.1", "", { "dependencies": { "chalk": "^5.0.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.6.1", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.1.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "strip-ansi": "^7.0.1", "wcwidth": "^1.0.1" } }, "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -1017,8 +874,6 @@ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -1027,8 +882,6 @@ "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], - "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -1045,10 +898,6 @@ "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], - "react-transition-state": ["react-transition-state@2.3.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-Z48el73x+7HUEM131dof9YpcQ5IlM4xB+pKWH/lX3FhxGfQaNTZa16zb7pWkC/y5btTZzXfCtglIJEGc57giOw=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], @@ -1065,12 +914,8 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], - "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], @@ -1089,12 +934,8 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1103,14 +944,6 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], - "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -1119,8 +952,6 @@ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - "tabbable": ["tabbable@6.3.0", "", {}, "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="], - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], @@ -1167,8 +998,6 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], @@ -1181,8 +1010,6 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], "vite": ["rolldown-vite@7.2.5", "", { "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.50", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA=="], @@ -1191,10 +1018,6 @@ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "webidl-conversions": ["webidl-conversions@8.0.0", "", {}, "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], @@ -1221,7 +1044,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -1235,10 +1058,6 @@ "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@floating-ui/react/@floating-ui/react-dom": ["@floating-ui/react-dom@1.3.0", "", { "dependencies": { "@floating-ui/dom": "^1.2.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g=="], - - "@headlessui/react/@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1249,8 +1068,6 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@shadcn/ui/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], @@ -1263,27 +1080,17 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/form-core/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], - - "@tanstack/pacer/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], - - "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@tremor/react/tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -1295,8 +1102,6 @@ "jsdom/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -1305,22 +1110,14 @@ "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="], - "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], } } diff --git a/ui/index.html b/ui/index.html index b3e313f..cb96666 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1,5 +1,5 @@ - + diff --git a/ui/package.json b/ui/package.json index f293fb0..6dfd8f1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,27 +18,21 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@shadcn/ui": "^0.0.4", "@tailwindcss/vite": "^4.1.17", - "@tanstack/react-form": "^1.27.0", "@tanstack/react-query": "^5.90.11", "@tanstack/react-router": "^1.139.10", - "@tanstack/react-table": "^8.21.3", - "@tremor/react": "^3.18.7", "@vitejs/plugin-react": "^5.1.1", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "framer-motion": "^12.23.24", "lucide-react": "^0.555.0", "nuqs": "^2.8.2", "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": "2.15.4", "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.17", - "zod": "^4.1.13" + "tailwindcss": "^4.1.17" }, "devDependencies": { "@tanstack/router-plugin": "^1.139.10", @@ -48,7 +42,6 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react-swc": "^4.2.2", "@vitest/ui": "^4.0.15", "globals": "^16.5.0", "happy-dom": "^20.0.11", diff --git a/ui/src/components/layout/theme-toggle.tsx b/ui/src/components/layout/theme-toggle.tsx index 338f70c..d98cff2 100644 --- a/ui/src/components/layout/theme-toggle.tsx +++ b/ui/src/components/layout/theme-toggle.tsx @@ -1,13 +1,9 @@ -import { useState, useEffect } from "react"; -import { getTheme, toggleTheme, applyTheme } from "@/lib/theme"; +import { useState } from "react"; +import { getTheme, toggleTheme } from "@/lib/theme"; export function ThemeToggle() { const [theme, setThemeState] = useState(getTheme()); - useEffect(() => { - applyTheme(theme); - }, [theme]); - const handleToggle = () => { const next = toggleTheme(); setThemeState(next); @@ -18,6 +14,7 @@ export function ThemeToggle() { onClick={handleToggle} className="text-xs border border-border px-2 py-1 hover:bg-card transition-colors font-mono" title="Toggle theme" + aria-label="Toggle theme" > {theme === "dark" ? "☀" : "☾"} diff --git a/ui/src/components/layout/top-bar.tsx b/ui/src/components/layout/top-bar.tsx index cecb3ff..869fa53 100644 --- a/ui/src/components/layout/top-bar.tsx +++ b/ui/src/components/layout/top-bar.tsx @@ -1,4 +1,5 @@ import { useProject } from "@/hooks/api"; +import { ThemeToggle } from "./theme-toggle"; export function Topbar() { const { data: project } = useProject(); @@ -22,7 +23,8 @@ export function Topbar() {
-
Cltr+K
+
Ctrl+K
+
); diff --git a/ui/src/components/shared/duration-badge.tsx b/ui/src/components/shared/duration-badge.tsx index b5b7a4f..38e60ff 100644 --- a/ui/src/components/shared/duration-badge.tsx +++ b/ui/src/components/shared/duration-badge.tsx @@ -1,4 +1,5 @@ import { formatDurationCompact } from "@/lib/formatters"; +import { DURATION_FAST_MS, DURATION_SLOW_MS } from "@/lib/constants"; interface DurationBadgeProps { durationMs: number; @@ -6,9 +7,10 @@ interface DurationBadgeProps { export function DurationBadge({ durationMs }: DurationBadgeProps) { const getColor = () => { - if (durationMs < 100) return "text-green-500 bg-green-500/10"; - if (durationMs < 500) return "text-yellow-500 bg-yellow-500/10"; - return "text-red-500 bg-red-500/10"; + if (durationMs < DURATION_FAST_MS) return "text-green-500 bg-green-500/10"; + if (durationMs < DURATION_SLOW_MS) + return "text-yellow-500 bg-yellow-500/10"; + return "text-destructive bg-destructive/10"; }; return ( diff --git a/ui/src/components/shared/query-boundary.tsx b/ui/src/components/shared/query-boundary.tsx new file mode 100644 index 0000000..48e01ed --- /dev/null +++ b/ui/src/components/shared/query-boundary.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; +import { LoadingState } from "./loading-state"; +import { ErrorState } from "./error-state"; + +interface QueryBoundaryProps { + isLoading: boolean; + error?: unknown; + onRetry?: () => void; + isEmpty?: boolean; + emptyMessage?: string; + loadingFallback?: ReactNode; + children: ReactNode; +} + +export function QueryBoundary({ + isLoading, + error, + onRetry, + isEmpty, + emptyMessage = "No data to display", + loadingFallback, + children, +}: QueryBoundaryProps) { + if (isLoading) { + return <>{loadingFallback ?? }; + } + + if (error) { + return ( + + ); + } + + if (isEmpty) { + return ( +
+

{emptyMessage}

+
+ ); + } + + return <>{children}; +} diff --git a/ui/src/components/shared/skeleton.tsx b/ui/src/components/shared/skeleton.tsx new file mode 100644 index 0000000..32afb58 --- /dev/null +++ b/ui/src/components/shared/skeleton.tsx @@ -0,0 +1,5 @@ +import { cn } from "@/lib/utils"; + +export function Skeleton({ className }: { className?: string }) { + return
; +} diff --git a/ui/src/components/shared/status-badge.tsx b/ui/src/components/shared/status-badge.tsx index 29b6516..9560d5f 100644 --- a/ui/src/components/shared/status-badge.tsx +++ b/ui/src/components/shared/status-badge.tsx @@ -1,6 +1,5 @@ import type { StatusCode } from "@/types"; import { statusBgColors, statusColors } from "@/lib/colors"; -import { STATUS_CODE_MAP } from "@/lib/constants"; interface StatusBadgeProps { status: StatusCode; @@ -11,7 +10,7 @@ export function StatusBadge({ status }: StatusBadgeProps) { - {STATUS_CODE_MAP[status]} + {status} ); } diff --git a/ui/src/features/dashboard/components/dashboard-skeleton.tsx b/ui/src/features/dashboard/components/dashboard-skeleton.tsx new file mode 100644 index 0000000..4118866 --- /dev/null +++ b/ui/src/features/dashboard/components/dashboard-skeleton.tsx @@ -0,0 +1,20 @@ +import { Skeleton } from "@/components/shared/skeleton"; + +export function DashboardSkeleton() { + return ( +
+
+ {Array.from({ length: 4 }, (_, i) => ( + + ))} +
+ +
+ + +
+ + +
+ ); +} diff --git a/ui/src/features/dashboard/components/recent-traces.tsx b/ui/src/features/dashboard/components/recent-traces.tsx index 6ee08a2..14e5ff7 100644 --- a/ui/src/features/dashboard/components/recent-traces.tsx +++ b/ui/src/features/dashboard/components/recent-traces.tsx @@ -23,7 +23,7 @@ export function RecentTraces({ traces }: RecentTracesProps) {
- {traces.slice(0, 5).map((trace) => ( + {traces.map((trace) => ( {trace.has_errors && ( - ERROR + ERROR )}
diff --git a/ui/src/features/dashboard/components/service-breakdown-chart.tsx b/ui/src/features/dashboard/components/service-breakdown-chart.tsx new file mode 100644 index 0000000..702c3c0 --- /dev/null +++ b/ui/src/features/dashboard/components/service-breakdown-chart.tsx @@ -0,0 +1,98 @@ +import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart"; +import type { ServiceStat } from "@/types"; + +const chartConfig = { + ok: { label: "OK", color: "var(--chart-1)" }, + errors: { label: "Errors", color: "var(--destructive)" }, +} satisfies ChartConfig; + +const MAX_SERVICES = 8; +const ROW_HEIGHT = 36; + +interface ServiceBreakdownChartProps { + services: ServiceStat[]; +} + +export function ServiceBreakdownChart({ + services, +}: ServiceBreakdownChartProps) { + const top = services.slice(0, MAX_SERVICES); + const rest = services.slice(MAX_SERVICES); + + const data = top.map((service) => ({ + name: service.name, + ok: service.trace_count - service.error_count, + errors: service.error_count, + total: service.trace_count, + })); + + if (rest.length > 0) { + const traceCount = rest.reduce((acc, s) => acc + s.trace_count, 0); + const errorCount = rest.reduce((acc, s) => acc + s.error_count, 0); + data.push({ + name: `other (${rest.length})`, + ok: traceCount - errorCount, + errors: errorCount, + total: traceCount, + }); + } + + return ( +
+
+

Traces by Service

+
+ + {data.length === 0 ? ( +
+

No services yet

+
+ ) : ( + + + + + name.length > 18 ? `${name.slice(0, 17)}…` : name + } + /> + } /> + + + + + + + )} +
+ ); +} diff --git a/ui/src/features/dashboard/components/stat-card.tsx b/ui/src/features/dashboard/components/stat-card.tsx index 5ffb70e..3785806 100644 --- a/ui/src/features/dashboard/components/stat-card.tsx +++ b/ui/src/features/dashboard/components/stat-card.tsx @@ -1,28 +1,15 @@ interface StatCardProps { label: string; value: string | number; - trend?: { - value: number; - direction: "up" | "down"; - }; + hint?: string; } -export function StatCard({ label, value, trend }: StatCardProps) { +export function StatCard({ label, value, hint }: StatCardProps) { return (
{label}
-
-
{value}
- {trend && ( -
- {trend.direction === "up" ? "↑" : "↓"} {trend.value}% -
- )} -
+
{value}
+ {hint &&
{hint}
}
); } diff --git a/ui/src/features/dashboard/components/traces-over-time-chart.tsx b/ui/src/features/dashboard/components/traces-over-time-chart.tsx new file mode 100644 index 0000000..9365e07 --- /dev/null +++ b/ui/src/features/dashboard/components/traces-over-time-chart.tsx @@ -0,0 +1,87 @@ +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart"; +import { formatDateTime, formatTimestamp } from "@/lib/formatters"; +import type { ActivityBucket } from "@/types"; + +const chartConfig = { + ok: { label: "OK", color: "var(--chart-1)" }, + errors: { label: "Errors", color: "var(--destructive)" }, +} satisfies ChartConfig; + +const MINUTE_NANOS = 60 * 1_000_000_000; +const DAY_NANOS = 24 * 60 * MINUTE_NANOS; + +interface TracesOverTimeChartProps { + activity: ActivityBucket[]; +} + +export function TracesOverTimeChart({ activity }: TracesOverTimeChartProps) { + const data = activity.map((bucket) => ({ + time: bucket.bucket_start_unix_nano, + ok: bucket.total - bucket.errors, + errors: bucket.errors, + })); + + const range = + data.length > 1 ? data[data.length - 1].time - data[0].time : 0; + const formatTick = (nano: number) => { + if (range > DAY_NANOS) return formatDateTime(nano).slice(0, 16); + // Sub-minute ranges need millisecond precision to tell ticks apart. + if (range < MINUTE_NANOS) return formatTimestamp(nano); + return formatTimestamp(nano).slice(0, 8); + }; + + return ( +
+
+

Trace Activity

+
+ + {data.length === 0 ? ( +
+

No trace activity yet

+
+ ) : ( + + + + + + + payload?.[0] + ? formatDateTime(payload[0].payload.time) + : "" + } + /> + } + /> + } /> + + + + + )} +
+ ); +} diff --git a/ui/src/features/dashboard/hooks/use-dashboard-data.ts b/ui/src/features/dashboard/hooks/use-dashboard-data.ts index 40ef1bb..71aacf6 100644 --- a/ui/src/features/dashboard/hooks/use-dashboard-data.ts +++ b/ui/src/features/dashboard/hooks/use-dashboard-data.ts @@ -1,33 +1,17 @@ -import { useTraces } from "@/hooks/api"; -import { useServices } from "@/hooks/api"; +import { useStats, useTraces } from "@/hooks/api"; export function useDashboardData() { - const { data: tracesData, isLoading: tracesLoading } = useTraces({ - limit: 10, - }); - const { data: servicesData, isLoading: servicesLoading } = useServices(); - - const traces = tracesData?.traces ?? []; - const services = servicesData ?? []; - const totalTraces = tracesData?.total ?? 0; - - const errorCount = traces.filter((t) => t.has_errors).length; - const errorRate = traces.length > 0 ? (errorCount / traces.length) * 100 : 0; - - const avgDuration = - traces.length > 0 - ? traces.reduce((acc, t) => acc + t.duration_ms, 0) / traces.length - : 0; + const stats = useStats(); + const traces = useTraces({ limit: 5 }); return { - traces, - services, - stats: { - totalServices: services.length, - totalTraces, - errorRate: errorRate.toFixed(1), - avgDuration: avgDuration.toFixed(0), + stats: stats.data, + recentTraces: traces.data?.traces ?? [], + isLoading: stats.isLoading || traces.isLoading, + error: stats.error ?? traces.error, + refetch: () => { + void stats.refetch(); + void traces.refetch(); }, - isLoading: tracesLoading || servicesLoading, }; } diff --git a/ui/src/features/logs/hooks/use-logs-data.ts b/ui/src/features/logs/hooks/use-logs-data.ts index 48916b9..4deb03f 100644 --- a/ui/src/features/logs/hooks/use-logs-data.ts +++ b/ui/src/features/logs/hooks/use-logs-data.ts @@ -1,5 +1,6 @@ import { useLogs } from "@/hooks/api"; import { useServices } from "@/hooks/api"; +import { SEVERITY_LEVEL_MAP } from "@/lib/constants"; import { useQueryStates, parseAsString, parseAsInteger } from "nuqs"; import { useMemo } from "react"; @@ -16,23 +17,35 @@ export function useLogsData() { const { data, isLoading, error, refetch } = useLogs({ limit: 100, service: filters.service ?? undefined, - level: filters.level ?? undefined, }); const services = servicesData ?? []; const allLogs = data ?? []; const filteredLogs = useMemo(() => { - if (!filters.search) return allLogs; + let result = allLogs; - const query = filters.search.toLowerCase(); - return allLogs.filter((log) => { - return ( - log.body.toLowerCase().includes(query) || - log.service_name?.toLowerCase().includes(query) + if (filters.level) { + const level = filters.level; + result = result.filter( + (log) => + log.severity_level && + SEVERITY_LEVEL_MAP[log.severity_level] === level, ); - }); - }, [allLogs, filters.search]); + } + + if (filters.search) { + const query = filters.search.toLowerCase(); + result = result.filter((log) => { + return ( + log.body.toLowerCase().includes(query) || + log.service_name?.toLowerCase().includes(query) + ); + }); + } + + return result; + }, [allLogs, filters.level, filters.search]); const totalItems = filteredLogs.length; const totalPages = Math.ceil(totalItems / filters.page_size); diff --git a/ui/src/features/services/components/service-chart.test.tsx b/ui/src/features/services/components/service-chart.test.tsx index 69ad25e..06b813e 100644 --- a/ui/src/features/services/components/service-chart.test.tsx +++ b/ui/src/features/services/components/service-chart.test.tsx @@ -17,7 +17,7 @@ const mockTraces: TraceInfo[] = [ has_errors: false, start_time: 1000000000000, root_span_name: "GET /api/users", - root_span_kind: "Server", + root_span_kind: "SERVER", }, { trace_id: "trace-2", @@ -27,7 +27,7 @@ const mockTraces: TraceInfo[] = [ has_errors: true, start_time: 1000000001000, root_span_name: "SELECT users", - root_span_kind: "Client", + root_span_kind: "CLIENT", }, { trace_id: "trace-3", @@ -37,7 +37,7 @@ const mockTraces: TraceInfo[] = [ has_errors: false, start_time: 1000000002000, root_span_name: "POST /api/login", - root_span_kind: "Server", + root_span_kind: "SERVER", }, ]; @@ -74,7 +74,7 @@ describe("ServiceChart", () => { has_errors: false, start_time: undefined, root_span_name: "Invalid trace", - root_span_kind: "Internal", + root_span_kind: "INTERNAL", }, ]; @@ -91,7 +91,7 @@ describe("ServiceChart", () => { has_errors: false, start_time: 1000000000000 + i * 1000, root_span_name: `Trace ${i}`, - root_span_kind: "Server", + root_span_kind: "SERVER", })); render(); @@ -134,7 +134,7 @@ describe("ServiceChart", () => { has_errors: false, start_time: 3000, root_span_name: "Third", - root_span_kind: "Server", + root_span_kind: "SERVER", }, { trace_id: "trace-1", @@ -143,7 +143,7 @@ describe("ServiceChart", () => { has_errors: false, start_time: 1000, root_span_name: "First", - root_span_kind: "Server", + root_span_kind: "SERVER", }, { trace_id: "trace-2", @@ -152,7 +152,7 @@ describe("ServiceChart", () => { has_errors: false, start_time: 2000, root_span_name: "Second", - root_span_kind: "Server", + root_span_kind: "SERVER", }, ]; diff --git a/ui/src/features/services/components/service-logs.tsx b/ui/src/features/services/components/service-logs.tsx index ecd9865..24abfc0 100644 --- a/ui/src/features/services/components/service-logs.tsx +++ b/ui/src/features/services/components/service-logs.tsx @@ -1,7 +1,7 @@ import { Link } from "@tanstack/react-router"; import type { Log } from "@/types"; import { formatRelativeTime } from "@/lib/formatters"; -import { getSeverityColor } from "@/lib/constants"; +import { getSeverityBgColor, getSeverityColor } from "@/lib/colors"; interface ServiceLogsProps { logs: Log[]; @@ -44,7 +44,7 @@ export function ServiceLogs({ logs, serviceName }: ServiceLogsProps) {
{log.severity_level && ( {log.severity_level} diff --git a/ui/src/features/services/hooks/use-service-details.ts b/ui/src/features/services/hooks/use-service-details.ts index bb6858c..d1ab520 100644 --- a/ui/src/features/services/hooks/use-service-details.ts +++ b/ui/src/features/services/hooks/use-service-details.ts @@ -56,7 +56,8 @@ export function useServiceDetails(serviceName: string) { const errorLogs = logs.filter( (l) => l.severity_level && - (l.severity_level === "Error" || l.severity_level === "Fatal"), + (l.severity_level.startsWith("ERROR") || + l.severity_level.startsWith("FATAL")), ).length; const totalMetrics = metrics.length; diff --git a/ui/src/features/traces/components/span-detail-dialog.tsx b/ui/src/features/traces/components/span-detail-dialog.tsx deleted file mode 100644 index 32a9d71..0000000 --- a/ui/src/features/traces/components/span-detail-dialog.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import type { Span } from "@/types"; -import { formatTimestamp, formatNanoDuration } from "@/lib/formatters"; -import { SPAN_KIND_MAP } from "@/lib/constants"; -import { StatusBadge } from "@/components/shared/status-badge"; -import { AttributesViewer } from "@/components/shared/attributes-viewer"; -import { CopyButton } from "@/components/shared/copy-button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -interface SpanDetailDialogProps { - span: Span | null; - open: boolean; - onClose: () => void; -} - -export function SpanDetailDialog({ - span, - open, - onClose, -}: SpanDetailDialogProps) { - if (!span) return null; - - const duration = formatNanoDuration( - span.start_time_unix_nano, - span.end_time_unix_nano, - ); - - return ( - - - - - Span Details - - - -
-
-
-

Name

-

{span.name}

-
- -
-

Kind

-

{SPAN_KIND_MAP[span.kind]}

-
- -
-

Duration

-

{duration}

-
- -
-

Status

- -
-
- -
-
-

Span ID

- -
-

- {span.span_id} -

-
- -
-
-

Trace ID

- -
-

- {span.trace_id} -

-
- - {span.parent_span_id && ( -
-
-

Parent Span ID

- -
-

- {span.parent_span_id} -

-
- )} - - {span.service_name && ( -
-

Service

-

{span.service_name}

-
- )} - -
-
-

Start Time

-

- {formatTimestamp(span.start_time_unix_nano)} -

-
- -
-

End Time

-

- {formatTimestamp(span.end_time_unix_nano)} -

-
-
- - {span.status.message && ( -
-

- Status Message -

-

- {span.status.message} -

-
- )} - - - - - Attributes ({Object.keys(span.attributes).length}) - - JSON - - - - - - - -
-
- -
-
-                  {JSON.stringify(span, null, 2)}
-                
-
-
-
-
-
-
- ); -} diff --git a/ui/src/features/traces/components/span-detail-sheet.tsx b/ui/src/features/traces/components/span-detail-sheet.tsx index 156431b..693cf83 100644 --- a/ui/src/features/traces/components/span-detail-sheet.tsx +++ b/ui/src/features/traces/components/span-detail-sheet.tsx @@ -1,6 +1,5 @@ import type { Span } from "@/types"; import { formatTimestamp, formatNanoDuration } from "@/lib/formatters"; -import { SPAN_KIND_MAP } from "@/lib/constants"; import { StatusBadge } from "@/components/shared/status-badge"; import { AttributesViewer } from "@/components/shared/attributes-viewer"; import { CopyButton } from "@/components/shared/copy-button"; @@ -42,7 +41,7 @@ export function SpanDetailSheet({ span, open, onClose }: SpanDetailSheetProps) {

Kind

-

{SPAN_KIND_MAP[span.kind]}

+

{span.kind}

diff --git a/ui/src/features/traces/components/span-timeline-simple.tsx b/ui/src/features/traces/components/span-timeline-simple.tsx deleted file mode 100644 index 351cbbb..0000000 --- a/ui/src/features/traces/components/span-timeline-simple.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useState } from "react"; -import type { Span } from "@/types"; -import { formatDurationCompact } from "@/lib/formatters"; -import { SpanDetailSheet } from "./span-detail-sheet"; - -interface SpanTimelineSimpleProps { - spans: Span[]; -} - -export function SpanTimelineSimple({ spans }: SpanTimelineSimpleProps) { - const [selectedSpan, setSelectedSpan] = useState(null); - - if (spans.length === 0) { - return ( -
-

No spans to display

-
- ); - } - - const minTime = Math.min(...spans.map((s) => s.start_time_unix_nano)); - const maxTime = Math.max(...spans.map((s) => s.end_time_unix_nano)); - const totalDuration = (maxTime - minTime) / 1_000_000; - - const sortedSpans = [...spans].sort( - (a, b) => a.start_time_unix_nano - b.start_time_unix_nano, - ); - - return ( - <> -
-
- - Total Duration: {formatDurationCompact(totalDuration)} - -
- -
- {sortedSpans.map((span) => { - const duration = - (span.end_time_unix_nano - span.start_time_unix_nano) / 1_000_000; - const startOffset = - ((span.start_time_unix_nano - minTime) / - 1_000_000 / - totalDuration) * - 100; - const width = (duration / totalDuration) * 100; - const hasError = span.status.code === "Error"; - - return ( - - ); - })} -
-
- - setSelectedSpan(null)} - /> - - ); -} diff --git a/ui/src/features/traces/components/span-timeline.tsx b/ui/src/features/traces/components/span-timeline.tsx deleted file mode 100644 index b946580..0000000 --- a/ui/src/features/traces/components/span-timeline.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useState } from "react"; -import type { Span } from "@/types"; -import { formatDurationCompact } from "@/lib/formatters"; -import { SpanDetailDialog } from "./span-detail-dialog"; - -interface SpanTimelineProps { - spans: Span[]; -} - -interface SpanNode extends Span { - children: SpanNode[]; - depth: number; -} - -interface TimelineSpan { - span: Span; - depth: number; - startOffset: number; - width: number; - children: TimelineSpan[]; -} - -function buildSpanTree(spans: Span[]): SpanNode[] { - const spanMap = new Map(); - const roots: SpanNode[] = []; - - spans.forEach((span) => { - spanMap.set(span.span_id, { ...span, children: [], depth: 0 }); - }); - - spans.forEach((span) => { - const node = spanMap.get(span.span_id)!; - if (span.parent_span_id) { - const parent = spanMap.get(span.parent_span_id); - if (parent) { - parent.children.push(node); - node.depth = parent.depth + 1; - } else { - roots.push(node); - } - } else { - roots.push(node); - } - }); - - return roots; -} - -function buildTimeline(spans: Span[]): { - items: TimelineSpan[]; - totalDuration: number; - minTime: number; -} { - if (spans.length === 0) { - return { items: [], totalDuration: 0, minTime: 0 }; - } - - const tree = buildSpanTree(spans); - const minTime = Math.min(...spans.map((s) => s.start_time_unix_nano)); - const maxTime = Math.max(...spans.map((s) => s.end_time_unix_nano)); - const totalDuration = (maxTime - minTime) / 1_000_000; - - function processNode(node: SpanNode): TimelineSpan { - const startOffset = - ((node.start_time_unix_nano - minTime) / 1_000_000 / totalDuration) * 100; - const duration = - (node.end_time_unix_nano - node.start_time_unix_nano) / 1_000_000; - const width = (duration / totalDuration) * 100; - - return { - span: node, - depth: node.depth, - startOffset, - width: Math.max(width, 0.5), - children: node.children.map(processNode), - }; - } - - const items = tree.map(processNode); - - return { items, totalDuration, minTime }; -} - -function flattenTimeline(items: TimelineSpan[]): TimelineSpan[] { - const result: TimelineSpan[] = []; - - function traverse(item: TimelineSpan) { - result.push(item); - item.children.forEach(traverse); - } - - items.forEach(traverse); - return result; -} - -function TimelineBar({ - item, - onClick, -}: { - item: TimelineSpan; - onClick: (span: Span) => void; -}) { - const hasError = item.span.status.code === "Error"; - const duration = - (item.span.end_time_unix_nano - item.span.start_time_unix_nano) / 1_000_000; - - const barColor = hasError - ? "bg-red-500/80 hover:bg-red-500" - : "bg-green-500/60 hover:bg-green-500"; - - return ( - - ); -} - -export function SpanTimeline({ spans }: SpanTimelineProps) { - const [selectedSpan, setSelectedSpan] = useState(null); - const { items, totalDuration } = buildTimeline(spans); - const flatItems = flattenTimeline(items); - - if (spans.length === 0) { - return ( -
-

No spans to display

-
- ); - } - - const maxDepth = Math.max(...flatItems.map((item) => item.depth)); - const height = (maxDepth + 1) * 32 + 40; - - const timeMarkers = [0, 25, 50, 75, 100]; - - return ( - <> -
-
- - Total Duration: {formatDurationCompact(totalDuration)} - - - {spans.length} {spans.length === 1 ? "span" : "spans"} - -
- -
-
- {timeMarkers.map((marker) => ( -
- - {marker}% - -
- ))} - - {flatItems.map((item, index) => ( - - ))} -
-
- -
-
-
- Normal -
-
-
- Error -
-
-
- - setSelectedSpan(null)} - /> - - ); -} diff --git a/ui/src/features/traces/components/span-tree.tsx b/ui/src/features/traces/components/span-tree.tsx deleted file mode 100644 index 2d3f8c9..0000000 --- a/ui/src/features/traces/components/span-tree.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useState } from "react"; -import type { Span } from "@/types"; -import { formatNanoDuration } from "@/lib/formatters"; -import { SPAN_KIND_MAP } from "@/lib/constants"; -import { StatusBadge } from "@/components/shared/status-badge"; -import { SpanDetailSheet } from "./span-detail-sheet"; - -interface SpanTreeProps { - spans: Span[]; -} - -interface SpanNode extends Span { - children: SpanNode[]; - depth: number; -} - -function buildSpanTree(spans: Span[]): SpanNode[] { - const spanMap = new Map(); - const roots: SpanNode[] = []; - - spans.forEach((span) => { - spanMap.set(span.span_id, { ...span, children: [], depth: 0 }); - }); - - spans.forEach((span) => { - const node = spanMap.get(span.span_id)!; - if (span.parent_span_id) { - const parent = spanMap.get(span.parent_span_id); - if (parent) { - parent.children.push(node); - node.depth = parent.depth + 1; - } else { - roots.push(node); - } - } else { - roots.push(node); - } - }); - - return roots; -} - -function SpanTreeNode({ - node, - onSpanClick, -}: { - node: SpanNode; - onSpanClick: (span: Span) => void; -}) { - const duration = formatNanoDuration( - node.start_time_unix_nano, - node.end_time_unix_nano, - ); - - return ( - <> - - - {node.children.map((child) => ( - - ))} - - ); -} - -export function SpanTree({ spans }: SpanTreeProps) { - const [selectedSpan, setSelectedSpan] = useState(null); - const tree = buildSpanTree(spans); - - return ( - <> -
- {tree.map((node) => ( - - ))} -
- - setSelectedSpan(null)} - /> - - ); -} diff --git a/ui/src/features/traces/components/span-waterfall.tsx b/ui/src/features/traces/components/span-waterfall.tsx index bc0069e..5bc51f7 100644 --- a/ui/src/features/traces/components/span-waterfall.tsx +++ b/ui/src/features/traces/components/span-waterfall.tsx @@ -1,104 +1,85 @@ -import { useState, useEffect, useMemo } from "react"; +import { useMemo, useState } from "react"; +import { ChevronDown, ChevronRight } from "lucide-react"; import type { Span } from "@/types"; import { formatDurationCompact } from "@/lib/formatters"; +import { + buildSpanTree, + collectParentIds, + type SpanNode, +} from "../lib/span-tree"; import { SpanDetailSheet } from "./span-detail-sheet"; interface SpanWaterfallProps { spans: Span[]; } -interface SpanNode extends Span { - children: SpanNode[]; - depth: number; -} - -function buildSpanTree(spans: Span[]): { - roots: SpanNode[]; - minTime: number; - maxTime: number; -} { - const spanMap = new Map(); - const roots: SpanNode[] = []; +/** Shared column template so every row lines up on one time gutter. */ +const GRID_COLS = "grid grid-cols-[minmax(280px,40%)_1fr]"; - spans.forEach((span) => { - spanMap.set(span.span_id, { ...span, children: [], depth: 0 }); - }); +/** Fractions of total duration where axis ticks and gridlines sit. */ +const TICK_FRACTIONS = [0, 0.25, 0.5, 0.75, 1]; - const sorted = [...spans].sort( - (a, b) => a.start_time_unix_nano - b.start_time_unix_nano, +function EmptyWaterfall({ message }: { message: string }) { + return ( +
+

{message}

+
); - - sorted.forEach((span) => { - const node = spanMap.get(span.span_id)!; - if (span.parent_span_id && span.parent_span_id !== "") { - const parent = spanMap.get(span.parent_span_id); - if (parent) { - parent.children.push(node); - node.depth = parent.depth + 1; - } else { - roots.push(node); - } - } else { - roots.push(node); - } - }); - - const startTimes = spans - .map((s) => s.start_time_unix_nano) - .filter((t) => t > 0); - const endTimes = spans.map((s) => s.end_time_unix_nano).filter((t) => t > 0); - - const minTime = Math.min(...startTimes); - const maxTime = Math.max(...endTimes); - - return { roots, minTime, maxTime }; } interface SpanRowProps { node: SpanNode; minTime: number; totalDuration: number; - onSpanClick: (span: Span) => void; - expanded: Set; + showService: boolean; + collapsed: Set; onToggle: (spanId: string) => void; + onSpanClick: (span: Span) => void; } function SpanRow({ node, minTime, totalDuration, - onSpanClick, - expanded, + showService, + collapsed, onToggle, + onSpanClick, }: SpanRowProps) { const duration = (node.end_time_unix_nano - node.start_time_unix_nano) / 1_000_000; const startOffset = ((node.start_time_unix_nano - minTime) / 1_000_000 / totalDuration) * 100; const width = (duration / totalDuration) * 100; - const hasError = node.status.code === "Error"; + const hasError = node.status.code === "ERROR"; const hasChildren = node.children.length > 0; - const isExpanded = expanded.has(node.span_id); + const isExpanded = !collapsed.has(node.span_id); return ( <> -
+
- {hasChildren && ( + {hasChildren ? ( + ) : ( +
)} - {!hasChildren &&
} - + {formatDurationCompact(duration)}
-
-
-
onSpanClick(node)} - /> -
+
+
- {hasChildren && isExpanded && ( - <> - {node.children.map((child) => ( - - ))} - - )} + {hasChildren && + isExpanded && + node.children.map((child) => ( + + ))} ); } export function SpanWaterfall({ spans }: SpanWaterfallProps) { const [selectedSpan, setSelectedSpan] = useState(null); - const [expanded, setExpanded] = useState>(new Set()); - - const spanTree = useMemo(() => { - if (!spans || spans.length === 0) return null; - try { - return buildSpanTree(spans); - } catch { - return null; - } - }, [spans]); - - useEffect(() => { - if (!spanTree) return; + const [collapsed, setCollapsed] = useState>(new Set()); - const allSpanIds = new Set(); - const traverse = (node: SpanNode) => { - if (node.children.length > 0) { - allSpanIds.add(node.span_id); - node.children.forEach(traverse); - } - }; - spanTree.roots.forEach(traverse); - setExpanded(allSpanIds); - }, [spanTree]); + const spanTree = useMemo( + () => (spans && spans.length > 0 ? buildSpanTree(spans) : null), + [spans], + ); - if (!spans || spans.length === 0) { - return ( -
-

No spans to display

-
- ); - } + const showService = useMemo( + () => new Set(spans?.map((s) => s.service_name).filter(Boolean)).size > 1, + [spans], + ); if (!spanTree) { - return ( -
-

Error displaying waterfall

-
- ); + return ; } - try { - const { roots, minTime, maxTime } = spanTree; - const totalDuration = (maxTime - minTime) / 1_000_000; + const { roots, minTime, maxTime } = spanTree; + const totalDuration = (maxTime - minTime) / 1_000_000; - if (!isFinite(totalDuration) || totalDuration <= 0) { - return ( -
-

Invalid span timing data

-
- ); - } + if (!isFinite(totalDuration) || totalDuration <= 0) { + return ; + } - const toggleExpand = (spanId: string) => { - const newExpanded = new Set(expanded); - if (newExpanded.has(spanId)) { - newExpanded.delete(spanId); + const toggleExpand = (spanId: string) => { + setCollapsed((prev) => { + const next = new Set(prev); + if (next.has(spanId)) { + next.delete(spanId); } else { - newExpanded.add(spanId); + next.add(spanId); } - setExpanded(newExpanded); - }; - - const expandAll = () => { - const allSpanIds = new Set(); - const traverse = (node: SpanNode) => { - if (node.children.length > 0) { - allSpanIds.add(node.span_id); - node.children.forEach(traverse); - } - }; - roots.forEach(traverse); - setExpanded(allSpanIds); - }; + return next; + }); + }; - const collapseAll = () => { - setExpanded(new Set()); - }; + const expandAll = () => setCollapsed(new Set()); + const collapseAll = () => setCollapsed(collectParentIds(roots)); - return ( - <> -
+ return ( + <> +
+
@@ -263,45 +209,71 @@ export function SpanWaterfall({ spans }: SpanWaterfallProps) {
-
-
- SPAN -
-
+
+
- TIMELINE + SPAN
+
+ {TICK_FRACTIONS.map((fraction) => ( + + {formatDurationCompact(totalDuration * fraction)} + + ))} +
+
-
- {roots.map((node) => ( - - ))} +
+ - ); - } + + setSelectedSpan(null)} + /> + + ); } diff --git a/ui/src/features/traces/components/trace-details.tsx b/ui/src/features/traces/components/trace-details.tsx index 761dc02..b664208 100644 --- a/ui/src/features/traces/components/trace-details.tsx +++ b/ui/src/features/traces/components/trace-details.tsx @@ -1,12 +1,34 @@ import type { Trace } from "@/types"; import { SpanWaterfall } from "./span-waterfall"; +import { TraceLogs } from "./trace-logs"; import { CopyButton } from "@/components/shared/copy-button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useLogs } from "@/hooks/api"; +import { + formatDurationCompact, + formatDateTime, +} from "@/lib/formatters"; interface TraceDetailsProps { trace: Trace; } export function TraceDetails({ trace }: TraceDetailsProps) { + const { data: logs, isLoading: logsLoading } = useLogs({ + trace_id: trace.trace_id, + }); + const startTimes = trace.spans + .map((s) => s.start_time_unix_nano) + .filter((t) => t > 0); + const endTimes = trace.spans + .map((s) => s.end_time_unix_nano) + .filter((t) => t > 0); + const startTime = startTimes.length > 0 ? Math.min(...startTimes) : null; + const durationMs = + startTime !== null && endTimes.length > 0 + ? (Math.max(...endTimes) - startTime) / 1_000_000 + : null; + return (
@@ -25,14 +47,51 @@ export function TraceDetails({ trace }: TraceDetailsProps) { {trace.service_name}
)} -
- Spans:{" "} - {trace.spans.length} +
+
+ Spans:{" "} + {trace.spans.length} +
+ {durationMs !== null && ( +
+ Duration:{" "} + + {formatDurationCompact(durationMs)} + +
+ )} + {startTime !== null && ( +
+ Started:{" "} + {formatDateTime(startTime)} +
+ )}
- + + + Waterfall + Logs ({logs?.length ?? 0}) + + + + {trace.spans.length === 0 ? ( +
+

+ No spans recorded for this trace +

+
+ ) : ( + + )} +
+ + + + +
); } diff --git a/ui/src/features/traces/components/trace-logs.tsx b/ui/src/features/traces/components/trace-logs.tsx new file mode 100644 index 0000000..b66c2e3 --- /dev/null +++ b/ui/src/features/traces/components/trace-logs.tsx @@ -0,0 +1,32 @@ +import type { Log } from "@/types"; +import { LogEntry } from "@/features/logs/components/log-entry"; +import { LoadingState } from "@/components/shared/loading-state"; + +interface TraceLogsProps { + logs: Log[]; + isLoading: boolean; +} + +export function TraceLogs({ logs, isLoading }: TraceLogsProps) { + if (isLoading) { + return ; + } + + if (logs.length === 0) { + return ( +
+

+ No logs correlated with this trace +

+
+ ); + } + + return ( +
+ {logs.map((log, index) => ( + + ))} +
+ ); +} diff --git a/ui/src/features/traces/components/traces-table.tsx b/ui/src/features/traces/components/traces-table.tsx index 2f75159..b1b52ca 100644 --- a/ui/src/features/traces/components/traces-table.tsx +++ b/ui/src/features/traces/components/traces-table.tsx @@ -12,12 +12,12 @@ import { } from "@/components/ui/table"; const SPAN_KIND_COLORS: Record = { - Server: "bg-blue-500/10 text-blue-500", - Client: "bg-purple-500/10 text-purple-500", - Producer: "bg-green-500/10 text-green-500", - Consumer: "bg-yellow-500/10 text-yellow-500", - Internal: "bg-gray-500/10 text-gray-500", - Unspecified: "bg-gray-500/10 text-gray-500", + SERVER: "bg-blue-500/10 text-blue-500", + CLIENT: "bg-purple-500/10 text-purple-500", + PRODUCER: "bg-green-500/10 text-green-500", + CONSUMER: "bg-yellow-500/10 text-yellow-500", + INTERNAL: "bg-gray-500/10 text-gray-500", + UNSPECIFIED: "bg-gray-500/10 text-gray-500", }; interface TracesTableProps { diff --git a/ui/src/features/traces/lib/span-tree.test.ts b/ui/src/features/traces/lib/span-tree.test.ts new file mode 100644 index 0000000..2e537cf --- /dev/null +++ b/ui/src/features/traces/lib/span-tree.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect } from "vitest"; +import type { Span } from "@/types"; +import { buildSpanTree, collectParentIds } from "./span-tree"; + +function makeSpan(overrides: Partial & { span_id: string }): Span { + return { + trace_id: "trace1", + name: `op-${overrides.span_id}`, + kind: "SERVER", + start_time_unix_nano: 1_000, + end_time_unix_nano: 2_000, + attributes: {}, + status: { code: "OK" }, + ...overrides, + }; +} + +describe("buildSpanTree", () => { + it("nests children under their parents with depths", () => { + const spans = [ + makeSpan({ span_id: "root", start_time_unix_nano: 1_000 }), + makeSpan({ + span_id: "child", + parent_span_id: "root", + start_time_unix_nano: 1_100, + }), + makeSpan({ + span_id: "grandchild", + parent_span_id: "child", + start_time_unix_nano: 1_200, + }), + ]; + + const { roots } = buildSpanTree(spans); + + expect(roots).toHaveLength(1); + expect(roots[0].span_id).toBe("root"); + expect(roots[0].depth).toBe(0); + expect(roots[0].children[0].span_id).toBe("child"); + expect(roots[0].children[0].depth).toBe(1); + expect(roots[0].children[0].children[0].span_id).toBe("grandchild"); + expect(roots[0].children[0].children[0].depth).toBe(2); + }); + + it("promotes spans with a missing parent to roots", () => { + const spans = [ + makeSpan({ span_id: "a" }), + makeSpan({ span_id: "orphan", parent_span_id: "not-collected" }), + ]; + + const { roots } = buildSpanTree(spans); + + expect(roots.map((r) => r.span_id).sort()).toEqual(["a", "orphan"]); + }); + + it("recovers spans trapped in a parent cycle", () => { + const spans = [ + makeSpan({ span_id: "a", parent_span_id: "b" }), + makeSpan({ span_id: "b", parent_span_id: "a" }), + ]; + + const { roots } = buildSpanTree(spans); + + expect(roots).toHaveLength(1); + const rendered: string[] = []; + const stack = [...roots]; + while (stack.length > 0) { + const node = stack.pop()!; + rendered.push(node.span_id); + stack.push(...node.children); + } + expect(rendered.sort()).toEqual(["a", "b"]); + }); + + it("computes min and max times ignoring zeroed timestamps", () => { + const spans = [ + makeSpan({ + span_id: "a", + start_time_unix_nano: 5_000, + end_time_unix_nano: 9_000, + }), + makeSpan({ + span_id: "b", + start_time_unix_nano: 3_000, + end_time_unix_nano: 4_000, + }), + makeSpan({ + span_id: "c", + start_time_unix_nano: 0, + end_time_unix_nano: 0, + }), + ]; + + const { minTime, maxTime } = buildSpanTree(spans); + + expect(minTime).toBe(3_000); + expect(maxTime).toBe(9_000); + }); +}); + +describe("collectParentIds", () => { + it("returns only ids of nodes with children", () => { + const spans = [ + makeSpan({ span_id: "root" }), + makeSpan({ span_id: "child", parent_span_id: "root" }), + makeSpan({ span_id: "leaf", parent_span_id: "child" }), + makeSpan({ span_id: "lone" }), + ]; + + const { roots } = buildSpanTree(spans); + const ids = collectParentIds(roots); + + expect([...ids].sort()).toEqual(["child", "root"]); + }); +}); diff --git a/ui/src/features/traces/lib/span-tree.ts b/ui/src/features/traces/lib/span-tree.ts new file mode 100644 index 0000000..d213405 --- /dev/null +++ b/ui/src/features/traces/lib/span-tree.ts @@ -0,0 +1,96 @@ +import type { Span } from "@/types"; + +export interface SpanNode extends Span { + children: SpanNode[]; + depth: number; +} + +export interface SpanTree { + roots: SpanNode[]; + minTime: number; + maxTime: number; +} + +/** + * Builds the parent/child tree for a trace's spans and computes its time + * bounds. Spans whose parent is missing become roots; nodes trapped in a + * parent cycle are promoted to roots with the cycle edge pruned, so every + * span is always rendered exactly once. + */ +export function buildSpanTree(spans: Span[]): SpanTree { + const spanMap = new Map(); + spans.forEach((span) => { + spanMap.set(span.span_id, { ...span, children: [], depth: 0 }); + }); + + const sorted = [...spans].sort( + (a, b) => a.start_time_unix_nano - b.start_time_unix_nano, + ); + + const roots: SpanNode[] = []; + sorted.forEach((span) => { + const node = spanMap.get(span.span_id)!; + const parent = span.parent_span_id + ? spanMap.get(span.parent_span_id) + : undefined; + if (parent && parent !== node) { + parent.children.push(node); + } else { + roots.push(node); + } + }); + + const visited = new Set(); + const walk = (root: SpanNode) => { + const stack = [{ node: root, depth: root.depth }]; + while (stack.length > 0) { + const { node, depth } = stack.pop()!; + visited.add(node.span_id); + node.depth = depth; + node.children = node.children.filter( + (child) => !visited.has(child.span_id), + ); + node.children.forEach((child) => { + stack.push({ node: child, depth: depth + 1 }); + }); + } + }; + + roots.forEach(walk); + + // Nodes never reached from a root are part of a parent cycle. + sorted.forEach((span) => { + const node = spanMap.get(span.span_id)!; + if (!visited.has(node.span_id)) { + roots.push(node); + walk(node); + } + }); + + let minTime = Infinity; + let maxTime = -Infinity; + for (const span of spans) { + if (span.start_time_unix_nano > 0 && span.start_time_unix_nano < minTime) { + minTime = span.start_time_unix_nano; + } + if (span.end_time_unix_nano > 0 && span.end_time_unix_nano > maxTime) { + maxTime = span.end_time_unix_nano; + } + } + + return { roots, minTime, maxTime }; +} + +/** Span ids of every node that has children, across the whole tree. */ +export function collectParentIds(roots: SpanNode[]): Set { + const ids = new Set(); + const stack = [...roots]; + while (stack.length > 0) { + const node = stack.pop()!; + if (node.children.length > 0) { + ids.add(node.span_id); + stack.push(...node.children); + } + } + return ids; +} diff --git a/ui/src/hooks/api/index.ts b/ui/src/hooks/api/index.ts index 6a4b21b..d3f1406 100644 --- a/ui/src/hooks/api/index.ts +++ b/ui/src/hooks/api/index.ts @@ -3,3 +3,4 @@ export * from "./use-logs"; export * from "./use-metrics"; export * from "./use-project"; export * from "./use-services"; +export * from "./use-stats"; diff --git a/ui/src/hooks/api/use-stats.ts b/ui/src/hooks/api/use-stats.ts new file mode 100644 index 0000000..bcb83b9 --- /dev/null +++ b/ui/src/hooks/api/use-stats.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { statsService } from "@/lib/api/services"; + +export function useStats() { + return useQuery({ + queryKey: ["stats"], + queryFn: () => statsService.getStats(), + }); +} diff --git a/ui/src/hooks/use-auto-refresh.ts b/ui/src/hooks/use-auto-refresh.ts index 04cb374..87aaf84 100644 --- a/ui/src/hooks/use-auto-refresh.ts +++ b/ui/src/hooks/use-auto-refresh.ts @@ -1,21 +1,40 @@ import { useEffect } from "react"; import { useQueryClient } from "@tanstack/react-query"; -import { getSettings } from "@/lib/settings"; +import { getSettings, SETTINGS_CHANGED_EVENT } from "@/lib/settings"; + +const DATA_KEYS = ["traces", "trace", "logs", "metrics", "services", "stats"]; export function useAutoRefresh() { const queryClient = useQueryClient(); useEffect(() => { - const settings = getSettings(); + let interval: ReturnType | undefined; + + const arm = () => { + if (interval) { + clearInterval(interval); + interval = undefined; + } + + const settings = getSettings(); + if (!settings.autoRefresh) { + return; + } - if (!settings.autoRefresh) { - return; - } + interval = setInterval(() => { + queryClient.invalidateQueries({ + predicate: (query) => DATA_KEYS.includes(query.queryKey[0] as string), + refetchType: "active", + }); + }, settings.refreshInterval); + }; - const interval = setInterval(() => { - queryClient.invalidateQueries(); - }, settings.refreshInterval); + arm(); + window.addEventListener(SETTINGS_CHANGED_EVENT, arm); - return () => clearInterval(interval); + return () => { + if (interval) clearInterval(interval); + window.removeEventListener(SETTINGS_CHANGED_EVENT, arm); + }; }, [queryClient]); } diff --git a/ui/src/index.css b/ui/src/index.css index 02aefaa..16bf0b2 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -191,10 +191,10 @@ @keyframes highlight-pulse { 0%, 100% { - background-color: rgba(255, 255, 255, 0.03); + background-color: color-mix(in oklab, var(--foreground) 3%, transparent); } 50% { - background-color: rgba(59, 130, 246, 0.2); + background-color: color-mix(in oklab, var(--primary) 20%, transparent); } } @@ -217,18 +217,19 @@ } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); + background: color-mix(in oklab, var(--foreground) 12%, transparent); border-radius: 0px; transition: background 0.2s ease; } ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); + background: color-mix(in oklab, var(--foreground) 25%, transparent); } * { scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.1) transparent; + scrollbar-color: color-mix(in oklab, var(--foreground) 12%, transparent) + transparent; } * { diff --git a/ui/src/lib/api/services/index.ts b/ui/src/lib/api/services/index.ts index af136c5..4e0143e 100644 --- a/ui/src/lib/api/services/index.ts +++ b/ui/src/lib/api/services/index.ts @@ -2,3 +2,4 @@ export * from "./traces"; export * from "./logs"; export * from "./metrics"; export * from "./services"; +export * from "./stats"; diff --git a/ui/src/lib/api/services/stats.ts b/ui/src/lib/api/services/stats.ts new file mode 100644 index 0000000..9c51174 --- /dev/null +++ b/ui/src/lib/api/services/stats.ts @@ -0,0 +1,9 @@ +import { apiClient } from "../client"; +import type { StatsResponse } from "@/types"; + +export const statsService = { + getStats: async (): Promise => { + const { data } = await apiClient.get("/stats"); + return data; + }, +}; diff --git a/ui/src/lib/colors.ts b/ui/src/lib/colors.ts index 3e3437a..112f984 100644 --- a/ui/src/lib/colors.ts +++ b/ui/src/lib/colors.ts @@ -19,15 +19,15 @@ export const severityBgColors: Record = { }; export const statusColors: Record = { - Unset: "text-foreground/40", - Ok: "text-green-500", - Error: "text-red-500", + UNSET: "text-foreground/40", + OK: "text-green-500", + ERROR: "text-red-500", }; export const statusBgColors: Record = { - Unset: "bg-foreground/5", - Ok: "bg-green-500/10", - Error: "bg-red-500/10", + UNSET: "bg-foreground/5", + OK: "bg-green-500/10", + ERROR: "bg-red-500/10", }; export function getSeverityColor(level: SeverityLevel): string { diff --git a/ui/src/lib/constants.ts b/ui/src/lib/constants.ts index d22c9fb..3ca3ddd 100644 --- a/ui/src/lib/constants.ts +++ b/ui/src/lib/constants.ts @@ -1,70 +1,34 @@ -import type { SeverityLevel, SpanKind, StatusCode } from "@/types"; +import type { SeverityLevel } from "@/types"; -export const SEVERITY_LEVEL_MAP: Record = { - Unspecified: "UNSPECIFIED", - Trace: "TRACE", - Trace2: "TRACE", - Trace3: "TRACE", - Trace4: "TRACE", - Debug: "DEBUG", - Debug2: "DEBUG", - Debug3: "DEBUG", - Debug4: "DEBUG", - Info: "INFO", - Info2: "INFO", - Info3: "INFO", - Info4: "INFO", - Warn: "WARN", - Warn2: "WARN", - Warn3: "WARN", - Warn4: "WARN", - Error: "ERROR", - Error2: "ERROR", - Error3: "ERROR", - Error4: "ERROR", - Fatal: "FATAL", - Fatal2: "FATAL", - Fatal3: "FATAL", - Fatal4: "FATAL", -}; - -export const SPAN_KIND_MAP: Record = { - Unspecified: "UNSPECIFIED", - Internal: "INTERNAL", - Server: "SERVER", - Client: "CLIENT", - Producer: "PRODUCER", - Consumer: "CONSUMER", -}; +/// Duration thresholds (ms) for fast/medium/slow badge coloring. +export const DURATION_FAST_MS = 100; +export const DURATION_SLOW_MS = 500; -export const STATUS_CODE_MAP: Record = { - Unset: "UNSET", - Ok: "OK", - Error: "ERROR", +/// Collapses the OTLP fine-grained levels (e.g. ERROR2..4) into display groups. +export const SEVERITY_LEVEL_MAP: Record = { + UNSPECIFIED: "UNSPECIFIED", + TRACE: "TRACE", + TRACE2: "TRACE", + TRACE3: "TRACE", + TRACE4: "TRACE", + DEBUG: "DEBUG", + DEBUG2: "DEBUG", + DEBUG3: "DEBUG", + DEBUG4: "DEBUG", + INFO: "INFO", + INFO2: "INFO", + INFO3: "INFO", + INFO4: "INFO", + WARN: "WARN", + WARN2: "WARN", + WARN3: "WARN", + WARN4: "WARN", + ERROR: "ERROR", + ERROR2: "ERROR", + ERROR3: "ERROR", + ERROR4: "ERROR", + FATAL: "FATAL", + FATAL2: "FATAL", + FATAL3: "FATAL", + FATAL4: "FATAL", }; - -export function getSeverityColor(level: string): string { - const upperLevel = level.toUpperCase(); - - if (upperLevel === "FATAL" || upperLevel === "ERROR") { - return "bg-red-500/10 text-red-500"; - } - - if (upperLevel === "WARN") { - return "bg-yellow-500/10 text-yellow-500"; - } - - if (upperLevel === "INFO") { - return "bg-blue-500/10 text-blue-500"; - } - - if (upperLevel === "DEBUG") { - return "bg-purple-500/10 text-purple-500"; - } - - if (upperLevel === "TRACE") { - return "bg-gray-500/10 text-gray-500"; - } - - return "bg-gray-500/10 text-gray-500"; -} diff --git a/ui/src/lib/settings.ts b/ui/src/lib/settings.ts index 1031817..3a40e75 100644 --- a/ui/src/lib/settings.ts +++ b/ui/src/lib/settings.ts @@ -1,5 +1,7 @@ const SETTINGS_KEY = "faze-settings"; +export const SETTINGS_CHANGED_EVENT = "faze:settings-changed"; + export interface Settings { autoRefresh: boolean; refreshInterval: number; @@ -25,7 +27,17 @@ export function getSettings(): Settings { export function saveSettings(settings: Settings): void { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); + window.dispatchEvent(new Event(SETTINGS_CHANGED_EVENT)); } catch (err) { console.error("Failed to save settings:", err); } } + +export function resetSettings(): void { + try { + localStorage.removeItem(SETTINGS_KEY); + window.dispatchEvent(new Event(SETTINGS_CHANGED_EVENT)); + } catch (err) { + console.error("Failed to reset settings:", err); + } +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 8de8b4e..59317ec 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -6,7 +6,7 @@ import { NuqsAdapter } from "nuqs/adapters/react"; import { router } from "./router"; import { queryClient } from "./lib/query-client"; import { ToastProvider } from "./components/shared/toast-provider"; -// import { applyTheme, getTheme } from "./lib/theme"; +import { applyTheme, getTheme } from "./lib/theme"; import "./index.css"; declare module "@tanstack/react-router" { @@ -15,7 +15,7 @@ declare module "@tanstack/react-router" { } } -// applyTheme(getTheme()); +applyTheme(getTheme()); ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/ui/src/routes/index.tsx b/ui/src/routes/index.tsx index b74e97e..a1d9341 100644 --- a/ui/src/routes/index.tsx +++ b/ui/src/routes/index.tsx @@ -1,19 +1,41 @@ import { createFileRoute } from "@tanstack/react-router"; import { StatCard } from "@/features/dashboard/components/stat-card"; import { RecentTraces } from "@/features/dashboard/components/recent-traces"; +import { DashboardSkeleton } from "@/features/dashboard/components/dashboard-skeleton"; +import { TracesOverTimeChart } from "@/features/dashboard/components/traces-over-time-chart"; +import { ServiceBreakdownChart } from "@/features/dashboard/components/service-breakdown-chart"; import { useDashboardData } from "@/features/dashboard/hooks/use-dashboard-data"; -import { LoadingState } from "@/components/shared/loading-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; +import { formatDurationCompact, formatNumber } from "@/lib/formatters"; export const Route = createFileRoute("/")({ component: DashboardPage, }); +function EmptyDashboard() { + return ( +
+

No data yet

+

+ Point your OpenTelemetry exporter at faze to start collecting traces, + logs, and metrics. +

+
+
OTLP gRPC: localhost:4317
+
OTLP HTTP: localhost:4318
+
+
+ ); +} + function DashboardPage() { - const { traces, stats, isLoading } = useDashboardData(); + const { stats, recentTraces, isLoading, error, refetch } = + useDashboardData(); - if (isLoading) { - return ; - } + const errorRate = + stats && stats.traces.total > 0 + ? ((stats.traces.errors / stats.traces.total) * 100).toFixed(1) + : "0.0"; return (
@@ -24,14 +46,49 @@ function DashboardPage() {

-
- - - - -
+ } + > + {stats && stats.traces.total === 0 ? ( + + ) : ( + stats && ( + <> +
+ + + + +
+ +
+ + +
- + + + ) + )} +
); } diff --git a/ui/src/routes/logs/index.tsx b/ui/src/routes/logs/index.tsx index 22fed72..1f9d0fe 100644 --- a/ui/src/routes/logs/index.tsx +++ b/ui/src/routes/logs/index.tsx @@ -3,8 +3,7 @@ import { LogsHeader } from "@/features/logs/components/logs-header"; import { LogsList } from "@/features/logs/components/logs-list"; import { LogFilters } from "@/features/logs/components/log-filters"; import { useLogsData } from "@/features/logs/hooks/use-logs-data"; -import { LoadingState } from "@/components/shared/loading-state"; -import { ErrorState } from "@/components/shared/error-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; import { Pagination } from "@/components/shared/pagination"; export const Route = createFileRoute("/logs/")({ @@ -23,43 +22,36 @@ function LogsPage() { refetch, } = useLogsData(); - if (isLoading) { - return ; - } - - if (error) { - return ( - refetch()} - /> - ); - } - return ( -
- - - updateFilter("service", value)} - selectedLevel={filters.level} - onLevelChange={(value) => updateFilter("level", value)} - searchQuery={filters.search} - onSearchChange={(value) => updateFilter("search", value)} - /> - - - - -
+ refetch()} + > +
+ + + updateFilter("service", value)} + selectedLevel={filters.level} + onLevelChange={(value) => updateFilter("level", value)} + searchQuery={filters.search} + onSearchChange={(value) => updateFilter("search", value)} + /> + + + + +
+
); } diff --git a/ui/src/routes/metrics/index.tsx b/ui/src/routes/metrics/index.tsx index 53ad777..e282834 100644 --- a/ui/src/routes/metrics/index.tsx +++ b/ui/src/routes/metrics/index.tsx @@ -3,8 +3,7 @@ import { MetricsHeader } from "@/features/metrics/components/metrics-header"; import { MetricsGrid } from "@/features/metrics/components/metrics-grid"; import { MetricFilters } from "@/features/metrics/components/metric-filters"; import { useMetricsData } from "@/features/metrics/hooks/use-metrics-data"; -import { LoadingState } from "@/components/shared/loading-state"; -import { ErrorState } from "@/components/shared/error-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; export const Route = createFileRoute("/metrics/")({ component: MetricsPage, @@ -21,30 +20,23 @@ function MetricsPage() { refetch, } = useMetricsData(); - if (isLoading) { - return ; - } - - if (error) { - return ( - refetch()} - /> - ); - } - return ( -
- + refetch()} + > +
+ - updateFilter("service", value)} - /> + updateFilter("service", value)} + /> - -
+ +
+ ); } diff --git a/ui/src/routes/services/$serviceName.tsx b/ui/src/routes/services/$serviceName.tsx index c0f08e0..0e95c57 100644 --- a/ui/src/routes/services/$serviceName.tsx +++ b/ui/src/routes/services/$serviceName.tsx @@ -1,7 +1,6 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { useServiceDetails } from "@/features/services/hooks/use-service-details"; -import { LoadingState } from "@/components/shared/loading-state"; -import { ErrorState } from "@/components/shared/error-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; import { ServiceOverview } from "@/features/services/components/service-overview"; import { ServiceChart } from "@/features/services/components/service-chart"; import { ServiceTraces } from "@/features/services/components/service-traces"; @@ -27,48 +26,44 @@ function ServiceDetailPage() { refetch, } = useServiceDetails(decodedServiceName); - if (isLoading) { - return ; - } - - if (error) { - return ( - refetch()} - /> - ); - } - return ( -
-
- - ← Back to services - -
+ refetch()} + > +
+
+ + ← Back to services + +
-
-

{decodedServiceName}

-

- Service overview and metrics -

-
+
+

{decodedServiceName}

+

+ Service overview and metrics +

+
- + -
- -
+
+ +
-
- - - +
+ + + +
-
+
); } diff --git a/ui/src/routes/services/index.tsx b/ui/src/routes/services/index.tsx index c4ae543..e37259e 100644 --- a/ui/src/routes/services/index.tsx +++ b/ui/src/routes/services/index.tsx @@ -2,8 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { ServicesHeader } from "@/features/services/components/services-header"; import { ServicesList } from "@/features/services/components/services-list"; import { useServicesData } from "@/features/services/hooks/use-services-data"; -import { LoadingState } from "@/components/shared/loading-state"; -import { ErrorState } from "@/components/shared/error-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; export const Route = createFileRoute("/services/")({ component: ServicesPage, @@ -12,23 +11,16 @@ export const Route = createFileRoute("/services/")({ function ServicesPage() { const { services, count, isLoading, error, refetch } = useServicesData(); - if (isLoading) { - return ; - } - - if (error) { - return ( - refetch()} - /> - ); - } - return ( -
- - -
+ refetch()} + > +
+ + +
+
); } diff --git a/ui/src/routes/settings/index.tsx b/ui/src/routes/settings/index.tsx index f41acf0..6e6e41b 100644 --- a/ui/src/routes/settings/index.tsx +++ b/ui/src/routes/settings/index.tsx @@ -1,8 +1,21 @@ import { createFileRoute } from "@tanstack/react-router"; import { useState } from "react"; -import { getSettings, saveSettings, type Settings } from "@/lib/settings"; +import { + getSettings, + saveSettings, + resetSettings, + type Settings, +} from "@/lib/settings"; import { useToast } from "@/components/shared/toast-provider"; import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; export const Route = createFileRoute("/settings/")({ component: SettingsPage, @@ -10,21 +23,19 @@ export const Route = createFileRoute("/settings/")({ function SettingsPage() { const [settings, setSettings] = useState(getSettings()); + const [resetDialogOpen, setResetDialogOpen] = useState(false); const { showToast } = useToast(); const handleSave = () => { saveSettings(settings); showToast("Settings saved", "success"); - window.location.reload(); }; const handleReset = () => { - const confirmed = confirm("Are you sure you want to reset all settings?"); - if (confirmed) { - localStorage.clear(); - showToast("Settings reset", "success"); - setTimeout(() => window.location.reload(), 500); - } + resetSettings(); + setSettings(getSettings()); + setResetDialogOpen(false); + showToast("Settings reset", "success"); }; return ( @@ -94,16 +105,16 @@ function SettingsPage() {
-

Database

+

Reset

- Clear local database and reset application state + Restore all settings to their defaults

@@ -120,6 +131,34 @@ function SettingsPage() {
+ + + + + + Reset settings? + + + All settings will be restored to their defaults. This cannot be + undone. + + + + + + + +
); } diff --git a/ui/src/routes/traces/$traceId.tsx b/ui/src/routes/traces/$traceId.tsx index e46a04a..bdd4001 100644 --- a/ui/src/routes/traces/$traceId.tsx +++ b/ui/src/routes/traces/$traceId.tsx @@ -1,8 +1,7 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { TraceDetails } from "@/features/traces/components/trace-details"; import { useTraceDetails } from "@/features/traces/hooks/use-trace-details"; -import { LoadingState } from "@/components/shared/loading-state"; -import { ErrorState } from "@/components/shared/error-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; export const Route = createFileRoute("/traces/$traceId")({ component: TraceDetailPage, @@ -12,40 +11,27 @@ function TraceDetailPage() { const { traceId } = Route.useParams(); const { trace, isLoading, error, refetch } = useTraceDetails(traceId); - if (isLoading) { - return ; - } - - if (error) { - return ( - refetch()} - /> - ); - } - - if (!trace) { - return ( -
-

Trace not found

-
- ); - } - return ( -
-
- - ← Back to traces - -
+ refetch()} + isEmpty={!trace} + emptyMessage="Trace not found" + > +
+
+ + ← Back to traces + +
- -
+ {trace && } +
+ ); } diff --git a/ui/src/routes/traces/index.tsx b/ui/src/routes/traces/index.tsx index 28b62c8..893ba28 100644 --- a/ui/src/routes/traces/index.tsx +++ b/ui/src/routes/traces/index.tsx @@ -3,8 +3,7 @@ import { TracesHeader } from "@/features/traces/components/traces-header"; import { TracesTable } from "@/features/traces/components/traces-table"; import { TraceFilters } from "@/features/traces/components/trace-filters"; import { useTracesData } from "@/features/traces/hooks/use-traces-data"; -import { LoadingState } from "@/components/shared/loading-state"; -import { ErrorState } from "@/components/shared/error-state"; +import { QueryBoundary } from "@/components/shared/query-boundary"; import { Pagination } from "@/components/shared/pagination"; export const Route = createFileRoute("/traces/")({ @@ -29,45 +28,38 @@ function TracesPage() { refetch, } = useTracesData(); - if (isLoading) { - return ; - } - - if (error) { - return ( - refetch()} - /> - ); - } - return ( -
- + refetch()} + > +
+ - updateFilter("service", value)} - minDuration={filters.min_duration} - maxDuration={filters.max_duration} - onMinDurationChange={(value) => updateFilter("min_duration", value)} - onMaxDurationChange={(value) => updateFilter("max_duration", value)} - searchQuery={filters.search} - onSearchChange={(value) => updateFilter("search", value)} - /> + updateFilter("service", value)} + minDuration={filters.min_duration} + maxDuration={filters.max_duration} + onMinDurationChange={(value) => updateFilter("min_duration", value)} + onMaxDurationChange={(value) => updateFilter("max_duration", value)} + searchQuery={filters.search} + onSearchChange={(value) => updateFilter("search", value)} + /> - + - -
+ +
+ ); } diff --git a/ui/src/test/e2e/chart-navigation.test.tsx b/ui/src/test/e2e/chart-navigation.test.tsx index 2594837..78af74e 100644 --- a/ui/src/test/e2e/chart-navigation.test.tsx +++ b/ui/src/test/e2e/chart-navigation.test.tsx @@ -22,7 +22,7 @@ describe("Chart Navigation E2E", () => { has_errors: false, start_time: 1000000000000, root_span_name: "GET /api/users", - root_span_kind: "Server", + root_span_kind: "SERVER", }, { trace_id: "trace-def456", @@ -32,7 +32,7 @@ describe("Chart Navigation E2E", () => { has_errors: false, start_time: 1000000001000, root_span_name: "POST /auth/login", - root_span_kind: "Server", + root_span_kind: "SERVER", }, { trace_id: "trace-ghi789", @@ -42,7 +42,7 @@ describe("Chart Navigation E2E", () => { has_errors: true, start_time: 1000000002000, root_span_name: "SELECT * FROM users WHERE id = ?", - root_span_kind: "Client", + root_span_kind: "CLIENT", }, ]; @@ -120,7 +120,7 @@ describe("Chart Navigation E2E", () => { has_errors: false, start_time: 1000000000000 + i * 1000, root_span_name: `Operation ${i}`, - root_span_kind: "Server", + root_span_kind: "SERVER", })); render(); diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index fb85367..514730a 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -4,3 +4,4 @@ export * from "./log"; export * from "./metric"; export * from "./project"; export * from "./service"; +export * from "./stats"; diff --git a/ui/src/types/log.ts b/ui/src/types/log.ts index 62f4b3c..7ad0fc6 100644 --- a/ui/src/types/log.ts +++ b/ui/src/types/log.ts @@ -1,31 +1,31 @@ import type { Attributes } from "./common"; export type SeverityLevel = - | "Unspecified" - | "Trace" - | "Trace2" - | "Trace3" - | "Trace4" - | "Debug" - | "Debug2" - | "Debug3" - | "Debug4" - | "Info" - | "Info2" - | "Info3" - | "Info4" - | "Warn" - | "Warn2" - | "Warn3" - | "Warn4" - | "Error" - | "Error2" - | "Error3" - | "Error4" - | "Fatal" - | "Fatal2" - | "Fatal3" - | "Fatal4"; + | "UNSPECIFIED" + | "TRACE" + | "TRACE2" + | "TRACE3" + | "TRACE4" + | "DEBUG" + | "DEBUG2" + | "DEBUG3" + | "DEBUG4" + | "INFO" + | "INFO2" + | "INFO3" + | "INFO4" + | "WARN" + | "WARN2" + | "WARN3" + | "WARN4" + | "ERROR" + | "ERROR2" + | "ERROR3" + | "ERROR4" + | "FATAL" + | "FATAL2" + | "FATAL3" + | "FATAL4"; export interface Log { time_unix_nano: number; @@ -41,5 +41,6 @@ export interface Log { export interface LogFilters { service?: string; level?: string; + trace_id?: string; limit?: number; } diff --git a/ui/src/types/metric.ts b/ui/src/types/metric.ts index 5c71b10..2344e4f 100644 --- a/ui/src/types/metric.ts +++ b/ui/src/types/metric.ts @@ -1,8 +1,8 @@ import type { Attributes } from "./common"; -export type MetricType = "Gauge" | "Sum" | "Histogram" | "Summary"; +export type MetricType = "GAUGE" | "SUM" | "HISTOGRAM" | "SUMMARY"; -export type AggregationTemporality = "Unspecified" | "Delta" | "Cumulative"; +export type AggregationTemporality = "UNSPECIFIED" | "DELTA" | "CUMULATIVE"; export interface MetricDataPoint { time_unix_nano: number; diff --git a/ui/src/types/stats.ts b/ui/src/types/stats.ts new file mode 100644 index 0000000..2967b09 --- /dev/null +++ b/ui/src/types/stats.ts @@ -0,0 +1,26 @@ +export interface TraceStatsSummary { + total: number; + errors: number; + avg_duration_ms: number; +} + +export interface ServiceStat { + name: string; + trace_count: number; + error_count: number; +} + +export interface ActivityBucket { + bucket_start_unix_nano: number; + total: number; + errors: number; +} + +export interface StatsResponse { + spans: number; + logs: number; + metrics: number; + traces: TraceStatsSummary; + services: ServiceStat[]; + activity: ActivityBucket[]; +} diff --git a/ui/src/types/trace.ts b/ui/src/types/trace.ts index 41f38c4..4c31c62 100644 --- a/ui/src/types/trace.ts +++ b/ui/src/types/trace.ts @@ -1,14 +1,14 @@ import type { Attributes } from "./common"; export type SpanKind = - | "Unspecified" - | "Internal" - | "Server" - | "Client" - | "Producer" - | "Consumer"; - -export type StatusCode = "Unset" | "Ok" | "Error"; + | "UNSPECIFIED" + | "INTERNAL" + | "SERVER" + | "CLIENT" + | "PRODUCER" + | "CONSUMER"; + +export type StatusCode = "UNSET" | "OK" | "ERROR"; export interface Status { code: StatusCode; diff --git a/ui/vite.config.ts b/ui/vite.config.ts index ce6e462..2ba3f52 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -18,6 +18,12 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + server: { + proxy: { + "/api": "http://localhost:7070", + "/health": "http://localhost:7070", + }, + }, test: { globals: true, environment: "jsdom",