diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index 33abd2b8..00000000 --- a/public/favicon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/api/admin/auth.rs b/src/api/admin/auth.rs index 1332ce67..1c9aa504 100644 --- a/src/api/admin/auth.rs +++ b/src/api/admin/auth.rs @@ -465,8 +465,7 @@ impl AdminAuth { AdminRouteFamilyAccess::Explicit(values) => values.iter().all(|value| { value .parse::() - .ok() - .is_some_and(|family| provisioned.contains(&family)) + .is_ok_and(|family| provisioned.contains(&family)) }), } } diff --git a/src/api/handlers/websocket.rs b/src/api/handlers/websocket.rs index 2695a4bc..5523a0ee 100644 --- a/src/api/handlers/websocket.rs +++ b/src/api/handlers/websocket.rs @@ -567,8 +567,10 @@ mod tests { // Act let close_reason = websocket_close_reason(&result); - // Assert - assert_eq!(reason, "session frame error: Backpressure(Control)"); + // Assert: a non-timeout session error maps to CloseReason::Error (not + // Timeout/ClientClose), and the close carries the same reason that was + // produced — not a fixed string, since the reason text is itself derived + // from the SessionError and may evolve. assert!(matches!( close_reason, CloseReason::Error(message) if message == reason diff --git a/src/api/ingress.rs b/src/api/ingress.rs index 3d1fdad8..88d5fb54 100644 --- a/src/api/ingress.rs +++ b/src/api/ingress.rs @@ -174,26 +174,36 @@ mod tests { #[test] fn should_display_ingress_errors() { - // Arrange - let errors = vec![ + // Arrange / Act / Assert: each variant's Display impl must actually + // surface its structured data, not just a static label — this is + // what operators and clients see in logs/error responses. + let frame_too_large = format!( + "{}", IngressError::FrameTooLarge { size: 2048, max: 1024, - }, - IngressError::TooManyConnections, - IngressError::BackpressureFull, - IngressError::SessionNotFound(123), - IngressError::InvalidFrame("missing length prefix".to_string()), - IngressError::TransportError("connection reset".to_string()), - ]; - - // Act - let mut outputs: Vec = Vec::new(); - for error in errors { - outputs.push(format!("{error}")); - } - - // Assert - assert_eq!(outputs.len(), 6); + } + ); + assert!(frame_too_large.contains("2048")); + assert!(frame_too_large.contains("1024")); + assert!(format!("{}", IngressError::SessionNotFound(123)).contains("123")); + assert!(format!( + "{}", + IngressError::InvalidFrame("missing length prefix".to_string()) + ) + .contains("missing length prefix")); + assert!(format!( + "{}", + IngressError::TransportError("connection reset".to_string()) + ) + .contains("connection reset")); + + // Variants with no embedded data should still format to something + // non-empty and distinct from one another. + let too_many = format!("{}", IngressError::TooManyConnections); + let backpressure = format!("{}", IngressError::BackpressureFull); + assert!(!too_many.is_empty()); + assert!(!backpressure.is_empty()); + assert_ne!(too_many, backpressure); } } diff --git a/src/api/tcp.rs b/src/api/tcp.rs index aa721a7e..20e0adcd 100644 --- a/src/api/tcp.rs +++ b/src/api/tcp.rs @@ -293,30 +293,33 @@ mod tests { #[test] fn should_encode_length_prefix() { - // Arrange - let data = [1, 2, 3, 4, 5]; - let len = u32::try_from(data.len()).expect("test frame length fits in u32"); + // Arrange: a real length-prefixed frame buffer as `frame_len` expects + // to receive it off the wire (4-byte big-endian length + payload). + let data = [1u8, 2, 3, 4, 5]; + let mut buffer = BytesMut::new(); + let encoded_len = u32::try_from(data.len()).expect("test frame length should fit in u32"); + buffer.extend_from_slice(&encoded_len.to_be_bytes()); + buffer.extend_from_slice(&data); // Act - let len_bytes = len.to_be_bytes(); - let reconstructed = - usize::try_from(u32::from_be_bytes(len_bytes)).expect("u32 length fits in usize"); + let decoded_len = frame_len(&buffer); // Assert - assert_eq!(reconstructed, 5); + assert_eq!(decoded_len, 5); } #[test] fn should_handle_large_frames() { // Arrange - let large_len: i32 = 1024 * 1024; // 1 MB + let large_len: u32 = 1024 * 1024; // 1 MB + let mut buffer = BytesMut::new(); + buffer.extend_from_slice(&large_len.to_be_bytes()); + buffer.extend_from_slice(&[0u8; 4]); // frame_len only reads the prefix // Act - let len_bytes = large_len.cast_unsigned().to_be_bytes(); - let reconstructed = - usize::try_from(u32::from_be_bytes(len_bytes)).expect("u32 length fits in usize"); + let decoded_len = frame_len(&buffer); // Assert - assert_eq!(reconstructed, 1024 * 1024); + assert_eq!(decoded_len, 1024 * 1024); } } diff --git a/src/boot/domains.rs b/src/boot/domains.rs index 6faaa495..3bd425d2 100644 --- a/src/boot/domains.rs +++ b/src/boot/domains.rs @@ -704,11 +704,6 @@ mod tests { panic!("{domain} actor did not fail closed: {snapshots:?}"); } - #[test] - fn should_define_domain_setup() { - // Placeholder: Domain setup structure is well-defined - } - #[test] fn should_create_domain_sinks() { // Arrange diff --git a/src/boot/storage/tests.rs b/src/boot/storage/tests.rs index 8f7990d4..46dbae1c 100644 --- a/src/boot/storage/tests.rs +++ b/src/boot/storage/tests.rs @@ -64,21 +64,6 @@ fn should_detect_local_storage_by_default() { } } -#[test] -fn should_support_memory_storage_mode() { - // Arrange - let config = BootConfig::with_memory_storage(); - - // Act - let is_memory_mode = matches!( - config.storage_mode, - crate::boot::runtime::StorageMode::Memory - ); - - // Assert - assert!(is_memory_mode); -} - #[tokio::test] async fn should_provision_configured_route_family_column_families() { // Arrange diff --git a/src/boot/tests.rs b/src/boot/tests.rs index 399c75e1..58dc57fa 100644 --- a/src/boot/tests.rs +++ b/src/boot/tests.rs @@ -3,12 +3,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; const BOOT_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45); -#[test] -fn should_define_boot_module() { - // Placeholder: Module structure is well-defined and - // submodules are unit-testable in isolation -} - #[tokio::test] async fn should_translate_runtime_drain_into_planned_shutdown() { // Arrange diff --git a/src/domains/notice/bench.rs b/src/domains/notice/bench.rs deleted file mode 100644 index b2c5d09b..00000000 --- a/src/domains/notice/bench.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Zero-copy notification primitives for benchmarking -//! -//! This module provides allocation-free matching and fanout primitives -//! used to measure notification domain hot-path performance. -//! -//! **Purpose**: Performance benchmarking only -//! **Not used in production code**: These are simplified primitives for measuring overhead - -use crate::dispatch::protocol::tlv::MessageType; - -/// Subscriber identifier -pub type SubscriberId = usize; - -/// Minimal matcher: map exact message types to subscriber id lists. -/// Zero-copy matching via `matching_subscribers` that returns `Option<&[SubscriberId]>`. -/// -/// Added `match_into(&mut SmallVec)` API for a hot-path, allocation-free write into a -/// caller-provided buffer (benchable in isolation). -#[derive(Debug, Clone, Default)] -pub struct BenchMatchStub { - subs: Vec<(u16, Vec)>, -} - -impl BenchMatchStub { - #[must_use] - pub fn new() -> Self { - Self { subs: Vec::new() } - } - - /// Register a subscriber for a given message type - pub fn register(&mut self, msg_type: u16, sub: SubscriberId) { - // fast path: push onto existing bucket - if let Some((_t, vec)) = self.subs.iter_mut().find(|e| e.0 == msg_type) { - vec.push(sub); - return; - } - self.subs.push((msg_type, vec![sub])); - } - - /// Return a borrowed slice of subscriber ids for this message (zero-alloc) - #[must_use] - pub fn matching_subscribers<'a>( - &'a self, - msg_type: MessageType, - _payload: &'a [u8], - ) -> Option<&'a [SubscriberId]> { - // For this minimal implementation, we ignore payload predicates. - self.subs - .iter() - .find(|e| e.0 == msg_type.as_u16()) - .map(|(_, v)| &v[..]) - } - - /// Match into a caller-provided `SmallVec` without allocating. - /// Returns the number of subscribers written into `out`. - pub fn match_into( - &self, - out: &mut smallvec::SmallVec<[SubscriberId; 8]>, - msg_type: MessageType, - payload: &[u8], - ) -> usize { - out.clear(); - if let Some(subs) = self.matching_subscribers(msg_type, payload) { - out.extend_from_slice(subs); - subs.len() - } else { - 0 - } - } -} - -/// Minimal fan-out stub: records deliveries as a simple counter and stores an optional -/// record of (`SubscriberId`, `payload_len`) for verification. -#[derive(Debug, Default)] -pub struct BenchFanout { - deliveries: usize, -} - -impl BenchFanout { - #[must_use] - pub fn new() -> Self { - Self { deliveries: 0 } - } - - /// Deliver to the provided subscriber ids. This is intentionally minimal and deterministic. - /// NOTE: No heap allocations or vector pushes here — only a simple counter increment and - /// a `black_box` to represent delivery work. - pub fn deliver(&mut self, subs: &[SubscriberId], payload: &[u8]) -> usize { - // Touch the payload once to avoid size-dependent behavior in per-subscriber work. - core::hint::black_box(payload.as_ptr()); - core::hint::black_box(payload.len()); - - for &id in subs { - // per-subscriber work is intentionally tiny and payload-free - core::hint::black_box(id); - self.deliveries += 1; - } - subs.len() - } - - #[must_use] - pub fn delivered_count(&self) -> usize { - self.deliveries - } -} - -/// Thin `BenchNotificationDomain` that composes a `BenchMatchStub` and a `BenchFanout` and wires the -/// match -> fanout flow. This is intentionally small to make domain boundaries explicit. -#[derive(Debug, Default)] -pub struct BenchNotificationDomain { - matcher: BenchMatchStub, - fanout: BenchFanout, -} - -impl BenchNotificationDomain { - #[must_use] - pub fn new() -> Self { - Self { - matcher: BenchMatchStub::new(), - fanout: BenchFanout::new(), - } - } - - /// Register a subscription in the underlying matcher - pub fn register(&mut self, msg_type: u16, sub: SubscriberId) { - self.matcher.register(msg_type, sub); - } - - /// Handle a single message: query matching subscribers and call fanout to perform - /// the stubbed delivery. Uses borrowed slices to avoid allocations/copies in the hot-path. - pub fn handle(&mut self, msg_type: MessageType, payload: &[u8]) -> usize { - if let Some(subs) = self.matcher.matching_subscribers(msg_type, payload) { - self.fanout.deliver(subs, payload) - } else { - 0 - } - } - - #[must_use] - pub fn fanout_delivered(&self) -> usize { - self.fanout.delivered_count() - } - - #[must_use] - pub fn matcher(&self) -> &BenchMatchStub { - &self.matcher - } -} - -#[cfg(test)] -mod tests { - use super::*; - use smallvec::SmallVec; - - #[test] - fn should_match_into_smallvec_zero_copy() { - // Arrange - let mut m = BenchMatchStub::new(); - m.register(42, 1); - m.register(42, 2); - let payload = b"hello"; - - // Act - let mut out: SmallVec<[SubscriberId; 8]> = SmallVec::new(); - let n = m.match_into(&mut out, MessageType::new(42), payload); - - // Assert - assert_eq!(n, 2); - assert_eq!(out.len(), 2); - assert!(out.contains(&1)); - assert!(out.contains(&2)); - } - - #[test] - fn should_dispatch_to_fanout_on_match() { - // Arrange - let mut d = BenchNotificationDomain::new(); - d.register(10, 3); - d.register(10, 4); - - // Act - let n = d.handle(MessageType::new(10), b"payload"); - - // Assert - assert_eq!(n, 2); - assert_eq!(d.fanout_delivered(), 2); - } - - #[test] - fn should_return_zero_when_no_subscribers() { - // Arrange - let mut d = BenchNotificationDomain::new(); - - // Act - let n = d.handle(MessageType::new(99), b"x"); - - // Assert - assert_eq!(n, 0); - assert_eq!(d.fanout_delivered(), 0); - } -} diff --git a/src/domains/notice/mod.rs b/src/domains/notice/mod.rs index f67970f0..98195c76 100644 --- a/src/domains/notice/mod.rs +++ b/src/domains/notice/mod.rs @@ -27,8 +27,6 @@ pub mod metrics; pub mod protocol; pub mod sink; -pub mod bench; // Zero-copy notification primitives for benchmarking - pub use metrics::NoticeMetrics; pub use protocol::{ DeliverMessage, NoticeClientNotification, NoticeClientRequest, NoticeClientResponse, diff --git a/src/domains/schedule/protocol_tests.rs b/src/domains/schedule/protocol_tests.rs index 69032e18..ac05657e 100644 --- a/src/domains/schedule/protocol_tests.rs +++ b/src/domains/schedule/protocol_tests.rs @@ -126,34 +126,6 @@ fn should_find_leap_day_after_non_leap_century_year() { ); } -#[test] -fn should_create_schedule_def() { - // Arrange - let route = "schedule://acme/jobs/backup/run".to_string(); - let cron = "0 */6 * * *".to_string(); - let payload = Bytes::from("backup data"); - - // Act - let parsed_cron = CronSchedule::parse(&cron).expect("Valid cron"); - let route_parts = parse_concrete_schedule_route(&route).expect("valid schedule route"); - let def = ScheduleDef { - route, - route_parts, - cron, - delivery_mode: ScheduleDeliveryMode::Broadcast, - parsed_cron, - payload, - next_fire_time: Instant::now(), - next_fire_ms: 0, - last_fire_ms: None, - executions_total: 0, - list_index: 0, - }; - - // Assert - assert_eq!(def.route, "schedule://acme/jobs/backup/run"); -} - #[test] fn should_parse_concrete_schedule_route_given_valid_route() { // Arrange diff --git a/src/domains/stream/storage/compact_page_values.rs b/src/domains/stream/storage/compact_page_values.rs index 232f2982..d65dd327 100644 --- a/src/domains/stream/storage/compact_page_values.rs +++ b/src/domains/stream/storage/compact_page_values.rs @@ -451,7 +451,9 @@ impl PostingPageValue { return Err("decode posting page value: invalid offset payload".to_string()); } let entries = bytes[6..] - .chunks_exact(24) + .as_chunks::<24>() + .0 + .iter() .map(|chunk| { let mut offset = [0u8; 8]; let mut parent = [0u8; 8]; diff --git a/src/observability/global.rs b/src/observability/global.rs index 14b0bb04..86dbc9e0 100644 --- a/src/observability/global.rs +++ b/src/observability/global.rs @@ -78,14 +78,12 @@ pub fn gauge_dec(name: &str) { /// collecting attribution data. pub fn hot_path_metrics_enabled() -> bool { *HOT_PATH_METRICS_ENABLED.get_or_init(|| { - std::env::var("FITZ_HOT_PATH_METRICS") - .ok() - .is_some_and(|value| { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "1" | "true" | "yes" | "on" - ) - }) + std::env::var("FITZ_HOT_PATH_METRICS").is_ok_and(|value| { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) }) } @@ -343,13 +341,3 @@ fn init_observability_with_options( Ok(metrics_collector) } - -#[cfg(test)] -mod tests { - #[test] - fn should_initialize_observability_once() { - // Note: This test assumes the global is not yet initialized - // In practice, you'd want to use a test harness that resets globals - // between test runs. - } -} diff --git a/src/protocol/frame_context.rs b/src/protocol/frame_context.rs index 102d0844..73458a98 100644 --- a/src/protocol/frame_context.rs +++ b/src/protocol/frame_context.rs @@ -68,28 +68,3 @@ impl std::fmt::Debug for FrameContext { .finish() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn should_create_frame_context() { - // Arrange - - // Act - let ctx = FrameContext::new( - 123, - ChannelId::Pub, - MessageType::new(103), // KV GET operation - Bytes::from("test payload"), - RouteFamily::new(1), - ); - - // Assert - assert_eq!(ctx.session_id, 123); - assert_eq!(ctx.channel_id, ChannelId::Pub); - assert_eq!(ctx.msg_type.as_u16(), 103); - assert_eq!(ctx.payload.len(), 12); - } -} diff --git a/src/protocol/tlv.rs b/src/protocol/tlv.rs index 21519d45..41bf10af 100644 --- a/src/protocol/tlv.rs +++ b/src/protocol/tlv.rs @@ -727,15 +727,4 @@ mod tests { fn should_reject_websocket_text_frame() { assert!(TlvDecoder::new().decode_all(b"text").is_err()); } - - #[test] - fn should_preserve_domain_error_shape_across_tcp_plus_websocket() { - // Arrange - // Act - // Assert - assert!(matches!( - TlvDecoder::new().decode_all(&[1]), - Err(TlvError::IncompleteLength) - )); - } } diff --git a/src/runtime/actor.rs b/src/runtime/actor.rs index c87cdaef..d19c298d 100644 --- a/src/runtime/actor.rs +++ b/src/runtime/actor.rs @@ -591,56 +591,6 @@ mod tests { Route::new(route), ) } - #[test] - fn should_create_actor_id() { - // Arrange - let id = 42; - - // Act - let actor_id = ActorId::new(id); - - // Assert - assert_eq!(actor_id.as_u64(), id); - } - - #[test] - fn should_compare_equal_actor_ids() { - // Arrange - let id1 = ActorId::new(1); - let id2 = ActorId::new(1); - - // Act - let are_equal = id1 == id2; - - // Assert - assert!(are_equal); - } - - #[test] - fn should_compare_unequal_actor_ids() { - // Arrange - let id1 = ActorId::new(1); - let id2 = ActorId::new(2); - - // Act - let are_equal = id1 == id2; - - // Assert - assert!(!are_equal); - } - - #[test] - fn should_format_actor_id() { - // Arrange - let actor_id = ActorId::new(123); - - // Act - let formatted = format!("{actor_id}"); - - // Assert - assert_eq!(formatted, "Actor(123)"); - } - #[test] fn should_create_context_with_running_state() { // Arrange diff --git a/src/runtime/matcher.rs b/src/runtime/matcher.rs index 52d2881c..fd9b1232 100644 --- a/src/runtime/matcher.rs +++ b/src/runtime/matcher.rs @@ -443,30 +443,6 @@ mod tests { assert!(result); } - #[test] - fn should_match_single_star_wildcard_update() { - // Arrange - let pattern = Pattern::new("notice://acme/orders/*"); - - // Act - let result = pattern.matches(&route("notice://acme/orders/update")); - - // Assert - assert!(result); - } - - #[test] - fn should_match_single_star_wildcard_delete() { - // Arrange - let pattern = Pattern::new("notice://acme/orders/*"); - - // Act - let result = pattern.matches(&route("notice://acme/orders/delete")); - - // Assert - assert!(result); - } - #[test] fn should_not_match_across_single_star_boundary() { let pattern = Pattern::new("notice://acme/orders/*"); @@ -570,18 +546,6 @@ mod tests { assert!(result); } - #[test] - fn should_match_multiple_wildcards_inventory() { - // Arrange - let pattern = Pattern::new("notice://acme/*/*/created"); - - // Act - let result = pattern.matches(&route("notice://acme/inventory/check/created")); - - // Assert - assert!(result); - } - #[test] fn should_not_match_multiple_wildcards_insufficient_segments() { // Arrange @@ -606,18 +570,6 @@ mod tests { assert!(result); } - #[test] - fn should_match_pattern_without_scheme_update() { - // Arrange - let pattern = Pattern::new("acme/orders/*"); - - // Act - let result = pattern.matches(&route("acme/orders/update")); - - // Assert - assert!(result); - } - #[test] fn should_match_double_star_with_no_segments() { // Arrange diff --git a/src/runtime/routing.rs b/src/runtime/routing.rs index d0a33954..6b72a751 100644 --- a/src/runtime/routing.rs +++ b/src/runtime/routing.rs @@ -523,18 +523,6 @@ impl fmt::Display for RouteAddress { mod tests { use super::*; - #[test] - fn should_create_route_family() { - // Arrange - // (no setup needed) - - // Act - let family = RouteFamily::new(1); - - // Assert - assert_eq!(family.id(), 1); - } - #[test] fn should_reject_route_family_values_above_u32_range() { // Arrange @@ -547,81 +535,6 @@ mod tests { assert!(result.is_err()); } - #[test] - fn should_compare_route_families_by_identity() { - // Arrange - let family1 = RouteFamily::new(1); - let family2 = RouteFamily::new(1); - let family3 = RouteFamily::new(2); - - // Act - let eq_result = family1 == family2; - let ne_result = family1 != family3; - - // Assert - assert!(eq_result); - assert!(ne_result); - } - - #[test] - fn should_hash_route_families_consistently() { - // Arrange - let family1 = RouteFamily::new(1); - let family2 = RouteFamily::new(1); - - let mut hasher1 = std::collections::hash_map::DefaultHasher::new(); - let mut hasher2 = std::collections::hash_map::DefaultHasher::new(); - - // Act - family1.hash(&mut hasher1); - family2.hash(&mut hasher2); - - // Assert - assert_eq!(hasher1.finish(), hasher2.finish()); - } - - #[test] - fn should_create_route() { - // Arrange - // (no setup needed) - - // Act - let route = Route::new("/user/123"); - - // Assert - assert_eq!(route.as_str(), "/user/123"); - } - - #[test] - fn should_compare_routes_by_path() { - // Arrange - let route1 = Route::new("/user/123"); - let route2 = Route::new("/user/123"); - let route3 = Route::new("/user/456"); - - // Act - let eq_result = route1 == route2; - let ne_result = route1 != route3; - - // Assert - assert!(eq_result); - assert!(ne_result); - } - - #[test] - fn should_create_route_address() { - // Arrange - let family = RouteFamily::new(100); - let route = Route::new("/service/method"); - - // Act - let address = RouteAddress::new(family, route.clone()); - - // Assert - assert_eq!(address.family(), &family); - assert_eq!(address.route(), &route); - } - #[test] fn should_isolate_same_route_in_different_families() { // Arrange diff --git a/src/runtime/subscriptions/tests.rs b/src/runtime/subscriptions/tests.rs index 848db8dc..07cdcd0b 100644 --- a/src/runtime/subscriptions/tests.rs +++ b/src/runtime/subscriptions/tests.rs @@ -438,34 +438,6 @@ fn should_match_single_star_for_create_operation() { assert_eq!(matches.as_slice(), &[sub_id(1)]); } -#[test] -fn should_match_single_star_for_update_operation() { - // Arrange - let mut index = SubscriptionIndex::new(); - let f = family(1); - index.insert(f, &route("notify://realm/orders/*"), sub_id(1)); - - // Act - let matches = index.match_all(f, &route("notify://realm/orders/update")); - - // Assert - assert_eq!(matches.as_slice(), &[sub_id(1)]); -} - -#[test] -fn should_match_single_star_for_delete_operation() { - // Arrange - let mut index = SubscriptionIndex::new(); - let f = family(1); - index.insert(f, &route("notify://realm/orders/*"), sub_id(1)); - - // Act - let matches = index.match_all(f, &route("notify://realm/orders/delete")); - - // Assert - assert_eq!(matches.as_slice(), &[sub_id(1)]); -} - #[test] fn should_match_star_with_multiple_wildcards() { // Arrange diff --git a/src/runtime/supervision.rs b/src/runtime/supervision.rs index 8d8ec7ac..b1f33d85 100644 --- a/src/runtime/supervision.rs +++ b/src/runtime/supervision.rs @@ -141,24 +141,6 @@ mod tests { } } - #[test] - fn should_create_stop_strategy() { - let strategy = SupervisorStrategy::stop(); - assert!(matches!(strategy, SupervisorStrategy::Stop)); - } - - #[test] - fn should_create_escalate_strategy() { - let strategy = SupervisorStrategy::escalate(); - assert!(matches!(strategy, SupervisorStrategy::Escalate)); - } - - #[test] - fn should_create_resume_strategy() { - let strategy = SupervisorStrategy::resume(); - assert!(matches!(strategy, SupervisorStrategy::Resume)); - } - #[test] fn should_decide_restart_action() { // Arrange diff --git a/tests/dependency_drift_workflow.rs b/tests/dependency_drift_workflow.rs index 6c74f645..5c4dde26 100644 --- a/tests/dependency_drift_workflow.rs +++ b/tests/dependency_drift_workflow.rs @@ -1,58 +1,41 @@ -const WORKFLOW: &str = include_str!("../.github/workflows/dependency-drift.yml"); const CARGO_MANIFEST: &str = include_str!("../Cargo.toml"); -#[test] -fn should_check_each_git_main_dependency_on_a_weekly_schedule() { - // Arrange - let dependency_names = ["cntryl-lexkey", "cntryl-midge", "cntryl-stress"]; - - // Act - let missing = dependency_names - .into_iter() - .filter(|name| !WORKFLOW.contains(name)) - .collect::>(); - - // Assert - assert!(WORKFLOW.contains("cron:")); - assert!(missing.is_empty(), "missing dependency checks: {missing:?}"); -} - +/// These internal `cntryl-*` crates are deliberately tracked against their +/// `main` branch rather than a pinned rev or a published crates.io version, +/// because no published release exists yet. If a dependency line silently +/// drifts to a pinned rev/tag or drops the `main` branch tracking, that's an +/// intentional-looking change that should be caught, not waved through. #[test] fn should_track_internal_git_dependencies_on_main_until_published_releases_exist() { // Arrange - let expected_dependencies = [ - r#"cntryl-lexkey = { git = "https://github.com/cntryl/lexkey-rs", branch = "main" }"#, - r#"cntryl-midge = { git = "https://github.com/cntryl/midge", branch = "main" }"#, - r#"cntryl-stress = { git = "https://github.com/cntryl/stress", branch = "main" }"#, + let expected = [ + ("cntryl-lexkey", "https://github.com/cntryl/lexkey-rs"), + ("cntryl-midge", "https://github.com/cntryl/midge"), + ("cntryl-stress", "https://github.com/cntryl/stress"), ]; // Act - let missing = expected_dependencies - .into_iter() - .filter(|dependency| !CARGO_MANIFEST.contains(dependency)) - .collect::>(); - - // Assert - assert!( - missing.is_empty(), - "dependencies not tracking main: {missing:?}" - ); -} - -#[test] -fn should_have_permission_plus_logic_to_report_drift_once() { - // Arrange - let required_contract = ["issues: write", "ahead_by", "total_count === 0"]; - - // Act - let missing = required_contract - .into_iter() - .filter(|fragment| !WORKFLOW.contains(fragment)) - .collect::>(); + let dependency_line = |name: &str| { + CARGO_MANIFEST + .lines() + .find(|line| line.trim_start().starts_with(&format!("{name} ="))) + }; // Assert - assert!( - missing.is_empty(), - "missing drift-reporting contract: {missing:?}" - ); + for (name, repo_url) in expected { + let line = dependency_line(name) + .unwrap_or_else(|| panic!("Cargo.toml is missing a dependency line for {name}")); + assert!( + line.contains(repo_url), + "{name} should point at {repo_url}, found: {line}" + ); + assert!( + line.contains("branch") && line.contains("main"), + "{name} should track the main branch (no published release exists yet), found: {line}" + ); + assert!( + !line.contains("rev =") && !line.contains("tag ="), + "{name} should not be pinned to a rev/tag while tracking main, found: {line}" + ); + } } diff --git a/tests/observability_metrics.rs b/tests/observability_metrics.rs index 0d139601..003e330d 100644 --- a/tests/observability_metrics.rs +++ b/tests/observability_metrics.rs @@ -198,87 +198,6 @@ mod tests { assert_eq!(mc.counter_get("concurrent_test"), 1000); } - #[test] - fn should_support_metric_constants() { - // Arrange - let mc = MetricsCollector::new(); - - // Act - // Test that constants are accessible - mc.counter_inc(obs::METRIC_CONNECTIONS_OPENED); - mc.counter_add(obs::METRIC_FRAMES_RECEIVED, 5); - mc.gauge_set(obs::METRIC_CONNECTIONS_ACTIVE, 10); - mc.histogram_observe_ms(obs::METRIC_MESSAGE_LATENCY, 100); - - // Assert - assert!(mc.counter_get(obs::METRIC_CONNECTIONS_OPENED) > 0); - assert_eq!(mc.counter_get(obs::METRIC_FRAMES_RECEIVED), 5); - assert_eq!(mc.gauge_get(obs::METRIC_CONNECTIONS_ACTIVE), 10); - } - - #[test] - fn should_include_all_required_metrics() { - // Arrange - // Verify that all expected metric names are defined as constants - - // Act - // Counters - let _ = obs::METRIC_CONNECTIONS_OPENED; - let _ = obs::METRIC_CONNECTIONS_CLOSED; - let _ = obs::METRIC_FRAMES_RECEIVED; - let _ = obs::METRIC_FRAMES_SENT; - let _ = obs::METRIC_ROUTE_MISMATCHES; - let _ = obs::METRIC_DELIVERY_FAILURES; - let _ = obs::METRIC_PERMISSION_DENIALS; - let _ = obs::METRIC_DOMAIN_OPERATIONS; - - // Gauges - let _ = obs::METRIC_CONNECTIONS_ACTIVE; - let _ = obs::METRIC_SESSIONS_ACTIVE; - let _ = obs::METRIC_MAILBOX_DEPTH; - - // Histograms - let _ = obs::METRIC_MESSAGE_LATENCY; - let _ = obs::METRIC_PERMISSION_CHECK_LATENCY; - let _ = obs::METRIC_DOMAIN_OPERATION_LATENCY; - - // Assert - } - - #[test] - fn should_include_all_required_span_names() { - // Arrange - // Verify that all expected span names are defined - - // Act - let _ = obs::SPAN_REQUEST; - let _ = obs::SPAN_TLV_ENCODE; - let _ = obs::SPAN_TLV_DECODE; - let _ = obs::SPAN_ROUTE_MATCH; - let _ = obs::SPAN_PERMISSION_CHECK; - let _ = obs::SPAN_DOMAIN_OPERATION; - - // Assert - } - - #[test] - fn should_include_all_required_attribute_keys() { - // Arrange - // Verify that all expected attribute keys are defined - - // Act - let _ = obs::ATTR_MESSAGE_ID; - let _ = obs::ATTR_ROUTE; - let _ = obs::ATTR_DOMAIN; - let _ = obs::ATTR_REALM; - let _ = obs::ATTR_SESSION_ID; - let _ = obs::ATTR_ACTOR_ID; - let _ = obs::ATTR_OPERATION; - let _ = obs::ATTR_ERROR_TYPE; - - // Assert - } - #[test] fn should_define_sampling_ratios() { // Arrange diff --git a/tests/observability_spans.rs b/tests/observability_spans.rs index d1a97beb..9a3fec3a 100644 --- a/tests/observability_spans.rs +++ b/tests/observability_spans.rs @@ -46,11 +46,4 @@ mod tests { // Assert assert!((0.005..0.1).contains(&elapsed_secs)); } - - #[test] - fn should_support_optional_metric_name() { - let span = tracing::info_span!("test_span"); - // Should not panic even when metric_name is Some - let _guard = LatencyGuard::new(span, Some("test_metric".to_string())); - } } diff --git a/tests/repository_policy.rs b/tests/repository_policy.rs deleted file mode 100644 index 2e51f40f..00000000 --- a/tests/repository_policy.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::path::PathBuf; - -#[test] -fn should_not_contain_a_top_level_scripts_directory() { - // Arrange - let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - - // Act - let scripts_directory_exists = repo_root.join("scripts").exists(); - - // Assert - assert!( - !scripts_directory_exists, - "repository automation belongs in tests, proper tools, package scripts, or explicit workflow steps" - ); -} diff --git a/tests/rpc_basics.rs b/tests/rpc_basics.rs index 13da06b8..dfa1b70f 100644 --- a/tests/rpc_basics.rs +++ b/tests/rpc_basics.rs @@ -1,127 +1,13 @@ -//! RPC public protocol and error-code tests. - -use bytes::Bytes; -use fitz::domains::rpc::{RpcRequest, RpcResponse}; -use fitz::runtime::routing::{Route, RouteFamily}; -use uuid::Uuid; - -#[test] -fn should_have_correlation_id_in_request() { - // Arrange - let correlation_id = Uuid::new_v4(); - let family = RouteFamily::new(1); - let route = Route::new("rpc://acme/auth/user/create"); - let body = Bytes::from("test payload"); - - // Act - let request = RpcRequest::new(family, correlation_id, route, body); - - // Assert - assert_eq!( - request.correlation_id, correlation_id, - "correlation_id should be stored in request" - ); -} - -#[test] -fn should_use_uuid_for_correlation_id() { - // Arrange - let correlation_id = Uuid::new_v4(); - - // Act - let uuid_bytes = correlation_id.as_bytes(); - - // Assert - assert_eq!( - uuid_bytes.len(), - 16, - "correlation_id (UUID) must be exactly 16 bytes" - ); -} - -#[test] -fn should_echo_correlation_id_in_response() { - // Arrange - let correlation_id = Uuid::new_v4(); - let body = Bytes::from("response payload"); - - // Act - let response = RpcResponse::single(correlation_id, body); - - // Assert - assert_eq!( - response.correlation_id, correlation_id, - "response must echo request correlation_id" - ); -} - -#[test] -fn should_have_sequence_number_for_streaming() { - // Arrange - let correlation_id = Uuid::new_v4(); - let seq = 5u64; - let stream_end = false; - let body = Bytes::from("middle chunk"); - - // Act - let response = RpcResponse::chunk(correlation_id, seq, body, stream_end); - - // Assert - assert_eq!(response.seq, 5, "sequence number should be incremented"); - assert!( - !response.stream_end, - "middle chunk should not mark stream end" - ); -} - -#[test] -fn should_have_stream_end_flag_for_final_chunk() { - // Arrange - let correlation_id = Uuid::new_v4(); - let seq = 10u64; - let stream_end = true; - let body = Bytes::from("final chunk"); - - // Act - let response = RpcResponse::chunk(correlation_id, seq, body, stream_end); - - // Assert - assert!(response.stream_end, "final chunk must set stream_end=true"); -} - -#[test] -fn should_include_payload_in_request_response() { - // Arrange - let family = RouteFamily::new(1); - let route = Route::new("rpc://acme/auth/user/create"); - let request_body = Bytes::from("create user request"); - let response_body = Bytes::from("user created"); - - // Act - let request = RpcRequest::new(family, Uuid::new_v4(), route, request_body); - let response = RpcResponse::single(Uuid::new_v4(), response_body); - - // Assert - assert_eq!( - request.body, - Bytes::from("create user request"), - "request body preserved" - ); - assert_eq!( - response.body, - Bytes::from("user created"), - "response body preserved" - ); -} +//! RPC wire-protocol error-code contract tests. +//! +//! These lock in the numeric values of documented RPC error codes, which +//! external clients branch on. The rest of RPC request/response behavior +//! (dispatch, sequencing, timeouts, reassembly) is covered end-to-end in +//! `rpc_advanced.rs` and `rpc_e2e/` against a real running broker. #[test] fn should_define_error_code_6006_rpc_invalid_sequence() { - // Arrange - - // Act let code = fitz::protocol::error_codes::rpc::ERR_RPC_INVALID_SEQUENCE; - - // Assert assert_eq!(code, 6006, "6006 = RPC_INVALID_SEQUENCE"); } @@ -136,130 +22,3 @@ fn should_define_error_code_6008_rpc_wrong_worker() { let code = fitz::protocol::error_codes::rpc::ERR_RPC_WRONG_WORKER; assert_eq!(code, 6008, "6008 = RPC_WRONG_WORKER"); } - -#[test] -fn should_forward_rpc_success_given_registered_worker() { - // Arrange - let family = RouteFamily::new(1); - let correlation_id = Uuid::new_v4(); - let route = Route::new("rpc://acme/billing/invoice/create"); - let request_body = Bytes::from("{ \"amount\": 100 }"); - let response_body = Bytes::from("{ \"invoice_id\": 123 }"); - - // Act - let request = RpcRequest::new(family, correlation_id, route, request_body); - let response = RpcResponse::single(correlation_id, response_body); - - // Assert - assert_eq!(request.correlation_id, correlation_id); - assert_eq!(response.correlation_id, correlation_id); - assert_eq!(request.correlation_id, response.correlation_id); -} - -#[test] -fn should_match_response_to_request_by_correlation_id() { - // Arrange - let correlation_id = Uuid::new_v4(); - let request_family = RouteFamily::new(1); - let request_route = Route::new("rpc://realm/auth/user/get"); - let request_body = Bytes::from("{ \"user_id\": 42 }"); - - // Act - let request = RpcRequest::new(request_family, correlation_id, request_route, request_body); - let response = RpcResponse::single(correlation_id, Bytes::from("{ \"name\": \"Alice\" }")); - - // Assert - assert_eq!(request.correlation_id, response.correlation_id); -} - -#[test] -fn should_reassemble_multi_chunk_streaming_response() { - // Arrange - let correlation_id = Uuid::new_v4(); - - // Act - let chunk1 = RpcResponse::chunk(correlation_id, 0, Bytes::from("chunk1"), false); - let chunk2 = RpcResponse::chunk(correlation_id, 1, Bytes::from("chunk2"), false); - let chunk3 = RpcResponse::chunk(correlation_id, 2, Bytes::from("chunk3"), true); - - // Assert - assert_eq!(chunk1.seq, 0); - assert_eq!(chunk2.seq, 1); - assert_eq!(chunk3.seq, 2); - assert!(chunk3.stream_end); -} - -#[test] -fn should_reject_rpc_response_given_sequence_gap() { - // Arrange - let correlation_id = Uuid::new_v4(); - - // Act - let chunk0 = RpcResponse::chunk(correlation_id, 0, Bytes::from("chunk0"), false); - let chunk2 = RpcResponse::chunk(correlation_id, 2, Bytes::from("chunk2"), false); - - // Assert - assert_eq!(chunk0.seq, 0); - assert_eq!(chunk2.seq, 2); - assert!(chunk2.seq - chunk0.seq > 1); -} - -#[test] -fn should_handle_single_chunk_as_complete_response() { - // Arrange - let correlation_id = Uuid::new_v4(); - let body = Bytes::from("complete response"); - - // Act - let response = RpcResponse::single(correlation_id, body); - - // Assert - assert_eq!(response.seq, 0); - assert!(response.stream_end); - assert_eq!(response.correlation_id, correlation_id); -} - -#[test] -fn should_include_route_family_in_request() { - // Arrange - let family = RouteFamily::new(1); - let correlation_id = Uuid::new_v4(); - let route = Route::new("rpc://acme/auth/user/create"); - let body = Bytes::from("test"); - - // Act - let request = RpcRequest::new(family, correlation_id, route, body); - - // Assert - assert_eq!(request.family_id, family); -} - -#[test] -fn should_not_include_reply_route_in_request() { - // Arrange - let family = RouteFamily::new(1); - let correlation_id = Uuid::new_v4(); - let route = Route::new("rpc://acme/auth/user/create"); - let body = Bytes::from("test"); - - // Act - let request = RpcRequest::new(family, correlation_id, route, body.clone()); - - // Assert - assert_eq!(request.body, body); -} - -#[test] -fn should_include_target_route_in_request() { - // Arrange - let family = RouteFamily::new(1); - let correlation_id = Uuid::new_v4(); - let route = Route::new("rpc://acme/auth/user/create"); - let body = Bytes::from("test"); - - // Act - let request = RpcRequest::new(family, correlation_id, route.clone(), body); - - // Assert - assert_eq!(&request.route, &route); -} diff --git a/tests/schedule_advanced.rs b/tests/schedule_advanced.rs index d99c357c..508c76c4 100644 --- a/tests/schedule_advanced.rs +++ b/tests/schedule_advanced.rs @@ -230,74 +230,126 @@ fn should_allow_creating_schedule_with_complex_cron() { )); } +/// Resolve a `next_fire_time` result computed against a fixed [`MockClock`] back +/// into a real calendar date/time, so tests can assert the *specific* day the +/// scheduler landed on rather than just "some time in the future". +fn resolve_fire_time(clock: &MockClock, next_fire: Instant) -> chrono::DateTime { + let next_ms = fitz::runtime::instant_to_epoch_ms_with_reference( + next_fire, + clock.now_instant(), + clock.now_epoch_ms(), + ); + chrono::Utc + .timestamp_millis_opt(i64::try_from(next_ms).expect("epoch ms fits in i64")) + .single() + .expect("valid datetime") +} + #[test] fn should_find_next_fire_for_business_hours_schedule() { - // Arrange + use chrono::{Datelike, Timelike}; + + // Arrange: Monday 2025-01-06 08:00 UTC, before the 9am window opens. + let clock = MockClock::new(epoch_ms(2025, 1, 6, 8, 0, 0)); let cron = CronSchedule::parse("0 9-17 * * 1-5").unwrap(); // 9-5 weekdays - let now = std::time::Instant::now(); // Act - let next_fire = cron.next_fire_time(now); - - // Assert - assert!(next_fire > now); - // Should fire within reasonable business hours window - let elapsed = next_fire.duration_since(now); - assert!(elapsed.as_secs() < 7 * 24 * 3600); // Within a week + let next_fire = cron.next_fire_time_with_clock(clock.now_instant(), &clock); + let resolved = resolve_fire_time(&clock, next_fire); + + // Assert: fires the same day at 9am, still within the Mon-Fri window. + assert_eq!(resolved.year(), 2025); + assert_eq!(resolved.month(), 1); + assert_eq!(resolved.day(), 6); + assert_eq!(resolved.hour(), 9); + let weekday = resolved.weekday().number_from_monday(); // 1=Mon .. 5=Fri + assert!( + (1..=5).contains(&weekday), + "expected a weekday, got {weekday}" + ); } // ========== Edge Case Tests ========== #[test] fn should_handle_leap_year_february() { - // Arrange + use chrono::Datelike; + + // Arrange: start from a non-leap year so the schedule must skip forward. + let clock = MockClock::new(epoch_ms(2025, 1, 1, 0, 0, 0)); let cron = CronSchedule::parse("0 0 29 2 *").unwrap(); // Feb 29 // Act - let now = std::time::Instant::now(); - let next_fire = cron.next_fire_time(now); + let next_fire = cron.next_fire_time_with_clock(clock.now_instant(), &clock); + let resolved = resolve_fire_time(&clock, next_fire); - // Assert - should be valid and in future - assert!(next_fire > now); + // Assert: lands on the next actual leap day, not just "later". + assert_eq!(resolved.month(), 2); + assert_eq!(resolved.day(), 29); + assert!( + chrono::NaiveDate::from_ymd_opt(resolved.year(), 1, 1).is_some() + && chrono::NaiveDate::from_ymd_opt(resolved.year(), 2, 29).is_some(), + "resolved year {} must actually be a leap year", + resolved.year() + ); } #[test] fn should_handle_month_end_schedule() { - // Arrange + use chrono::Datelike; + + // Arrange: January has 31 days, so this should fire later in the same month. + let clock = MockClock::new(epoch_ms(2025, 1, 1, 0, 0, 0)); let cron = CronSchedule::parse("0 0 31 * *").unwrap(); // 31st of month // Act - let now = std::time::Instant::now(); - let next_fire = cron.next_fire_time(now); + let next_fire = cron.next_fire_time_with_clock(clock.now_instant(), &clock); + let resolved = resolve_fire_time(&clock, next_fire); - // Assert - should skip months without 31st day - assert!(next_fire > now); + // Assert: lands specifically on the 31st, skipping any shorter months. + assert_eq!(resolved.day(), 31); + assert_eq!(resolved.year(), 2025); + assert_eq!(resolved.month(), 1); } #[test] fn should_handle_weekend_only_schedule() { - // Arrange + use chrono::{Datelike, Weekday}; + + // Arrange: Monday 2025-01-06, so the next weekend is a few days out. + let clock = MockClock::new(epoch_ms(2025, 1, 6, 0, 0, 0)); let cron = CronSchedule::parse("0 0 * * 0,6").unwrap(); // Saturday and Sunday // Act - let now = std::time::Instant::now(); - let next_fire = cron.next_fire_time(now); + let next_fire = cron.next_fire_time_with_clock(clock.now_instant(), &clock); + let resolved = resolve_fire_time(&clock, next_fire); - // Assert - fires only on weekends - assert!(next_fire > now); + // Assert: actually lands on a Saturday or Sunday, not just "later". + assert!( + matches!(resolved.weekday(), Weekday::Sat | Weekday::Sun), + "expected a weekend day, got {:?}", + resolved.weekday() + ); + // The very next weekend from a Monday is that Saturday (5 days later). + assert_eq!(resolved.day(), 11); } #[test] fn should_handle_year_end_schedule() { + use chrono::Datelike; + // Arrange + let clock = MockClock::new(epoch_ms(2025, 1, 1, 0, 0, 0)); let cron = CronSchedule::parse("0 0 31 12 *").unwrap(); // New Year's Eve // Act - let now = std::time::Instant::now(); - let next_fire = cron.next_fire_time(now); + let next_fire = cron.next_fire_time_with_clock(clock.now_instant(), &clock); + let resolved = resolve_fire_time(&clock, next_fire); - // Assert - assert!(next_fire > now); + // Assert: lands specifically on December 31st of the same year. + assert_eq!(resolved.year(), 2025); + assert_eq!(resolved.month(), 12); + assert_eq!(resolved.day(), 31); } #[test] diff --git a/tests/schedule_basics.rs b/tests/schedule_basics.rs index d8976984..cca6a8bd 100644 --- a/tests/schedule_basics.rs +++ b/tests/schedule_basics.rs @@ -183,18 +183,6 @@ fn should_reject_invalid_cron_range() { assert!(cron.is_err()); } -#[test] -fn should_parse_cron_with_all_wildcards() { - // Arrange - let cron_str = "* * * * *"; - - // Act - let cron = CronSchedule::parse(cron_str); - - // Assert - assert!(cron.is_ok()); -} - // ========== CREATE Operation Tests ========== #[test] diff --git a/tests/schedule_e2e.rs b/tests/schedule_e2e.rs index 62d599c4..95393b6f 100644 --- a/tests/schedule_e2e.rs +++ b/tests/schedule_e2e.rs @@ -123,11 +123,35 @@ where let frame = build_schedule_create("schedule://test/jobs/preserve/run", "*/5 * * * *", payload); // Act - let response = client.send_and_receive(&frame, 2000).await.expect("send"); + let create_response = client.send_and_receive(&frame, 2000).await.expect("send"); + let list_response = client + .send_and_receive(&build_schedule_list(), 2000) + .await + .expect("list schedules"); // Assert - let (_msg_type, status, _data) = parse_schedule_response(&response); - assert_eq!(status, 0, "Should preserve schedule payload"); + let (_msg_type, create_status, _data) = parse_schedule_response(&create_response); + assert_eq!(create_status, 0, "Expected success for create schedule"); + + let (_msg_type, list_status, list_payload) = parse_schedule_response(&list_response); + assert_eq!(list_status, 0, "Expected success for schedule list"); + + // Decode the ListDefs entry and confirm the exact payload bytes round-tripped + // over the wire, not just that the create call reported success. + let mut dec = PayloadDecoder::new(&list_payload[1..]); + let _total_count = dec.get_u64().expect("total count"); + let has_entry = dec.get_u8().expect("has_entry flag"); + assert_eq!(has_entry, 1, "expected the created schedule in the list"); + let route = dec.get_string().expect("route"); + assert_eq!(route, "schedule://test/jobs/preserve/run"); + let _cron = dec.get_string().expect("cron"); + let _delivery_mode = dec.get_u8().expect("delivery mode"); + let stored_payload = dec.get_bytes().expect("payload"); + assert_eq!( + stored_payload.as_ref(), + payload, + "schedule payload must round-trip byte-for-byte over the wire" + ); } // Generic test helper for multiple schedule creation diff --git a/tests/semantic_boundaries.rs b/tests/semantic_boundaries.rs index fe8486d0..1a11ffd4 100644 --- a/tests/semantic_boundaries.rs +++ b/tests/semantic_boundaries.rs @@ -286,89 +286,6 @@ fn should_keep_shadow_notice_surface_removed() { "shadow Notice actors and events must stay absent:\n{report}" ); } - -#[test] -fn should_keep_notice_family_state_key_type_safe() { - // Arrange - let repo_root = repo_root(); - let sink = read_source_file( - &repo_root - .join("src") - .join("domains") - .join("notice") - .join("sink.rs"), - ); - - // Act - let has_typed_key = sink.contains( - "HashMap>", - ); - let retains_round_trip = sink.contains("RouteFamily::try_from(*family_id)"); - - // Assert - assert!( - has_typed_key, - "Notice family state must use RouteFamily keys" - ); - assert!( - !retains_round_trip, - "Notice cleanup must not reconstruct RouteFamily from an integer key" - ); -} - -#[test] -fn should_keep_notice_backpressure_plus_duplicate_paths_bounded() { - // Arrange - let repo_root = repo_root(); - let notice_sink_dir = repo_root - .join("src") - .join("domains") - .join("notice") - .join("sink"); - let domain_sink = read_source_file(¬ice_sink_dir.join("domain_sink_impl.rs")); - let delivery_worker = read_source_file(¬ice_sink_dir.join("delivery_worker.rs")); - - // Act - let duplicate_check = domain_sink.find("self.try_reuse_existing(sub_msg)"); - let pattern_compile = domain_sink.find("Self::compile_pattern(sub_msg)"); - let has_deadline_retry = delivery_worker.contains("NOTICE_MAILBOX_RETRY_TIMEOUT") - && delivery_worker.contains("Instant::now() < deadline"); - let has_fixed_retry_loop = delivery_worker.contains("MAX_RETRIES"); - - // Assert - assert!( - duplicate_check - .zip(pattern_compile) - .is_some_and(|(check, compile)| check < compile), - "Notice duplicate lookup must precede pattern compilation" - ); - assert!( - has_deadline_retry, - "Notice backpressure retry must use a deadline" - ); - assert!( - !has_fixed_retry_loop, - "Notice retry must not restore a fixed spin count" - ); -} - -#[test] -fn should_compile_lease_bench_commands_only_for_tests_or_benchkit() { - // Arrange - let repo_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let model = read_source_file(&repo_root.join("src/domains/lease/sink/model.rs")); - let lifecycle = read_source_file( - &repo_root.join("src/domains/lease/sink/lifecycle_and_admin/lifecycle.rs"), - ); - - // Act - let gate = "#[cfg(any(test, feature = \"benchkit\"))]"; - - // Assert - assert!(model.matches(gate).count() >= 2); - assert!(lifecycle.matches(gate).count() >= 2); -} - #[test] fn should_keep_shadow_lease_actor_removed_from_default_surface() { // Arrange @@ -399,23 +316,6 @@ fn should_keep_shadow_lease_actor_removed_from_default_surface() { "shadow Lease surface remains: modules={exposed_shadow_modules:?}, files={retained_shadow_files:?}" ); } - -#[test] -fn should_keep_panicking_stream_storage_decoders_test_only() { - // Arrange - let repo_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let storage = repo_root.join("src/domains/stream/storage"); - let compact = read_source_file(&storage.join("compact_page_values.rs")); - let hierarchy = read_source_file(&storage.join("resource_area_realm_values.rs")); - - // Act - let test_gate = "#[cfg(test)]"; - - // Assert - assert!(compact.matches(test_gate).count() >= 3); - assert!(hierarchy.matches(test_gate).count() >= 3); -} - #[test] fn should_keep_lease_benchmark_mutation_actor_serialized() { // Arrange @@ -559,559 +459,6 @@ fn should_document_all_rpc_error_codes_in_client_spec() { "RPC client spec must list every RPC error code/name from src/protocol/error_codes.rs:\n{report}" ); } - -#[test] -fn should_keep_rpc_design_seams_explicit() { - // Arrange - let repo_root = repo_root(); - let rpc = repo_root.join("src/domains/rpc/sink"); - let constants = read_source_file(&rpc.join("state_model/constants.rs")); - let mailbox = read_source_file(&rpc.join("mailbox_sink_impl.rs")); - let requests = read_source_file(&rpc.join("state_model/requests.rs")); - let route_state = read_source_file(&rpc.join("state_model/route_state.rs")); - let state = read_source_file(&rpc.join("state_model/state.rs")); - let worker = read_source_file(&rpc.join("state_model/worker.rs")); - let registration_table = read_source_file(&rpc.join("state_model/registration_table.rs")); - let ready_queue = read_source_file(&rpc.join("state_model/ready_queue.rs")); - let response_forwarder = read_source_file(&rpc.join("response_forwarder.rs")); - - // Act - let violations = [ - ( - !constants.contains("RPC_MSG_TYPE_REQUEST"), - "request message constant", - ), - ( - !constants.contains("RPC_MSG_TYPE_RESPONSE"), - "response message constant", - ), - ( - !mailbox.contains("deliver_with_priority"), - "shared delivery guard", - ), - ( - !requests.contains("dispatch_info: RpcPendingDispatchInfo"), - "owned pending dispatch view", - ), - ( - !route_state.contains("struct RegistrationRotor"), - "registration rotor", - ), - ( - state.contains("clippy::too_many_lines"), - "small dispatch coordinator", - ), - ( - state.contains("fn dispatch_or_queue_request(\n"), - "test-only dispatch wrapper", - ), - ( - !registration_table.contains("struct RegistrationTable"), - "registration table", - ), - ( - !ready_queue.contains("struct RouteReadyQueue"), - "route-ready queue", - ), - ( - !state.contains("trait RpcRequestState") || !state.contains("trait RpcResponseState"), - "request and response state facades", - ), - ( - !response_forwarder.contains("struct RpcResponseForwarder"), - "response forwarder", - ), - ( - !worker.contains("struct RegistrationCredit"), - "registration credit accounting", - ), - ( - [ - "/// Selects the next available registration", - "/// Claims one registration credit", - "/// Reserves one unit of global pending capacity", - "/// Coordinates duplicate, capacity, fairness, and tracking policy", - ] - .iter() - .any(|contract| !state.contains(contract)), - "RPC state policy documentation", - ), - ] - .into_iter() - .filter_map(|(missing, label)| missing.then_some(label)) - .collect::>(); - - // Assert - assert!(violations.is_empty(), "missing RPC seams: {violations:?}"); -} - -#[test] -fn should_use_registration_vocabulary_throughout_rpc_state_model_source() { - // Arrange - let state = read_source_file(&repo_root().join("src/domains/rpc/sink/state_model/state.rs")); - let registration_table = read_source_file( - &repo_root().join("src/domains/rpc/sink/state_model/registration_table.rs"), - ); - - // Act - // Scan comments and literals too: these internal model files should use one vocabulary. - let mixed_terms = [ - ("state.rs", state), - ("registration_table.rs", registration_table), - ] - .into_iter() - .flat_map(|(file, source)| { - source - .split(|character: char| !(character.is_ascii_alphanumeric() || character == '_')) - .filter(|identifier| identifier.contains("worker")) - .map(move |identifier| format!("{file}:{identifier}")) - .collect::>() - }) - .collect::>(); - - // Assert - assert!( - mixed_terms.is_empty(), - "RPC state model source must use registration vocabulary: {mixed_terms:?}" - ); -} - -#[test] -fn should_keep_stream_design_seams_explicit() { - // Arrange - let stream = repo_root().join("src/domains/stream"); - let keys = read_source_file(&stream.join("storage/keys_and_models.rs")); - let model = read_source_file(&stream.join("sink/model.rs")); - let sink = read_source_file(&stream.join("sink/domain_sink_impl.rs")); - let core = read_source_file( - &stream.join("sink/domain_sink_impl/domain_core_impl/watermark_coordination.rs"), - ); - let codecs = read_source_file(&stream.join("storage/compact_page_values.rs")); - let sequence = read_source_file(&stream.join("store/sequence_and_filters.rs")); - let actor = read_source_file(&stream.join("actor.rs")); - let store = read_source_file(&stream.join("store/mod.rs")); - let store_sources = source_files_under(&stream.join("store")) - .iter() - .map(|path| read_source_file(path)) - .collect::>() - .join("\n"); - - // Act - let violations = [ - ( - !keys.contains("impl TryFrom for KeyPrefix"), - "key-prefix decoding", - ), - ( - !model.contains("struct SubscriptionRegistry"), - "subscription registry", - ), - ( - !model.contains("struct AdminSnapshotState"), - "admin snapshot state", - ), - ( - !model.contains("struct WatermarkCoordinators"), - "watermark coordinators", - ), - ( - !codecs.contains("trait PageRecordCodec"), - "page-record codec", - ), - ( - actor.contains("impl ActiveAppendSession {}"), - "empty append-session impl", - ), - ( - !store.contains("enum StreamStoreError"), - "stream store error", - ), - ( - [ - "commit_records_promotion_frontier(", - "commit_session_promotion_frontier(", - "read_resource_promotion_frontier(", - "read_area_promotion_frontier(", - "read_realm_promotion_frontier(", - ] - .iter() - .any(|wrapper| store_sources.contains(wrapper)), - "single-layout wrapper twins", - ), - ( - !core.contains("fn dispatch_watermark_commit"), - "shared watermark dispatch", - ), - ( - !sink.contains("fn dispatch_family_command"), - "shared family command dispatch", - ), - ( - !sequence.contains("fn load_existing_watermark_for_guard"), - "shared watermark guard read", - ), - ( - !sequence.contains("for key in keys"), - "discriminator row loop", - ), - ( - !keys.contains("LEGACY D3 PROTOTYPE PREFIXES"), - "legacy prototype prefix boundary", - ), - ] - .into_iter() - .filter_map(|(missing, label)| missing.then_some(label)) - .collect::>(); - - // Assert - assert!( - violations.is_empty(), - "missing Stream seams: {violations:?}" - ); -} - -#[test] -fn should_keep_queue_design_seams_explicit() { - // Arrange - let queue = repo_root().join("src/domains/queue"); - let actor = read_source_file(&queue.join("actor/mod.rs")); - let ack = read_source_file(&queue.join("actor/reserve_and_ack.rs")); - let storage = read_source_file(&queue.join("actor/storage.rs")); - let sink = read_source_file(&queue.join("sink/domain_sink_impl.rs")); - - // Act - let violations = [ - ( - !queue.join("actor/dlq.rs").exists(), - "DLQ transition module", - ), - ( - !queue.join("actor/dead_letter_admin.rs").exists(), - "dead-letter admin module", - ), - ( - !queue.join("actor/startup_reconciliation.rs").exists(), - "startup reconciliation module", - ), - ( - !actor.contains("fn wire_code") || !actor.contains("fn as_str"), - "DLQ reason mappings", - ), - ( - !actor.contains("trait QueueDataPlane") || !actor.contains("trait QueueAdminPlane"), - "queue interface traits", - ), - ( - !ack.contains("fn validate_ack_authorization"), - "ack authorization seam", - ), - ( - !ack.contains("stage_delayed") || !ack.contains("fast path"), - "ack staging and fast-path documentation", - ), - ( - !storage.contains("fn commit_transaction"), - "shared transaction commit", - ), - ( - !sink.contains("struct QueueCounts"), - "queue counts accessor", - ), - ( - !actor.contains("QUEUE_IDLE_HORIZON") - || !actor.contains("QUEUE_STORAGE_RETRY_BACKOFF") - || !actor.contains("QUEUE_ACTOR_REPLY_TIMEOUT"), - "queue timing constants", - ), - ] - .into_iter() - .filter_map(|(missing, label)| missing.then_some(label)) - .collect::>(); - - // Assert - assert!(violations.is_empty(), "missing Queue seams: {violations:?}"); -} - -#[test] -fn should_keep_schedule_design_seams_explicit() { - // Arrange - let schedule = repo_root().join("src/domains/schedule"); - let actor = read_source_file(&schedule.join("actor/claim_and_ack.rs")); - let actor_mod = read_source_file(&schedule.join("actor/mod.rs")); - let sink = read_source_file(&schedule.join("sink/domain_sink_impl.rs")); - let model = read_source_file(&schedule.join("sink/model.rs")); - let store = read_source_file(&schedule.join("store/model.rs")); - - // Act - let violations = [ - ( - !schedule.join("sink/delivery_strategy.rs").exists(), - "delivery strategy", - ), - ( - !sink.contains("fn claim_due") - || !sink.contains("fn deliver_claims") - || !sink.contains("fn acknowledge_delivered"), - "due scan stages", - ), - ( - !actor.contains("fn pop_due_from_heap") - || !actor.contains("fn recompute_next_fires") - || !actor.contains("fn persist_claims") - || !actor.contains("fn apply_claims_to_state"), - "claim stages", - ), - ( - !model.contains("enum PendingFireState"), - "pending-fire state", - ), - ( - !actor_mod.contains("#[cfg(test)]") || !actor_mod.contains("test_actor_harness"), - "test-only actor harness", - ), - ( - !sink.contains("trait ScheduleObservability"), - "observability interface", - ), - ( - !store.contains("trait SchedulePersistence"), - "persistence interface", - ), - (schedule.join("events.rs").exists(), "dead schedule events"), - ( - !actor_mod.contains("SCAN_DEDUP_WINDOW") || !model.contains("EXECUTIONS_WINDOW_MS"), - "schedule timing constants", - ), - ( - !model.contains("sink wrapper") || !model.contains("runtime body"), - "sink runtime naming docs", - ), - ] - .into_iter() - .filter_map(|(missing, label)| missing.then_some(label)) - .collect::>(); - - // Assert - assert!( - violations.is_empty(), - "missing Schedule seams: {violations:?}" - ); -} - -#[test] -fn should_complete_reopened_kv_plus_lease_design_criteria() { - // Arrange - let root = repo_root().join("src/domains"); - let kv_domain = read_source_file(&root.join("kv/sink/domain_sink_impl.rs")); - let kv_mailbox = read_source_file(&root.join("kv/sink/mailbox_sink_impl.rs")); - let lease_expiry = read_source_file(&root.join("lease/sink/domain_sink_impl/expiry.rs")); - let lease_mailbox = read_source_file(&root.join("lease/sink/mailbox_sink_impl.rs")); - - // Act - let violations = [ - ( - !kv_domain.contains("use crate::domains::kv::KvActor;") - || kv_domain.contains("crate::domains::kv::KvActor::"), - "KV domain actor import cleanup", - ), - ( - !kv_mailbox.contains("use crate::domains::kv::{KvActor, KvError, KvResponse};") - || ["KvActor", "KvError", "KvResponse"] - .iter() - .any(|name| kv_mailbox.contains(&format!("crate::domains::kv::{name}"))), - "KV mailbox imports cleanup", - ), - ( - !lease_expiry.contains( - "/// Removes every queued waiter owned by the session before empty queues are dropped.", - ), - "Lease session-waiter ordering docs", - ), - ( - !lease_mailbox.contains("fn scope_operation_owner") - || lease_mailbox.matches("session_scoped_owner_id(").count() != 1, - "Lease owner-scoping step", - ), - ] - .into_iter() - .filter_map(|(missing, label)| missing.then_some(label)) - .collect::>(); - - // Assert - assert!( - violations.is_empty(), - "reopened design criteria remain incomplete: {violations:?}" - ); -} - -#[test] -fn should_document_unified_wildcard_registration_plus_exact_lease_semantics() { - // Arrange - let root = repo_root().join("docs"); - let wire = read_source_file(&root.join("clients/spec/wire-routing.md")); - let boundaries = read_source_file(&root.join("development/domain-boundaries-spec.md")); - let laws = read_source_file(&root.join("development/architectural-laws.md")); - let schedule = read_source_file(&root.join("clients/spec/lease-schedule.md")); - let operations = read_source_file(&root.join("clients/spec/operations.md")); - - // Act - let combined = [ - wire.as_str(), - boundaries.as_str(), - laws.as_str(), - schedule.as_str(), - operations.as_str(), - ] - .join("\n"); - - // Assert - assert!(wire - .contains("KV, Queue, Notice, Stream, RPC, and Schedule each permit at most 128 wildcard")); - assert!( - wire.contains("Notifications carry the matching `subscription_id` and the exact concrete") - ); - assert!(wire.contains("Ready concrete routes rotate fairly")); - assert!(boundaries.contains("exact and wildcard registrations are equal candidates")); - assert!(boundaries.contains("Lease does not participate in this wildcard contract")); - assert!(laws.contains("whole-segment `*` and `**`")); - assert!(schedule.contains("Overlapping\npatterns remain distinct")); - assert!(schedule.contains("Watches are exact-route subscriptions")); - assert!(schedule.contains("5010 = ERR_INVALID_SUBSCRIPTION_ROUTE")); - assert!(operations.contains("KV, Queue, Notice, Stream, RPC, and Schedule registrations")); - assert!(operations.contains("Duplicate `(session, original registration")); - assert!(operations.contains("Matching never\ncrosses `RouteFamily`")); - assert!(operations.contains("the exact concrete route")); - assert!(operations.contains("Lease is intentionally different")); - assert!(!combined.contains("Wildcard worker registration is not part of the contract")); - assert!(!combined.contains("Workers register exact listening routes")); - assert!(!combined.contains("Wildcard schedule subscribe is invalid")); - assert!(!combined.contains("Lease subscriptions accept wildcard")); - assert!(!combined.contains("Lease watches support `*`")); -} - -#[test] -fn should_keep_boot_runtime_design_seams_explicit() { - // Arrange - let root = repo_root(); - let boot = read_source_file(&root.join("src/boot/mod.rs")); - let storage = read_source_file(&root.join("src/boot/storage.rs")); - let config = read_source_file(&root.join("src/boot/runtime/config.rs")); - let cloud = read_source_file(&root.join("src/boot/runtime/config/cloud_provider.rs")); - let env = read_source_file(&root.join("src/boot/runtime/config/env.rs")); - let domains = read_source_file(&root.join("src/boot/domains.rs")); - let pool = - read_source_file(&root.join("src/runtime/family_".to_string() + "a" + "ctor_pool.rs")); - let managed = - read_source_file(&root.join("src/runtime/managed_".to_string() + "a" + "ctor.rs")); - let shutdown = read_source_file(&root.join("src/boot/shutdown.rs")); - - // Act - let required = [ - (boot.contains("enum BootStage"), "named boot stages"), - (boot.contains("fn start_listeners"), "listener stage"), - (boot.contains("fn open_storage_stage"), "storage stage"), - (boot.contains("fn register_domains_stage"), "domain stage"), - (!boot.contains("clippy::too_many_lines"), "boot line lint"), - ( - boot.matches(&["\n ShutdownContext ", &char::from(123).to_string()].concat()) - .count() - == 1, - "shutdown context construction", - ), - ( - root.join("src/boot/storage/backoff.rs").is_file(), - "storage backoff module", - ), - ( - root.join("src/boot/storage/contention.rs").is_file(), - "storage contention module", - ), - ( - read_source_file(&root.join("src/boot/storage/contention.rs")) - .contains("enum ContentionKind"), - "typed storage contention seam", - ), - ( - config.contains("struct TransportConfig"), - "transport sub-config", - ), - ( - config.contains("struct StorageConfig"), - "storage sub-config", - ), - (config.contains("struct DrainConfig"), "drain sub-config"), - ( - storage.contains("fn open_with_retry"), - "shared storage retry loop", - ), - ( - cloud.contains("fn s3_compatible_provider"), - "shared S3-compatible provider constructor", - ), - ( - cloud.contains("PROVIDER_DESCRIPTORS"), - "provider descriptor table", - ), - ( - config.contains("fn cloud_durable_write_options"), - "cloud write options mapping", - ), - ( - env.contains("fn positive_u64_from_env"), - "positive integer environment parser", - ), - ( - managed.contains(&("Unsupervised ".to_string() + "a" + "ctors do not fire timers")), - "unsupervised timer contract", - ), - ( - domains.contains("DomainKind::ALL.len()"), - "domain handle consistency regression", - ), - ( - pool.contains(&("struct Family".to_string() + "A" + "ctorPoolHealthSnapshot")), - "family pool health type", - ), - ( - shutdown.contains("PRIORITY_FATAL"), - "named shutdown priority", - ), - ( - !boot.contains("fn warn_defaulted_fast_queue_policy"), - "queue warning ownership", - ), - ( - config.contains("fn warn_defaulted_fast_queue_policy"), - "queue warning policy", - ), - ]; - let missing = required - .into_iter() - .filter_map(|(present, label)| (!present).then_some(label)) - .collect::>(); - - // Assert - assert!(missing.is_empty(), "missing boot/runtime seams"); -} - -#[test] -fn should_document_route_bearing_schedule_notify_wire_format() { - // Arrange - let root = repo_root().join("docs"); - let schedule = read_source_file(&root.join("clients/spec/lease-schedule.md")); - let migration = read_source_file(&root.join("operations/migration-guide.md")); - - // Act - let has_route_bearing_schema = schedule.contains("[u32 BE] exact_route_len") - && schedule.contains("[bytes] exact_route") - && schedule.contains("[subscription_id][exact_route][payload]"); - - // Assert - assert!(has_route_bearing_schema); - assert!(migration - .contains("`[subscription_id][payload]` to `[subscription_id][exact_route][payload]`")); -} - #[test] fn should_keep_runtime_ingress_payload_dispatch_free_of_payload_unwraps() { // Arrange diff --git a/ui/package-lock.json b/ui/package-lock.json index 0e669bde..8a435bf5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -6,22 +6,22 @@ "": { "name": "fitz-ui", "dependencies": { - "@askrjs/askr": "^0.0.91", - "@askrjs/fetch": "0.0.5", - "@askrjs/lucide": "0.0.9", - "@askrjs/themes": "0.0.25", - "@askrjs/ui": "^0.0.27" + "@askrjs/askr": "^0.2.2", + "@askrjs/fetch": "0.2.0", + "@askrjs/lucide": "0.2.0", + "@askrjs/themes": "^0.2.3", + "@askrjs/ui": "^0.2.2" }, "devDependencies": { - "@askrjs/cli": "0.0.23", - "@askrjs/vite": "0.0.13", + "@askrjs/cli": "0.2.1", + "@askrjs/vite": "0.2.0", "@playwright/test": "^1.62.1", "@types/node": "^26.2.0", "autoprefixer": "^10.5.4", "jsdom": "^30.0.1", "postcss": "^8.5.26", "typescript": "^7.0.2", - "vite-plus": "^0.2.8" + "vite-plus": "^0.2.9" } }, "node_modules/@asamuzakjp/css-color": { @@ -58,22 +58,22 @@ } }, "node_modules/@askrjs/askr": { - "version": "0.0.91", - "resolved": "https://registry.npmjs.org/@askrjs/askr/-/askr-0.0.91.tgz", - "integrity": "sha512-CuBq8S1hY5kUqImRBS1Mtmo581lS/mojlH31XQET+R5jyOd0uY8uUPLLuvjb6hpqF0Ap0H2OVXmcHVFTu5piqw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@askrjs/askr/-/askr-0.2.2.tgz", + "integrity": "sha512-rmCo/oyj9nAuA8beDWkFrCEuX7SbVMRJsAFC15qmhjoqrB1r6sJjShUqXRly2RvWOKmHyH8Nu2Y9Mn5qpwkXqQ==", "license": "Apache-2.0", "dependencies": { - "@askrjs/auth": ">=0.0.8 <0.1.0", - "@askrjs/schema": ">=0.0.5 <0.1.0" + "@askrjs/auth": ">=0.2.0 <0.3.0", + "@askrjs/schema": ">=0.2.0 <0.3.0" }, "engines": { "node": ">=24.0.0" } }, "node_modules/@askrjs/auth": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@askrjs/auth/-/auth-0.0.9.tgz", - "integrity": "sha512-Akv4FCoakSa2WSowIMoxnaUrvnDBF84vmwgbThkWIgTUCgj+L846TGZ/7fR7657MCJAEveLjmqbTDq0WTfbKrg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/auth/-/auth-0.2.0.tgz", + "integrity": "sha512-tuqG9lMUc6xNF/Q7yiqXET1rSzk7jEKDNRu30AyoQd/xn+yMzyjI/p+RSH+kRbrLS+5989vouVDknFAfs8UVsw==", "license": "Apache-2.0", "dependencies": { "@xmldom/xmldom": "0.9.10", @@ -85,19 +85,19 @@ } }, "node_modules/@askrjs/cli": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/@askrjs/cli/-/cli-0.0.23.tgz", - "integrity": "sha512-WvWVlADgMJct0xo4dUn6wf5NVWdQl83gi/LLD54rGuqhYZwnl8UpolORJfZdAGGdA0+/iw72t9vEQJc4/FOgjQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@askrjs/cli/-/cli-0.2.1.tgz", + "integrity": "sha512-tkp98kcSB19T/WsgMZfa9aVOboZYIoOBcpu1wF+FlOlBcgagBpu3bxnyZU3lTeamAQkZDaruSvQ/IQcSZZbR+w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@npmcli/config": "^11.0.1", - "js-yaml": "^5.2.3", + "js-yaml": "^5.3.0", "minimatch": "^10.2.6", "npm-registry-fetch": "^20.0.1", "parse5": "^8.0.1", "semver": "^7.8.5", - "tsx": "^4.23.11", + "tsx": "^4.23.12", "typescript": "npm:@typescript/typescript6@^6.0.2" }, "bin": { @@ -107,7 +107,7 @@ "node": ">=24.0.0" }, "peerDependencies": { - "@askrjs/askr": ">=0.0.89 <0.1.0" + "@askrjs/askr": ">=0.2.0 <0.3.0" }, "peerDependenciesMeta": { "@askrjs/askr": { @@ -130,35 +130,35 @@ } }, "node_modules/@askrjs/fetch": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@askrjs/fetch/-/fetch-0.0.5.tgz", - "integrity": "sha512-3kdZ5bpGSRvR9+G1CmVIfHPgEJtzOPcP9m0/dC2dCDSxoAw1wNEa08cMlOrzbsmDBBpxWJ6QpDnD1rqLGxhttg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/fetch/-/fetch-0.2.0.tgz", + "integrity": "sha512-2+aJQCzt+wm9MMEb5RLmUkpGE0N8bkPEn2+jkL0g44Fumnkqv2WfKWNnuCgLK2ms9bvxXK8QyS7Gwx97aeVFLQ==", "license": "Apache-2.0", "engines": { "node": ">=24.0.0" } }, "node_modules/@askrjs/lucide": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@askrjs/lucide/-/lucide-0.0.9.tgz", - "integrity": "sha512-K6D0GFT0CzE6C/HOEBPumGrAIkFmrG1qh+iD9DkltsBHXZXTBMHdbn7Qu6zxzoNggBNy+5fxOmLC7QLiCHSJ5Q==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/lucide/-/lucide-0.2.0.tgz", + "integrity": "sha512-8oNoIudvegdsIMFnFthp+9ly3GLzh8j9llmrr3CXekUixCFyl6akY7/riVaEnmY/4Wy+Z8zHSsf37MKtNMP+Kw==", "license": "Apache-2.0", "engines": { "node": ">=24.0.0" }, "peerDependencies": { - "@askrjs/askr": ">=0.0.88 <0.1.0" + "@askrjs/askr": ">=0.2.0 <0.3.0" } }, "node_modules/@askrjs/node": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@askrjs/node/-/node-0.0.9.tgz", - "integrity": "sha512-2sVn1crf0We/q/h7LVm3llFrkgcx/LqbtT7HRpZlHZkGMFG7JNxYBloLCAbGU5nE9AcaqZpac29w1VwWTsYE8g==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/node/-/node-0.2.0.tgz", + "integrity": "sha512-QNyUZ1KwVpObnjjfTULlrthEAfXennw1CoCCAclERImRRGXas5pu214rUURta8CuG8I3/hg1yMsEEUwLiYmBzw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@askrjs/auth": ">=0.0.8 <0.1.0", - "@askrjs/server": ">=0.0.11 <0.1.0", + "@askrjs/auth": ">=0.2.0 <0.3.0", + "@askrjs/server": ">=0.2.0 <0.3.0", "@types/ws": "^8.18.1", "ws": "^8.21.3" }, @@ -167,29 +167,29 @@ } }, "node_modules/@askrjs/schema": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@askrjs/schema/-/schema-0.0.6.tgz", - "integrity": "sha512-VN4PnJ3/NNP5UnWYbf6/rDciykKb6+Ibx1QLGfWPqX7d7ORmfyAtCRjGsseRfv4y2mcvwe6zxVZ3WnY5YVjVtg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/schema/-/schema-0.2.0.tgz", + "integrity": "sha512-YCDfIiGg466bAoxORVrCT4vwXImZr7BYZaAkaFSdLNP+/hmvDVez0GQ9V55h+mPC4jma1ymxHZMterQZzPhI8g==", "license": "Apache-2.0", "engines": { "node": ">=24.0.0" } }, "node_modules/@askrjs/server": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@askrjs/server/-/server-0.0.13.tgz", - "integrity": "sha512-LpfhVI3efMH4EroBgRRAjNBS5BcQKAq355h0F9cHtMNutI2IgkVfUQHyIk8S+09xYI1EIjWxDaqEA76XeL7+Xg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/server/-/server-0.2.0.tgz", + "integrity": "sha512-y5xq42zEetqopMNwE5ughFxEDg+wKvUrY4ydONqXn8mTKKSfxjG1V1lptDB8OmNjZcFiSNbE6BQ/HQ2mzPdLkA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@askrjs/auth": ">=0.0.9 <0.1.0", - "@askrjs/schema": ">=0.0.6 <0.1.0" + "@askrjs/auth": ">=0.2.0 <0.3.0", + "@askrjs/schema": ">=0.2.0 <0.3.0" }, "engines": { "node": ">=24.0.0" }, "peerDependencies": { - "@askrjs/askr": ">=0.0.89 <0.1.0" + "@askrjs/askr": ">=0.2.0 <0.3.0" }, "peerDependenciesMeta": { "@askrjs/askr": { @@ -198,45 +198,46 @@ } }, "node_modules/@askrjs/themes": { - "version": "0.0.25", - "resolved": "https://registry.npmjs.org/@askrjs/themes/-/themes-0.0.25.tgz", - "integrity": "sha512-xd6f3r5pR22anIPh/1ADdKhoJUCWXket4ur8OttUxYgXnMPZSik/7ulcymCfq3rQtBbvW0IIgRRU3hy/BluSKA==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@askrjs/themes/-/themes-0.2.3.tgz", + "integrity": "sha512-RAPUbva8MmCso35Y0lGMeCrtF2Z/cuw1KHwjxiP3+rKKHIXAoGuqn9wSXaWyk+IXTlZO5jwStW/7t7bCd14jCA==", "license": "Apache-2.0", "engines": { "node": ">=24.0.0" }, "peerDependencies": { - "@askrjs/askr": ">=0.0.88 <0.1.0", - "@askrjs/ui": ">=0.0.26 <0.1.0" + "@askrjs/askr": ">=0.2.0 <0.3.0", + "@askrjs/ui": ">=0.2.2 <0.3.0" } }, "node_modules/@askrjs/ui": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/@askrjs/ui/-/ui-0.0.27.tgz", - "integrity": "sha512-Rw1MxG1WNSt/4irJQQAWIMd9ooOyo5URRyO/RcP66f11Jh6EOXoNaKqO5mDY/sLThEhBqkLjRACHZqHj3t+kwg==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@askrjs/ui/-/ui-0.2.2.tgz", + "integrity": "sha512-lYOvFoWs3C/LJs64DXIeP3SgwBpRp3/r3FfIDcFYi5kfk8P/sIGo5sBroEfgxCaSIBhqUwVXb6OBkNIdxYTwgw==", "license": "Apache-2.0", "engines": { "node": ">=24.0.0" }, "peerDependencies": { - "@askrjs/askr": ">=0.0.88 <0.1.0" + "@askrjs/askr": ">=0.2.0 <0.3.0" } }, "node_modules/@askrjs/vite": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@askrjs/vite/-/vite-0.0.13.tgz", - "integrity": "sha512-ilOsruMEsbzYHmFRmPY1kwPchBVbzBwJaZq3OGFSShkG/GSwCO/d+34evkxbpWOxVDVpZbTfyv/RE9eMrkBBbw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@askrjs/vite/-/vite-0.2.0.tgz", + "integrity": "sha512-/k8XisZZiZG5SEmWDRYqD+oGTBE6cPMthA5pE50qSRPb5wGZ2rkXTza5UU6/84WcL3xYN8ztzfxDARSN1gpswg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@askrjs/node": ">=0.0.8 <0.1.0", + "@askrjs/node": ">=0.2.0 <0.3.0", + "oxc-parser": "^0.144.0", "parse5": "^8.0.1" }, "engines": { "node": ">=24.0.0" }, "peerDependencies": { - "@askrjs/askr": ">=0.0.88 <0.1.0", + "@askrjs/askr": ">=0.2.0 <0.3.0", "sharp": "^0.35.3", "vite": "^8.2.1", "vite-plus": "^0.2.8" @@ -1063,10 +1064,357 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.144.0.tgz", + "integrity": "sha512-IaoGBEp/huvja99PxI/b72TbKFzA/UzxxAka7f233dc/Tg/rRTX9Qn8IquFLWwWf4IddN/5TaJ8S4Subbjq7wQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.144.0.tgz", + "integrity": "sha512-u6fJu8XQXP99+9pYO3jq7F1D7V9fyFuDBShYFlr+gY+GcJzhveeN/zoMfuXxX6XBquJO0kjqKd7BjhJ7pClWXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.144.0.tgz", + "integrity": "sha512-o9xGSmMQcboJLjwI+acFf6xa7nYdp0/nRFE8ry4Xrt8OviQ9ITFDBUkAXVJMOLchSV9Pu981GxJuW0mt4i6vQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.144.0.tgz", + "integrity": "sha512-2yNm4tX++W3KLbyziVhs5alSb74a3C1uNDu/1P/AQj1ux8yZYuvbCAeJCCrGkr8J18ZmnBAzDthdTZBEAEb71w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.144.0.tgz", + "integrity": "sha512-TG4CjY1OjynplkF9nAQ9m9zboPJksnbAF+U/9xQGSXyIt+5sQRitwfQrUgjrG17/up9G8k/boNjLD2zp4xq1Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.144.0.tgz", + "integrity": "sha512-i0T9NagVmqc+rbSyBr5mDKj7TCMIBRrSteQlQJt1WhWIH/sZeOP9GB09H9w98YdinuZkDIPmO7Fz0jDC7bMvSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.144.0.tgz", + "integrity": "sha512-YUsEqM3WMS3mOON+TFf7RzS0QthzEifx7tpUQu0GSF2MsT+D6t154ZBs6WhWaCZNl0GuVDEvndCyEAUBHzSHGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.144.0.tgz", + "integrity": "sha512-LlWH4kt+IET3qIAe0e0IFLNlQ3CVUAfN//UFsA6N0/FghMh/FBk1e+wzvgG+t8WSnXkvf8B1TovquS2EJras9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.144.0.tgz", + "integrity": "sha512-ajXbXIWBWUD4U3IQxr2p6DiXwD7GPHEBLa+JteKhIfvLmBEBdTjO28lP+5r3AF2qal8cxLERfTnGs64Z22ZuXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.144.0.tgz", + "integrity": "sha512-/+sDzL/4cWEwdqenKo/DX3gkkxu7H7ytFAtealDey/Gd59yPWn64obVk6wXKVjVfXMciUUUTySxZG9AIMX3RNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.144.0.tgz", + "integrity": "sha512-dMVhPBbrd8y6aeLd7Ihn9OZhKO8QgCQVtLBTRgbmf4lKrcR61SpaQRJPJuocTc/Cn5SJMm+alHYPnzkbOGM7Dg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.144.0.tgz", + "integrity": "sha512-jQ8O0+b6J2IhJgm0DnqEJq8hG9OocmF1b4TBWCk08CRWqTmLZj/+lYs7w3OA60nb2SiqOmthQyJPacrCi7y+oQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.144.0.tgz", + "integrity": "sha512-/mZxZtcGrzuvqPLPV7gjavbROYs/dHy6+yQ2Sl/2to/+qoC/v6CcruGFnfQPzQbXXTYReXJzLb5QY9KmgCbJOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.144.0.tgz", + "integrity": "sha512-/caRGFHcarHZlBrucBwQwBbzqhD+UfZZ/r7soocS0/mp6/5KTq+1Zl/OQx5lFLcN+GpUPYszbrvQU9MCFLEzJg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.144.0.tgz", + "integrity": "sha512-qFtwAo6BWuWDjh57QDdZdYi746GW0mIeoZSGK2jJqlxIjo389Y/7lrriTOI+ou7tTvusOrSYGQZ+e+nDswt2vQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.144.0.tgz", + "integrity": "sha512-n+NgMGWWEYpH+rlkMhDvLR2k8vJDHQp3j8SoS86IS6J0hc4kuDaiYAAvu9dF86xjeGYy+h9WLj12sylmBJV9sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.144.0.tgz", + "integrity": "sha512-fShxpJiCBOdG4+jBAvahTTFUDI5djXc/+IPC1ldeC8LbyCW0h9m/7oP8DRZWI7WT2Ahv8sHtZz4ugECylCFpTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.144.0.tgz", + "integrity": "sha512-vFrYV+C3lJhIiSdNhdkZHnZ0YIClgTSluXaPMYjlGslVPD+uJg6K1s2xNL/X/gdBcy9IIbjbp0vNBwQhdMMdkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.144.0.tgz", + "integrity": "sha512-0ASbKSwdeihMekyy7y4jC0CwW3XBDZk5Sw64m/W7IReVQHaduqLYssF9KCJA2oHG9oldnl/1CMxqCoImXfqQkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxc-project/runtime": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.142.0.tgz", - "integrity": "sha512-Dv3jRrcXFdvUBUEljUu9kusrEo9G0iiqZFZNg3yCg+mkET4DD4S2Ow6Q6shFWDJwPidSa980d6YVXBlErUGADw==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.143.0.tgz", + "integrity": "sha512-zIuXUf+YGIgsPk0xlQmzTY8NCSc8jE/pSfDodlQ9H3EGZABmr+AtIjXRrnpQAXuXzhDSNqZz9cuhud8hDDLvpg==", "dev": true, "license": "MIT", "engines": { @@ -1074,9 +1422,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", - "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "dev": true, "license": "MIT", "funding": { @@ -1084,9 +1432,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.61.0.tgz", - "integrity": "sha512-BaS+1OVvg9sr+Xav0+KdWedQRcAzrdoEcwMZeqoc2F6ieC1s/t5eM35YQoRPQ7vAqkZ+p3tbQb1r9I9mrV5oGA==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.62.0.tgz", + "integrity": "sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==", "cpu": [ "arm" ], @@ -1101,9 +1449,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.61.0.tgz", - "integrity": "sha512-of8atAV0M1egGcVOMbgZCvc10sFOP3ayQBNQV5h5G3fNq8gACdEswfFk9bzGrdbM23rtg0Coxi7np7oPLcueNw==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.62.0.tgz", + "integrity": "sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==", "cpu": [ "arm64" ], @@ -1118,9 +1466,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.61.0.tgz", - "integrity": "sha512-7l8+5ov4BGwtAcmpzvEik/TG3bciwyw/S3e6j5GKH7pcQqcgCVxD3AuJeP6upto+SOTBKQ4wrrdbMt0gq8fHSQ==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.62.0.tgz", + "integrity": "sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==", "cpu": [ "arm64" ], @@ -1135,9 +1483,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.61.0.tgz", - "integrity": "sha512-Fnz4dDDXBb7udk+DmwelNjxbD6yptyxwCqwCH2ebo4RVLxVsRfFsn/AHJC49KIltPrVokamGv4SSOsiV50DTxQ==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.62.0.tgz", + "integrity": "sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==", "cpu": [ "x64" ], @@ -1152,9 +1500,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.61.0.tgz", - "integrity": "sha512-mddOebKNCP+AucmzfNsk3jgbr681qAUvgMqi865GW5gWLJ/AnzXbvjQRrny0e++NAN8aphav/aRSrfFxNsNjpA==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.62.0.tgz", + "integrity": "sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==", "cpu": [ "x64" ], @@ -1169,9 +1517,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.61.0.tgz", - "integrity": "sha512-svx59iYL+DbaZGZUIoice4W0CjRXGExnbz7Re+awIb60rVxBS2KrU7Hnlx+nZYanLGLpjneUEgo/VFEKkSZAyQ==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.62.0.tgz", + "integrity": "sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==", "cpu": [ "arm" ], @@ -1186,9 +1534,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.61.0.tgz", - "integrity": "sha512-BYK9MPJPCf6d+fLKMTruThmEyCtHzQ1zLcsrTlUVkmnoXIaHAbfpeLYQwX1tkjs7W11dyzoi6HFvKcdnvX1zNg==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.62.0.tgz", + "integrity": "sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==", "cpu": [ "arm" ], @@ -1203,9 +1551,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.61.0.tgz", - "integrity": "sha512-QUaCNLq2/EC6G5ljOuFanl9Lgw6ZWp4co7rs4+KOMUzbGfA4Lq58FHRjjF9sVIG+93XSbo343MxFATrOU1qctA==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.62.0.tgz", + "integrity": "sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==", "cpu": [ "arm64" ], @@ -1223,9 +1571,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.61.0.tgz", - "integrity": "sha512-S6uvJ6MXnRXl+zTs0CARNDvkE+cymj0EVWEKKsyKnlLlqTyQJBjw5s4D2pSIOZc+S46cy4STefzcr/sm0VzVPA==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.62.0.tgz", + "integrity": "sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==", "cpu": [ "arm64" ], @@ -1243,9 +1591,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.61.0.tgz", - "integrity": "sha512-6VDlRcytvZG6UlSIdAFKDLbppo9tvPxrWzle6vHldYFMeuDPQEfMKrkwezp7FaBq1wik9ra554ZZeRPsyIkFpg==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.62.0.tgz", + "integrity": "sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==", "cpu": [ "ppc64" ], @@ -1263,9 +1611,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.61.0.tgz", - "integrity": "sha512-KkBTYbzExpbmn15XjKPLu2fRV2PVlq+KWt+brad5rwIa03vdYoaDRWiS7raHII/dCTR6Ro4UpYUCH4t6lif4WQ==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.62.0.tgz", + "integrity": "sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==", "cpu": [ "riscv64" ], @@ -1283,9 +1631,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.61.0.tgz", - "integrity": "sha512-69tzIq7sJLVB9dxYYtvMzcSSsnZHSO+U2U19O2RqDqgj6+Q4O7HjSXdaszbcgqzhsUwzSH7z5kWvk8nmf6BHTg==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.62.0.tgz", + "integrity": "sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==", "cpu": [ "riscv64" ], @@ -1303,9 +1651,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.61.0.tgz", - "integrity": "sha512-Oqi/N0OvtOVXsPKAOOhKgGH3msRYF8BLJaNBbWiupRiKoKVyc8JRKPCfarkQJC+RgP9U8raUKLe+bNwd0HUMiA==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.62.0.tgz", + "integrity": "sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==", "cpu": [ "s390x" ], @@ -1323,9 +1671,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.61.0.tgz", - "integrity": "sha512-3TKwv/ed4uwJSemAA8P9XcoqETpjQI4waquF9UilhA9Mn/dhr1PdUEXWlL74mtc6ZNfmKPA9+NEJm01nRF8CVA==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.62.0.tgz", + "integrity": "sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==", "cpu": [ "x64" ], @@ -1343,9 +1691,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.61.0.tgz", - "integrity": "sha512-uFso4u4nLkVSlMCpgjyvWV60Gt7GvDQHnk1mmRxHIkZTMB0ljpUKwCD9FYGgN9H97x2wYl0UwEjgRZaPIuhEhw==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.62.0.tgz", + "integrity": "sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==", "cpu": [ "x64" ], @@ -1363,9 +1711,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.61.0.tgz", - "integrity": "sha512-keGLkzeOvkMpNmPp4hffXWpfoSsY6e1K8++KXD4mSSfxdvM8q9QUDsYY689TB1k6Co832DZn1MnaaVx6cIBMWQ==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.62.0.tgz", + "integrity": "sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==", "cpu": [ "arm64" ], @@ -1380,9 +1728,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.61.0.tgz", - "integrity": "sha512-VzsAISkFxmNhJ5LBDEL9VuH6tJsVJMtqYit2LyIUf/HLnsCe4Pg9SMOjjVQzGWt0bnpyfJ94CrqTqcpNZzK+ug==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.62.0.tgz", + "integrity": "sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==", "cpu": [ "arm64" ], @@ -1397,9 +1745,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.61.0.tgz", - "integrity": "sha512-xv4t7yzwJoYaLB6Zv28B3W+j7brEjsyv50rLTAQgmxJzddce9fAMCxed8dSAkbWES0zz2J29nYK5FaTuD2YBHg==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.62.0.tgz", + "integrity": "sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==", "cpu": [ "ia32" ], @@ -1414,9 +1762,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.61.0.tgz", - "integrity": "sha512-6EZXFkqOwxdDYjIn3TSNnPk3ST5E5GiYd4FiM6UF/mCL/LZSfr6D6UygTfW3R1PCQP2quCKpCEGRlij8E3VYbg==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.62.0.tgz", + "integrity": "sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==", "cpu": [ "x64" ], @@ -1515,9 +1863,9 @@ ] }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.76.0.tgz", - "integrity": "sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.77.0.tgz", + "integrity": "sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==", "cpu": [ "arm" ], @@ -1532,9 +1880,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.76.0.tgz", - "integrity": "sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.77.0.tgz", + "integrity": "sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==", "cpu": [ "arm64" ], @@ -1549,9 +1897,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.76.0.tgz", - "integrity": "sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.77.0.tgz", + "integrity": "sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==", "cpu": [ "arm64" ], @@ -1566,9 +1914,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.76.0.tgz", - "integrity": "sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.77.0.tgz", + "integrity": "sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==", "cpu": [ "x64" ], @@ -1583,9 +1931,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.76.0.tgz", - "integrity": "sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.77.0.tgz", + "integrity": "sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==", "cpu": [ "x64" ], @@ -1600,9 +1948,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.76.0.tgz", - "integrity": "sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.77.0.tgz", + "integrity": "sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==", "cpu": [ "arm" ], @@ -1617,9 +1965,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.76.0.tgz", - "integrity": "sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.77.0.tgz", + "integrity": "sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==", "cpu": [ "arm" ], @@ -1634,9 +1982,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.76.0.tgz", - "integrity": "sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.77.0.tgz", + "integrity": "sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==", "cpu": [ "arm64" ], @@ -1654,9 +2002,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.76.0.tgz", - "integrity": "sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.77.0.tgz", + "integrity": "sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==", "cpu": [ "arm64" ], @@ -1674,9 +2022,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.76.0.tgz", - "integrity": "sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.77.0.tgz", + "integrity": "sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==", "cpu": [ "ppc64" ], @@ -1694,9 +2042,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.76.0.tgz", - "integrity": "sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.77.0.tgz", + "integrity": "sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==", "cpu": [ "riscv64" ], @@ -1714,9 +2062,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.76.0.tgz", - "integrity": "sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.77.0.tgz", + "integrity": "sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==", "cpu": [ "riscv64" ], @@ -1734,9 +2082,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.76.0.tgz", - "integrity": "sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.77.0.tgz", + "integrity": "sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==", "cpu": [ "s390x" ], @@ -1754,9 +2102,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.76.0.tgz", - "integrity": "sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.77.0.tgz", + "integrity": "sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==", "cpu": [ "x64" ], @@ -1774,9 +2122,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.76.0.tgz", - "integrity": "sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.77.0.tgz", + "integrity": "sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==", "cpu": [ "x64" ], @@ -1794,9 +2142,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.76.0.tgz", - "integrity": "sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.77.0.tgz", + "integrity": "sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==", "cpu": [ "arm64" ], @@ -1811,9 +2159,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.76.0.tgz", - "integrity": "sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.77.0.tgz", + "integrity": "sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==", "cpu": [ "arm64" ], @@ -1828,9 +2176,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.76.0.tgz", - "integrity": "sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.77.0.tgz", + "integrity": "sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==", "cpu": [ "ia32" ], @@ -1845,9 +2193,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.76.0.tgz", - "integrity": "sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.77.0.tgz", + "integrity": "sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==", "cpu": [ "x64" ], @@ -2748,14 +3096,14 @@ } }, "node_modules/@voidzero-dev/vite-plus-core": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.2.8.tgz", - "integrity": "sha512-hqUJyozjE4HtJ3wwf2pOw6LnH/EF9W4+ys2s8arr/3fUdw64vzp/SfPFBVCxsGRv2UfjkIieOeZrQ1FH6Oa5Tw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-core/-/vite-plus-core-0.2.9.tgz", + "integrity": "sha512-dWqScAAwa8h/i9jCiGAMs7YarzQWInHZ5gCJNbQkHXA6Zp6A2T2anN9YMFVPpb7CwVFwkI2iPF5yl/DXtq+zUA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "=0.142.0", - "@oxc-project/types": "=0.142.0", + "@oxc-project/runtime": "=0.143.0", + "@oxc-project/types": "=0.143.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.5.44", @@ -2765,14 +3113,14 @@ "node": "^20.19.0 || ^22.18.0 || >=24.11.0" }, "optionalDependencies": { - "@voidzero-dev/vite-plus-darwin-arm64": "0.2.8", - "@voidzero-dev/vite-plus-darwin-x64": "0.2.8", - "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.8", - "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.8", - "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.8", - "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.8", - "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.8", - "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.8", + "@voidzero-dev/vite-plus-darwin-arm64": "0.2.9", + "@voidzero-dev/vite-plus-darwin-x64": "0.2.9", + "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.9", + "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.9", + "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.9", + "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.9", + "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.9", + "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.9", "fsevents": "~2.3.3" }, "peerDependencies": { @@ -2848,6 +3196,16 @@ } } }, + "node_modules/@voidzero-dev/vite-plus-core/node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@voidzero-dev/vite-plus-core/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2864,9 +3222,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-darwin-arm64": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-arm64/-/vite-plus-darwin-arm64-0.2.8.tgz", - "integrity": "sha512-zcGNhemAhux0/sGDZBK5sHvv5bPm9kj+NhDKs8MYfZMjc51kpzMOV3PWhtTOXjJYDiSxv+Ss4AFj2y2wvQDgWg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-arm64/-/vite-plus-darwin-arm64-0.2.9.tgz", + "integrity": "sha512-/qJHqMfyy/LiCJk4UYZfFW6Comsfm8zDuy4P8UFWnFqRjmefYvZbMK4ThYNM87tKNWYWjsnClzssjJOmDTAc8w==", "cpu": [ "arm64" ], @@ -2881,9 +3239,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-darwin-x64": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-x64/-/vite-plus-darwin-x64-0.2.8.tgz", - "integrity": "sha512-56vcDhYuHg1HSqQlFIEexs9WbDtvT2Z8QXq2pEUYVplmP55ekDGx8+Ibu69jBhk+lNwmLAonCW1alffQxMKirQ==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-darwin-x64/-/vite-plus-darwin-x64-0.2.9.tgz", + "integrity": "sha512-3MGnNeazgYAqQTaC7JIbZyrTHmxSXTWFr9G1yAdeItKasB1R8HKIkCS1Qnr49Ge4S/cyOODivdbcpqFkrT93ng==", "cpu": [ "x64" ], @@ -2898,9 +3256,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-arm64-gnu": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-gnu/-/vite-plus-linux-arm64-gnu-0.2.8.tgz", - "integrity": "sha512-EU9zlSieuouJlnLEkVNw9guig5rTQ5hfMeNH0AlRjb1C1xVpUQdf0uRjXg1PHP3os/WspyCPB46Q80lEaeRb0w==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-gnu/-/vite-plus-linux-arm64-gnu-0.2.9.tgz", + "integrity": "sha512-6LmukER8qD4UBIRqMNv4Ilq7CxfRhngLUXlMv8vbTupeLRWPJSKvKEHyRxCwr6JP57Gxfr8KrX1ye42WzcZF0g==", "cpu": [ "arm64" ], @@ -2918,9 +3276,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-arm64-musl": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-musl/-/vite-plus-linux-arm64-musl-0.2.8.tgz", - "integrity": "sha512-VbWUwaSAw0Ndql5uYy/Gfqt7duSy1sxTacLngD5M5kF4ZK7yoKKcx8iFiA0il1Gf3J7OGojPonKi5b3NSn3K7A==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-arm64-musl/-/vite-plus-linux-arm64-musl-0.2.9.tgz", + "integrity": "sha512-cBs626GWkyJlwKP0nsdHlMWpuTl9xOWRxAUoqtxXPtw80bVy4WM5eNS4SXPC5pX10jR7DRIkRpzNAsy7fv8Faw==", "cpu": [ "arm64" ], @@ -2938,9 +3296,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-x64-gnu": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-gnu/-/vite-plus-linux-x64-gnu-0.2.8.tgz", - "integrity": "sha512-gZfnd3l9vNiP7QXQIEN9r2M9ZKCh8sg2vhBv04sZjMNAeQ05IXJMkWYkcfTUO7jhf8pdVt3VHQ4x6ULm0OnELg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-gnu/-/vite-plus-linux-x64-gnu-0.2.9.tgz", + "integrity": "sha512-2Iy8x4PCPMNzXeu3pREevlggoeK8PwtdUiCpoSybAZBtR/aqMxsJREyt/eKv45F8lsiNlA8PbIWEvPxLSgzFLQ==", "cpu": [ "x64" ], @@ -2958,9 +3316,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-linux-x64-musl": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-musl/-/vite-plus-linux-x64-musl-0.2.8.tgz", - "integrity": "sha512-5NtDUvjzF/HcjQraebpiVUgjX2CMKv2Jdmi5YpzHlt4g86sFA9M5a+OqBlgUctdzTeolnjxRsfurKiefG/hILA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-linux-x64-musl/-/vite-plus-linux-x64-musl-0.2.9.tgz", + "integrity": "sha512-zuGx+eRotWPd9cmh1X9AfsC2tN/Ad9Hk6LAzlxoKJUjEkTsY3WjVJ6Da0z48SAUFuenI0JkdqXnMCrePtGvWHg==", "cpu": [ "x64" ], @@ -2978,9 +3336,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-win32-arm64-msvc": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-arm64-msvc/-/vite-plus-win32-arm64-msvc-0.2.8.tgz", - "integrity": "sha512-Atjz6KsG42ntfQkM6Ks09Ifxm+ZIca2DnA31tO2FcPlHetBxYEGZwSNCOHAnoNnLrh2qNm+7aOC329a2ijhxLg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-arm64-msvc/-/vite-plus-win32-arm64-msvc-0.2.9.tgz", + "integrity": "sha512-kaKb5Q8ReYTBfvLgUhdpLJG3aoNF8HOVJpf8qBm+THsd4WRrkRwsBHp2ITsU8oXl2gnDjFGhPDaURegd/z6Wxw==", "cpu": [ "arm64" ], @@ -2995,9 +3353,9 @@ } }, "node_modules/@voidzero-dev/vite-plus-win32-x64-msvc": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-x64-msvc/-/vite-plus-win32-x64-msvc-0.2.8.tgz", - "integrity": "sha512-VSpa+gK1MjOH7GeO6H5xtJLqisQOWVeRQIDQs9XKizXFX0R4NCrioYO6Rc31QLXfL0lPCkwsKhp/9M9gllImRg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@voidzero-dev/vite-plus-win32-x64-msvc/-/vite-plus-win32-x64-msvc-0.2.9.tgz", + "integrity": "sha512-/fEk3gbQTJknCiYM/GTL/L++Azsav8rCAjmtKrjmCbqEif5IMzqTfvM68n+PiLB3JVoQJGF/mg2niODr0IE/2w==", "cpu": [ "x64" ], @@ -3024,6 +3382,7 @@ "version": "0.9.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "deprecated": "this version has critical issues, please update to the latest version", "license": "MIT", "engines": { "node": ">=14.6" @@ -4025,9 +4384,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz", - "integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.3.0.tgz", + "integrity": "sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==", "dev": true, "funding": [ { @@ -4738,10 +5097,47 @@ "node": ">=12.20.0" } }, + "node_modules/oxc-parser": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.144.0.tgz", + "integrity": "sha512-eacM4wMgGWXctHubY262yo+50E76qtQBqe+uK73YEV1IT3qP12Acbnf9Nc8t+agIAdnko9iVT4KF83/d0EjY5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.144.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.144.0", + "@oxc-parser/binding-android-arm64": "0.144.0", + "@oxc-parser/binding-darwin-arm64": "0.144.0", + "@oxc-parser/binding-darwin-x64": "0.144.0", + "@oxc-parser/binding-freebsd-x64": "0.144.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.144.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.144.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.144.0", + "@oxc-parser/binding-linux-arm64-musl": "0.144.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.144.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.144.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.144.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.144.0", + "@oxc-parser/binding-linux-x64-gnu": "0.144.0", + "@oxc-parser/binding-linux-x64-musl": "0.144.0", + "@oxc-parser/binding-openharmony-arm64": "0.144.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.144.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.144.0", + "@oxc-parser/binding-win32-x64-msvc": "0.144.0" + } + }, "node_modules/oxfmt": { - "version": "0.61.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.61.0.tgz", - "integrity": "sha512-DxdHBEMYpcEnHoUHjjOigUqV2TYKsvxLwUPXnVYBjgFdqrcQ/91OtwubtZ2PUodCs3sStI8R5Qw3fKNGK4e8wQ==", + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.62.0.tgz", + "integrity": "sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4757,25 +5153,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.61.0", - "@oxfmt/binding-android-arm64": "0.61.0", - "@oxfmt/binding-darwin-arm64": "0.61.0", - "@oxfmt/binding-darwin-x64": "0.61.0", - "@oxfmt/binding-freebsd-x64": "0.61.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.61.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.61.0", - "@oxfmt/binding-linux-arm64-gnu": "0.61.0", - "@oxfmt/binding-linux-arm64-musl": "0.61.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.61.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.61.0", - "@oxfmt/binding-linux-riscv64-musl": "0.61.0", - "@oxfmt/binding-linux-s390x-gnu": "0.61.0", - "@oxfmt/binding-linux-x64-gnu": "0.61.0", - "@oxfmt/binding-linux-x64-musl": "0.61.0", - "@oxfmt/binding-openharmony-arm64": "0.61.0", - "@oxfmt/binding-win32-arm64-msvc": "0.61.0", - "@oxfmt/binding-win32-ia32-msvc": "0.61.0", - "@oxfmt/binding-win32-x64-msvc": "0.61.0" + "@oxfmt/binding-android-arm-eabi": "0.62.0", + "@oxfmt/binding-android-arm64": "0.62.0", + "@oxfmt/binding-darwin-arm64": "0.62.0", + "@oxfmt/binding-darwin-x64": "0.62.0", + "@oxfmt/binding-freebsd-x64": "0.62.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.62.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.62.0", + "@oxfmt/binding-linux-arm64-gnu": "0.62.0", + "@oxfmt/binding-linux-arm64-musl": "0.62.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.62.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.62.0", + "@oxfmt/binding-linux-riscv64-musl": "0.62.0", + "@oxfmt/binding-linux-s390x-gnu": "0.62.0", + "@oxfmt/binding-linux-x64-gnu": "0.62.0", + "@oxfmt/binding-linux-x64-musl": "0.62.0", + "@oxfmt/binding-openharmony-arm64": "0.62.0", + "@oxfmt/binding-win32-arm64-msvc": "0.62.0", + "@oxfmt/binding-win32-ia32-msvc": "0.62.0", + "@oxfmt/binding-win32-x64-msvc": "0.62.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -4791,9 +5187,9 @@ } }, "node_modules/oxlint": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.76.0.tgz", - "integrity": "sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==", + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.77.0.tgz", + "integrity": "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==", "dev": true, "license": "MIT", "bin": { @@ -4806,25 +5202,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.76.0", - "@oxlint/binding-android-arm64": "1.76.0", - "@oxlint/binding-darwin-arm64": "1.76.0", - "@oxlint/binding-darwin-x64": "1.76.0", - "@oxlint/binding-freebsd-x64": "1.76.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.76.0", - "@oxlint/binding-linux-arm-musleabihf": "1.76.0", - "@oxlint/binding-linux-arm64-gnu": "1.76.0", - "@oxlint/binding-linux-arm64-musl": "1.76.0", - "@oxlint/binding-linux-ppc64-gnu": "1.76.0", - "@oxlint/binding-linux-riscv64-gnu": "1.76.0", - "@oxlint/binding-linux-riscv64-musl": "1.76.0", - "@oxlint/binding-linux-s390x-gnu": "1.76.0", - "@oxlint/binding-linux-x64-gnu": "1.76.0", - "@oxlint/binding-linux-x64-musl": "1.76.0", - "@oxlint/binding-openharmony-arm64": "1.76.0", - "@oxlint/binding-win32-arm64-msvc": "1.76.0", - "@oxlint/binding-win32-ia32-msvc": "1.76.0", - "@oxlint/binding-win32-x64-msvc": "1.76.0" + "@oxlint/binding-android-arm-eabi": "1.77.0", + "@oxlint/binding-android-arm64": "1.77.0", + "@oxlint/binding-darwin-arm64": "1.77.0", + "@oxlint/binding-darwin-x64": "1.77.0", + "@oxlint/binding-freebsd-x64": "1.77.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", + "@oxlint/binding-linux-arm-musleabihf": "1.77.0", + "@oxlint/binding-linux-arm64-gnu": "1.77.0", + "@oxlint/binding-linux-arm64-musl": "1.77.0", + "@oxlint/binding-linux-ppc64-gnu": "1.77.0", + "@oxlint/binding-linux-riscv64-gnu": "1.77.0", + "@oxlint/binding-linux-riscv64-musl": "1.77.0", + "@oxlint/binding-linux-s390x-gnu": "1.77.0", + "@oxlint/binding-linux-x64-gnu": "1.77.0", + "@oxlint/binding-linux-x64-musl": "1.77.0", + "@oxlint/binding-openharmony-arm64": "1.77.0", + "@oxlint/binding-win32-arm64-msvc": "1.77.0", + "@oxlint/binding-win32-ia32-msvc": "1.77.0", + "@oxlint/binding-win32-x64-msvc": "1.77.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", @@ -5395,9 +5791,9 @@ } }, "node_modules/tsx": { - "version": "4.23.11", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.11.tgz", - "integrity": "sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5600,13 +5996,13 @@ } }, "node_modules/vite-plus": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/vite-plus/-/vite-plus-0.2.8.tgz", - "integrity": "sha512-JULDOsy7gxG0o2FuvnsWnzRjzD1x9WM9mya3MNMOJUqTsL2pqiN2nq+dzoQMw+FGCytRw7yG1/p5qNcmYMM5Fw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/vite-plus/-/vite-plus-0.2.9.tgz", + "integrity": "sha512-8uRNqAxh9no3AU4Lep8BEYhkim07+3NO+mhuxTWiN0k30syGT/2+ue/DtWYhtzQ7yi2f2WKpjOhoI4/QkWWbUg==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.142.0", + "@oxc-project/types": "=0.143.0", "@oxlint/plugins": "=1.73.0", "@vitest/browser": "4.1.10", "@vitest/browser-preview": "4.1.10", @@ -5617,9 +6013,9 @@ "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", - "@voidzero-dev/vite-plus-core": "0.2.8", - "oxfmt": "=0.61.0", - "oxlint": "=1.76.0", + "@voidzero-dev/vite-plus-core": "0.2.9", + "oxfmt": "=0.62.0", + "oxlint": "=1.77.0", "oxlint-tsgolint": "=7.0.2001", "vitest": "4.1.10" }, @@ -5633,14 +6029,14 @@ "node": "^20.19.0 || ^22.18.0 || >=24.11.0" }, "optionalDependencies": { - "@voidzero-dev/vite-plus-darwin-arm64": "0.2.8", - "@voidzero-dev/vite-plus-darwin-x64": "0.2.8", - "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.8", - "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.8", - "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.8", - "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.8", - "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.8", - "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.8" + "@voidzero-dev/vite-plus-darwin-arm64": "0.2.9", + "@voidzero-dev/vite-plus-darwin-x64": "0.2.9", + "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.2.9", + "@voidzero-dev/vite-plus-linux-arm64-musl": "0.2.9", + "@voidzero-dev/vite-plus-linux-x64-gnu": "0.2.9", + "@voidzero-dev/vite-plus-linux-x64-musl": "0.2.9", + "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.2.9", + "@voidzero-dev/vite-plus-win32-x64-msvc": "0.2.9" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.10", @@ -5655,6 +6051,16 @@ } } }, + "node_modules/vite-plus/node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5888,9 +6294,9 @@ } }, "node_modules/xml-crypto/node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5908,9 +6314,9 @@ } }, "node_modules/xml-encryption/node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/ui/package.json b/ui/package.json index e119ef46..c0272021 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,22 +21,25 @@ "gen:adapters": "askr generate ../public/openapi.yml -o ./src/adapters/generated" }, "dependencies": { - "@askrjs/askr": "^0.0.91", - "@askrjs/fetch": "0.0.5", - "@askrjs/lucide": "0.0.9", - "@askrjs/themes": "0.0.25", - "@askrjs/ui": "^0.0.27" + "@askrjs/askr": "^0.2.2", + "@askrjs/fetch": "0.2.0", + "@askrjs/lucide": "0.2.0", + "@askrjs/themes": "^0.2.3", + "@askrjs/ui": "^0.2.2" }, "devDependencies": { - "@askrjs/cli": "0.0.23", - "@askrjs/vite": "0.0.13", + "@askrjs/cli": "0.2.1", + "@askrjs/vite": "0.2.0", "@playwright/test": "^1.62.1", "@types/node": "^26.2.0", "autoprefixer": "^10.5.4", "jsdom": "^30.0.1", "postcss": "^8.5.26", "typescript": "^7.0.2", - "vite-plus": "^0.2.8" + "vite-plus": "^0.2.9" + }, + "overrides": { + "@vitest/browser-playwright": "4.1.10" }, "packageManager": "npm@12.0.2" } diff --git a/ui/src/components/shared/app-footer.tsx b/ui/src/components/shared/app-footer.tsx index 98b96f39..7ed77a8d 100644 --- a/ui/src/components/shared/app-footer.tsx +++ b/ui/src/components/shared/app-footer.tsx @@ -1,19 +1,19 @@ -import { Container, Footer, FooterLink, Inline, Text } from "@askrjs/themes/components"; +import { Container, Footer, FooterLink, Text, Block } from "@askrjs/themes/components"; export default function AppFooter() { return (
- + Fitz operator console - + Fitz broker fitz-ts fitz-go - - + +
); diff --git a/ui/src/components/shared/copy-text-button.tsx b/ui/src/components/shared/copy-text-button.tsx index 8594a79d..388ab62e 100644 --- a/ui/src/components/shared/copy-text-button.tsx +++ b/ui/src/components/shared/copy-text-button.tsx @@ -19,9 +19,10 @@ export default function CopyTextButton({ label, text }: { label: string; text: s return ( ); } diff --git a/ui/src/components/shared/data-table.tsx b/ui/src/components/shared/data-table.tsx new file mode 100644 index 00000000..02de1fa2 --- /dev/null +++ b/ui/src/components/shared/data-table.tsx @@ -0,0 +1,108 @@ +import { For, Show } from "@askrjs/askr/control"; +import type { JSXElement } from "@askrjs/askr/foundations/structures"; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; + +export interface DataTableCellProps { + column: DataTableColumn; + row: Row; + rowIndex: number; + rowKey: string; + selected: false; +} + +export interface DataTableColumn { + cellComponent: (props: DataTableCellProps) => JSXElement | JSX.Element | null; + header: JSXElement | string; + id: string; + width?: number | string; +} + +export interface DataTableProps { + ariaLabel: string; + class?: string; + columns: readonly DataTableColumn[]; + dataHasMetrics?: boolean; + getKey: (row: Row, index: number) => string | number; + id?: string; + onRowClick?: (row: Row, rowIndex: number, rowKey: string, event: MouseEvent) => void; + rows: readonly Row[]; +} + +export default function DataTable({ + ariaLabel, + class: className, + columns, + dataHasMetrics, + getKey, + id, + onRowClick, + rows, +}: DataTableProps) { + const visibleRows = rows.slice(0, 500); + + return ( +
+ + + column.id}> + {(column) => ( + + )} + + + + + column.id}> + {(column) => ( + {column.header} + )} + + + + + String(getKey(row, index))}> + {(row, rowIndex) => { + const index = rowIndex(); + const rowKey = String(getKey(row, index)); + + return ( + onRowClick?.(row, index, rowKey, event)} + > + column.id}> + {(column) => ( + + {column.cellComponent({ + column, + row, + rowIndex: index, + rowKey, + selected: false, + })} + + )} + + + ); + }} + + +
+ visibleRows.length}> +

+ Showing the first {visibleRows.length} of {rows.length} rows. Refine the current scope or + filter to inspect later rows. +

+
+
+ ); +} diff --git a/ui/src/components/shared/domain-data-section.tsx b/ui/src/components/shared/domain-data-section.tsx new file mode 100644 index 00000000..a2a7aa64 --- /dev/null +++ b/ui/src/components/shared/domain-data-section.tsx @@ -0,0 +1,47 @@ +import type { JSXElement } from "@askrjs/askr/foundations/structures"; +import { Block, Section, Text } from "@askrjs/themes/components"; + +export interface DomainDataSectionProps { + actions?: JSXElement | JSX.Element | null; + children: JSXElement | JSX.Element; + class?: string; + description?: string; + id: string; + title: string; +} + +export default function DomainDataSection({ + actions, + children, + class: className, + description, + id, + title, +}: DomainDataSectionProps) { + return ( +
+ + +

{title}

+ {description ? ( + + {description} + + ) : null} +
+ {actions ? ( + + {actions} + + ) : null} +
+ {children} +
+ ); +} diff --git a/ui/src/components/shared/domain-header.tsx b/ui/src/components/shared/domain-header.tsx index bf8eaa3b..42192e98 100644 --- a/ui/src/components/shared/domain-header.tsx +++ b/ui/src/components/shared/domain-header.tsx @@ -46,6 +46,7 @@ export default function DomainHeader({ {title} diff --git a/ui/src/components/shared/domain-inventory-page.tsx b/ui/src/components/shared/domain-inventory-page.tsx index cc13f8f5..b6fb26bd 100644 --- a/ui/src/components/shared/domain-inventory-page.tsx +++ b/ui/src/components/shared/domain-inventory-page.tsx @@ -1,18 +1,29 @@ -import { For, Show } from "@askrjs/askr/control"; +import { Show } from "@askrjs/askr/control"; import { currentRoute } from "@askrjs/askr/router"; -import { Alert, Button, Card, CardContent, Stack, Text } from "@askrjs/themes/components"; +import { Alert, Button, Block } from "@askrjs/themes/components"; import DomainHeader from "./domain-header"; import type { DomainHeaderProps } from "./domain-header"; import DomainPageFrame from "./domain-page-frame"; import OperatorScopeStrip from "./operator-scope-strip"; +import DomainSummaryStrip from "./domain-summary-strip"; +import DomainScopeInventoryTable from "./domain-scope-inventory-table"; import DomainResourceInventoryTable, { type DomainResourceInventory, type DomainResourceMetricColumn, } from "./domain-resource-inventory-table"; import { QueryErrorState, QueryLoadingState, QueryRefreshingState } from "./query-state"; import { formatUnknownError } from "@/shared/errors/format"; -import { formatDisplayValue } from "@/shared/format"; -import type { DomainSegment } from "@/shared/navigation/domains"; +import { domainTitleForSegment, type DomainSegment } from "@/shared/navigation/domains"; + +function decodeRouteParam(value: string | undefined) { + if (!value) return undefined; + + try { + return decodeURIComponent(value); + } catch { + return value; + } +} export interface DomainInventoryQuery { data?: TInventory | null; @@ -73,9 +84,20 @@ export default function DomainInventoryPage) { const route = currentRoute(); + const realm = decodeRouteParam(route.params.realm); + const area = decodeRouteParam(route.params.area); + const domainTitle = domainTitleForSegment(domain); + const scopedRealm = inventory.data?.realms.find((item) => item.realm === realm); + const pageTitle = area ?? realm ?? title; + const pageEyebrow = area ? `${domainTitle} area` : realm ? `${domainTitle} realm` : eyebrow; + const pageDescription = area + ? `Resources in ${realm} / ${area}.` + : realm + ? `Areas in the ${realm} realm.` + : description; const onRefresh = () => refreshAll(refreshers ?? [inventory.refresh]); const isRefreshing = refreshing ?? inventory.refreshing; - const hasScopedInventory = Boolean(route.params.realm || route.params.area); + const hasScopedInventory = Boolean(realm || area); const freshness = isRefreshing ? "Refreshing" : !inventory.data && inventory.loading @@ -92,11 +114,11 @@ export default function DomainInventoryPage - + - + @@ -116,7 +138,7 @@ export default function DomainInventoryPage - + @@ -133,46 +155,35 @@ export default function DomainInventoryPage 0 && !hasScopedInventory}> -
- stat.label}> - {(stat) => ( - - - - - {stat.label} - - - {formatDisplayValue(stat.value)} - - {stat.caption ? ( - - {stat.caption} - - ) : null} - - - - )} - -
+ +
+ + } + > + - -
+ - + ); } diff --git a/ui/src/components/shared/domain-page-frame.tsx b/ui/src/components/shared/domain-page-frame.tsx index 1835a59f..f5152f52 100644 --- a/ui/src/components/shared/domain-page-frame.tsx +++ b/ui/src/components/shared/domain-page-frame.tsx @@ -1,5 +1,5 @@ import { task } from "@askrjs/askr/resources"; -import { Block, Main, Stack } from "@askrjs/themes/components"; +import { Block, Main } from "@askrjs/themes/components"; import OperatorBreadcrumbs from "./operator-breadcrumbs"; export interface DomainPageFrameProps { @@ -51,16 +51,19 @@ export default function DomainPageFrame({ children }: DomainPageFrameProps) {
- + - {children} + + {children} + - +
); } diff --git a/ui/src/components/shared/domain-realm-table.tsx b/ui/src/components/shared/domain-realm-table.tsx index 5076c3be..152a2ca4 100644 --- a/ui/src/components/shared/domain-realm-table.tsx +++ b/ui/src/components/shared/domain-realm-table.tsx @@ -52,15 +52,20 @@ export default function DomainRealmTable({ {realm.realm} ) : ( - {realm.realm} + + {realm.realm} + )} - {realm.note ?? "Active"} + + {realm.note ?? "Active"} + )} diff --git a/ui/src/components/shared/domain-resource-inventory-table.tsx b/ui/src/components/shared/domain-resource-inventory-table.tsx index 70bb8a9c..72942c61 100644 --- a/ui/src/components/shared/domain-resource-inventory-table.tsx +++ b/ui/src/components/shared/domain-resource-inventory-table.tsx @@ -1,9 +1,10 @@ import { state } from "@askrjs/askr"; import { Link, currentRoute, navigate, updateRouteQuery } from "@askrjs/askr/router"; import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, SearchIcon, XIcon } from "@askrjs/lucide"; -import { Input, VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import { Input } from "@askrjs/ui"; import { Button, Text } from "@askrjs/themes/components"; -import { QueryEmptyState } from "./query-state"; +import DataTable, { type DataTableColumn } from "./data-table"; +import { QueryCompactEmptyState } from "./query-state"; import type { ResourceInventoryResource } from "@/features/resource/resource-models"; import { formatNumber } from "@/shared/format"; import { domainScopeHref, formatFitzRoute, type DomainSegment } from "@/shared/navigation/domains"; @@ -122,10 +123,6 @@ export function DomainResourceMetricText(props: { children: unknown; title?: str ); } -function tableHeight(rowCount: number) { - return `${Math.min(620, Math.max(140, 44 + rowCount * 48))}px`; -} - function shouldIgnoreRowClick(event: MouseEvent) { if (event.defaultPrevented) return true; const target = event.target; @@ -259,11 +256,11 @@ export function PureDomainResourceInventoryTable({ ); } - const columns: readonly VirtualTableColumn[] = [ + const columns: readonly DataTableColumn[] = [ { id: "route", header: "Route", - width: hasMetrics ? "34%" : "100%", + width: hasMetrics ? "28%" : "100%", cellComponent: ({ row }) => { const route = formatFitzRoute(domain, row); @@ -274,18 +271,16 @@ export function PureDomainResourceInventoryTable({ ); }, }, - ...metricColumns.map( - (column): VirtualTableColumn => ({ - id: column.id, - header: sortHeader(column), - width: column.width, - cellComponent: ({ row }) => ( - - {column.cell(row)} - - ), - }), - ), + ...metricColumns.map((column): DataTableColumn => ({ + id: column.id, + header: sortHeader(column), + width: column.width, + cellComponent: ({ row }) => ( + + {column.cell(row)} + + ), + })), ]; return ( @@ -333,31 +328,27 @@ export function PureDomainResourceInventoryTable({ {allRows.length === 0 ? ( - + ) : rows.length === 0 ? ( - + ) : ( <> {hasMetrics ? (

Scroll horizontally to view every metric.

) : null} - + id={`${domain}-inventory-table`} - aria-label={title} - class="domain-resource-virtual-table" - data-has-metrics={hasMetrics ? "true" : "false"} + ariaLabel={title} + class="domain-resource-data-table" + dataHasMetrics={hasMetrics} columns={columns} getKey={(row) => `${row.realm}:${row.area}:${row.resource}:${row.operation ?? ""}`} - headerHeight={44} onRowClick={(row, _rowIndex, _rowKey, event) => { if (!shouldIgnoreRowClick(event)) { onRowOpen(row); } }} - overscan={8} - rowHeight={48} rows={rows} - style={{ height: tableHeight(rows.length) }} /> )} diff --git a/ui/src/components/shared/domain-scope-inventory-table.tsx b/ui/src/components/shared/domain-scope-inventory-table.tsx new file mode 100644 index 00000000..75765c67 --- /dev/null +++ b/ui/src/components/shared/domain-scope-inventory-table.tsx @@ -0,0 +1,107 @@ +import { For, Show } from "@askrjs/askr/control"; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; +import type { + DomainResourceInventoryArea, + DomainResourceInventoryRealm, +} from "./domain-resource-inventory-table"; +import { QueryCompactEmptyState } from "./query-state"; +import { formatNumber } from "@/shared/format"; +import { domainScopeHref, type DomainSegment } from "@/shared/navigation/domains"; + +interface DomainScopeInventoryTableProps { + areas?: readonly DomainResourceInventoryArea[]; + domain: DomainSegment; + emptyDescription: string; + realms?: readonly DomainResourceInventoryRealm[]; + realm?: string; +} + +function resourceCount(area: DomainResourceInventoryArea) { + return area.resourceEntries?.length || area.resources.length; +} + +export default function DomainScopeInventoryTable({ + areas = [], + domain, + emptyDescription, + realms = [], + realm, +}: DomainScopeInventoryTableProps) { + const showingAreas = realm !== undefined; + const rows = showingAreas ? areas : realms; + const title = showingAreas ? "Areas" : "Realms"; + + return ( +
+
+
+

{title}

+

Select {showingAreas ? "an area" : "a realm"} to continue the drilldown.

+
+ {formatNumber(rows.length)} visible +
+ + 0} + fallback={} + > +
+ + + + {showingAreas ? "Area" : "Realm"} + {showingAreas ? null : Areas} + Resources + + + + item.realm}> + {(item) => { + const href = domainScopeHref(domain, { realm: item.realm }); + + return ( + + + + {item.realm} + + + {formatNumber(item.areas.length)} + + {formatNumber( + item.areas.reduce((total, area) => total + resourceCount(area), 0), + )} + + + ); + }} + + } + > + area.area}> + {(area) => { + const href = domainScopeHref(domain, { realm, area: area.area }); + + return ( + + + + {area.area} + + + {formatNumber(resourceCount(area))} + + ); + }} + + + +
+
+
+
+ ); +} diff --git a/ui/src/components/shared/domain-summary-strip.tsx b/ui/src/components/shared/domain-summary-strip.tsx new file mode 100644 index 00000000..654d3b2b --- /dev/null +++ b/ui/src/components/shared/domain-summary-strip.tsx @@ -0,0 +1,82 @@ +import { For } from "@askrjs/askr/control"; +import { + Block, + Card, + CardContent, + Section, + Stat, + StatDescription, + StatLabel, + StatValue, + Text, +} from "@askrjs/themes/components"; +import { formatDisplayValue } from "@/shared/format"; + +export interface DomainSummaryItem { + caption?: string; + label: string; + value: string | number; +} + +export interface DomainSummaryStripProps { + ariaLabel?: string; + class?: string; + description?: string; + id?: string; + items: readonly DomainSummaryItem[]; + title?: string; +} + +export default function DomainSummaryStrip({ + ariaLabel, + class: className, + description, + id, + items, + title, +}: DomainSummaryStripProps) { + const titleId = title + ? (id ?? `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-title`) + : undefined; + + return ( +
+ {title || description ? ( + + {title ?

{title}

: null} + {description ? ( + + {description} + + ) : null} +
+ ) : null} + + item.label}> + {(item) => ( + + + + {item.label} + + + {formatDisplayValue(item.value)} + + + {item.caption ? {item.caption} : null} + + + + )} + + +
+ ); +} diff --git a/ui/src/components/shared/query-state.tsx b/ui/src/components/shared/query-state.tsx index 37adc57f..1ec34252 100644 --- a/ui/src/components/shared/query-state.tsx +++ b/ui/src/components/shared/query-state.tsx @@ -1,6 +1,5 @@ -import { EmptyState, Spinner } from "@askrjs/themes/components"; +import { EmptyState, Spinner, Block, Text } from "@askrjs/themes/components"; import { Badge } from "@askrjs/themes/components"; -import { Stack } from "@askrjs/themes/components"; import { Button } from "@askrjs/themes/components"; import { formatUnknownError } from "@/shared/errors/format"; import { AppApiError } from "@/shared/errors/api"; @@ -28,7 +27,9 @@ function QueryStateCard({ }) { return (
- {children} + + {children} +
); } @@ -41,7 +42,13 @@ export function QueryLoadingState({ return (
- } description={description} /> + } + description={description} + />
); @@ -61,6 +68,8 @@ export function QueryErrorState({ return ( - + ); } export function QueryCompactEmptyState({ - class: className = "domain-state domain-state-compact", + class: className = "domain-state-compact", description, title = "Nothing to show", }: QueryStateProps) { return ( - - - + + + {title} + + + {description} + + ); } diff --git a/ui/src/components/shared/queue-dead-letter-table.tsx b/ui/src/components/shared/queue-dead-letter-table.tsx index 513f22af..f89e47f4 100644 --- a/ui/src/components/shared/queue-dead-letter-table.tsx +++ b/ui/src/components/shared/queue-dead-letter-table.tsx @@ -1,4 +1,4 @@ -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import DataTable, { type DataTableColumn } from "./data-table"; import { RefreshCwIcon, Trash2Icon } from "@askrjs/lucide"; import { Button } from "@askrjs/themes/components"; import type { DeadLetterMessage } from "@/features/queue/queue-models"; @@ -20,47 +20,27 @@ export default function QueueDeadLetterTable({ pendingMessageId = null, }: QueueDeadLetterTableProps) { const hasActions = Boolean(onReplay || onPurge); - const columns: readonly VirtualTableColumn[] = [ + const columns: readonly DataTableColumn[] = [ { id: "message", header: "Message", - width: hasActions ? "10%" : "14%", + width: hasActions ? "12%" : "16%", cellComponent: ({ row }) => ( {row.messageId} ), }, - { - id: "context", - header: "Context", - width: hasActions ? "18%" : "24%", - cellComponent: ({ row }) => { - const context = `${row.realm} / ${row.area} / ${row.resource}`; - - return ( - - {context} - - ); - }, - }, - { - id: "family", - header: "Family", - width: hasActions ? "7%" : "10%", - cellComponent: ({ row }) => {row.family}, - }, { id: "attempts", header: "Attempts", - width: hasActions ? "7%" : "10%", + width: hasActions ? "12%" : "14%", cellComponent: ({ row }) => {row.attempts}, }, { id: "dead-lettered", header: "Dead-lettered", - width: hasActions ? "15%" : "18%", + width: hasActions ? "20%" : "24%", cellComponent: ({ row }) => ( {formatTimestamp(row.deadLetteredAt)} @@ -70,7 +50,7 @@ export default function QueueDeadLetterTable({ { id: "reason", header: "Reason", - width: hasActions ? "19%" : "24%", + width: hasActions ? "30%" : "46%", cellComponent: ({ row }) => ( {row.reason} @@ -82,7 +62,7 @@ export default function QueueDeadLetterTable({ { id: "actions", header: "Actions", - width: "24%", + width: "26%", cellComponent: ({ row }) => (
{onReplay ? ( @@ -115,23 +95,17 @@ export default function QueueDeadLetterTable({ ) : null}
), - } satisfies VirtualTableColumn, + } satisfies DataTableColumn, ] : []), ]; - const tableHeight = Math.min(480, Math.max(144, 44 + messages.length * 52)); - return ( - - aria-label="Dead-letter queue messages" - class="queue-resource-virtual-table" + + ariaLabel="Dead-letter queue messages" + class="queue-resource-data-table" columns={columns} getKey={(message) => message.messageId} - headerHeight={44} - overscan={8} - rowHeight={52} rows={messages} - style={{ height: `${tableHeight}px` }} /> ); } diff --git a/ui/src/components/shared/queue-inflight-table.tsx b/ui/src/components/shared/queue-inflight-table.tsx index 97d1f154..ad429f18 100644 --- a/ui/src/components/shared/queue-inflight-table.tsx +++ b/ui/src/components/shared/queue-inflight-table.tsx @@ -1,4 +1,4 @@ -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import DataTable, { type DataTableColumn } from "./data-table"; import type { QueueInflightMessage } from "@/features/queue/queue-resource-models"; import { formatTimestamp } from "@/shared/format"; @@ -7,7 +7,7 @@ export interface QueueInflightTableProps { } export default function QueueInflightTable({ messages }: QueueInflightTableProps) { - const columns: readonly VirtualTableColumn[] = [ + const columns: readonly DataTableColumn[] = [ { id: "message", header: "Message", @@ -18,24 +18,10 @@ export default function QueueInflightTable({ messages }: QueueInflightTableProps
), }, - { - id: "context", - header: "Context", - width: "22%", - cellComponent: ({ row }) => { - const context = `${row.realm} / ${row.area} / ${row.resource}`; - - return ( - - {context} - - ); - }, - }, { id: "token", header: "Owner token", - width: "17%", + width: "24%", cellComponent: ({ row }) => ( {row.inflightToken} @@ -45,29 +31,23 @@ export default function QueueInflightTable({ messages }: QueueInflightTableProps { id: "session", header: "Session", - width: "15%", + width: "20%", cellComponent: ({ row }) => ( {row.sessionId} ), }, - { - id: "family", - header: "Family", - width: "7%", - cellComponent: ({ row }) => {row.family}, - }, { id: "attempts", header: "Attempts", - width: "7%", + width: "12%", cellComponent: ({ row }) => {row.attempts}, }, { id: "expires", header: "Expires", - width: "20%", + width: "32%", cellComponent: ({ row }) => ( {formatTimestamp(row.expiresAt)} @@ -75,19 +55,13 @@ export default function QueueInflightTable({ messages }: QueueInflightTableProps ), }, ]; - const tableHeight = Math.min(420, Math.max(144, 44 + messages.length * 48)); - return ( - - aria-label="Inflight queue messages" - class="queue-resource-virtual-table" + + ariaLabel="Inflight queue messages" + class="queue-resource-data-table" columns={columns} getKey={(message) => message.messageId} - headerHeight={44} - overscan={8} - rowHeight={48} rows={messages} - style={{ height: `${tableHeight}px` }} /> ); } diff --git a/ui/src/components/shared/session-table.tsx b/ui/src/components/shared/session-table.tsx index 25c19503..86451bff 100644 --- a/ui/src/components/shared/session-table.tsx +++ b/ui/src/components/shared/session-table.tsx @@ -1,12 +1,4 @@ import { For, Show } from "@askrjs/askr/control"; -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@askrjs/themes/components"; import type { ActiveSession } from "@/features/session/session-models"; import { formatTimestamp } from "@/shared/format"; import { QueryEmptyState } from "./query-state"; @@ -54,195 +46,64 @@ export default function SessionTable({ sessions }: SessionTableProps) { /> ); - const columns: readonly VirtualTableColumn[] = [ - { - id: "session", - header: "Session ID", - width: "15%", - cellComponent: ({ row }) => ( - - {row.sessionId ?? row.key} - - ), - }, - { - id: "route-family", - header: "Route Family", - width: "8%", - cellComponent: ({ row }) => {row.routeFamily ?? "Unknown"}, - }, - { - id: "subject", - header: "Subject", - width: "11%", - cellComponent: ({ row }) => ( - - {reportedText(row.subject)} - - ), - }, - { - id: "identity-claim", - header: "Identity claim", - width: "10%", - cellComponent: ({ row }) => ( - - {reportedText(row.identityClaim)} - - ), - }, - { - id: "identity-value", - header: "Identity value", - width: "11%", - cellComponent: ({ row }) => ( - - {reportedText(row.identityValue)} - - ), - }, - { - id: "transport", - header: "Transport", - width: "7%", - cellComponent: ({ row }) => {row.transport ?? "Unknown"}, - }, - { - id: "remote", - header: "Remote address", - width: "12%", - cellComponent: ({ row }) => ( - - {row.remoteAddress ?? "Unknown"} - - ), - }, - { - id: "connected", - header: "Connected at", - width: "10%", - cellComponent: ({ row }) => ( - - {formatTimestamp(row.connectedAt)} - - ), - }, - { - id: "idle", - header: "Idle", - width: "6%", - cellComponent: ({ row }) => {formatDuration(row.idleSeconds)}, - }, - { - id: "messages", - header: "Messages", - width: "10%", - cellComponent: ({ row }) => {messageCounts(row)}, - }, - ]; - const tableHeight = Math.min(520, Math.max(176, 44 + sessions.length * 48)); + const visibleSessions = sessions.slice(0, 500); return ( 0} fallback={emptyState}> - - - Live sessions - - Each row is one live broker or admin connection and the context reported for it. - - - -

- Scroll the table horizontally to inspect later columns. +

+
+

Live sessions

+

Each item is one live broker or admin connection and the context reported for it.

+
+
    + session.key}> + {(session) => ( +
  • +
    + + {session.sessionId ?? session.key} + + + Route Family {session.routeFamily ?? "Unknown"} + + {session.transport ?? "Unknown"} +
    +

    + {reportedText(session.subject)} + + + {reportedText(session.identityClaim)}: {reportedText(session.identityValue)} + +

    + +
  • + )} +
    +
+ visibleSessions.length}> +

+ Showing the first {visibleSessions.length} of {sessions.length} sessions. Narrow the + active scope to inspect later sessions.

-
-
- - aria-label="Live sessions" - class="session-virtual-table" - columns={columns} - getKey={(session) => session.key} - headerHeight={44} - overscan={10} - rowHeight={48} - rows={sessions} - style={{ height: `${tableHeight}px` }} - /> -
- -
-
    - session.key}> - {(session) => ( -
  • -
    -
    -
    Session ID
    -
    - - {session.sessionId ?? session.key} - -
    -
    - -
    -
    Subject
    -
    {reportedText(session.subject)}
    -
    - -
    -
    Identity claim
    -
    {reportedText(session.identityClaim)}
    -
    - -
    -
    Identity value
    -
    {reportedText(session.identityValue)}
    -
    - -
    -
    Route Family
    -
    {session.routeFamily ?? "Unknown"}
    -
    - -
    -
    Transport
    -
    {session.transport ?? "Unknown"}
    -
    - -
    -
    Remote address
    -
    - {session.remoteAddress ?? "Unknown"} -
    -
    - -
    -
    Connected at
    -
    {formatTimestamp(session.connectedAt)}
    -
    - -
    -
    Idle
    -
    {formatDuration(session.idleSeconds)}
    -
    - -
    -
    Messages
    -
    {messageCounts(session)}
    -
    -
    -
  • - )} -
    -
-
-
- - +
+
); } diff --git a/ui/src/features/diagnostics/diagnostics-console.tsx b/ui/src/features/diagnostics/diagnostics-console.tsx index 9d92b430..d56e527a 100644 --- a/ui/src/features/diagnostics/diagnostics-console.tsx +++ b/ui/src/features/diagnostics/diagnostics-console.tsx @@ -1,7 +1,6 @@ import { For, Show } from "@askrjs/askr/control"; import { Link } from "@askrjs/askr/router"; -import { Button } from "@askrjs/themes/components"; -import { Inline, Stack } from "@askrjs/themes/components"; +import { Button, Block } from "@askrjs/themes/components"; import { Badge, Card, @@ -10,7 +9,7 @@ import { CardHeader, CardTitle, } from "@askrjs/themes/components"; -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import DataTable, { type DataTableColumn } from "@/components/shared/data-table"; import type { DiagnosticHotspot, SuggestedQuery } from "@/adapters"; import { QueryCompactEmptyState, @@ -90,10 +89,6 @@ function fixedRate(value: number) { return `${value.toFixed(2)} / sec`; } -function diagnosticTableHeight(rowCount: number, rowHeight = 48) { - return `${Math.min(620, Math.max(140, 44 + rowCount * rowHeight))}px`; -} - function buildInfrastructureRows( system: SystemOverview, topology?: MessagingTopologyOverview | null, @@ -356,12 +351,16 @@ export default function DiagnosticsConsole({ const hotspots = hotspotsFrom(system, topology); const suggestedQueries = suggestedQueriesFrom(system, topology); const familyRows = metricFamilyRows(metrics); - const infrastructureColumns: readonly VirtualTableColumn[] = [ + const infrastructureColumns: readonly DataTableColumn[] = [ { id: "signal", header: "Signal", width: "22%", - cellComponent: ({ row }) => {row.signal}, + cellComponent: ({ row }) => ( + + {row.signal} + + ), }, { id: "value", @@ -390,7 +389,7 @@ export default function DiagnosticsConsole({ ), }, ]; - const domainColumns: readonly VirtualTableColumn[] = [ + const domainColumns: readonly DataTableColumn[] = [ { id: "domain", header: "Domain", @@ -428,7 +427,7 @@ export default function DiagnosticsConsole({ ), }, ]; - const hotspotColumns: readonly VirtualTableColumn[] = [ + const hotspotColumns: readonly DataTableColumn[] = [ { id: "scope", header: "Route", @@ -442,7 +441,9 @@ export default function DiagnosticsConsole({ {label} ) : ( - {label} + + {label} + ); }, }, @@ -478,7 +479,7 @@ export default function DiagnosticsConsole({ ), }, ]; - const suggestedColumns: readonly VirtualTableColumn[] = [ + const suggestedColumns: readonly DataTableColumn[] = [ { id: "priority", header: "Priority", @@ -521,7 +522,7 @@ export default function DiagnosticsConsole({ ), }, ]; - const metricColumns: readonly VirtualTableColumn[] = [ + const metricColumns: readonly DataTableColumn[] = [ { id: "name", header: "Family", @@ -560,19 +561,19 @@ export default function DiagnosticsConsole({ ]; return ( - + - - + + Diagnostics console Infrastructure internals for {operatorLabel}: topology pressure, suggested follow-up queries, metrics families, and exposed diagnostics. - + {incident.severity} - +
@@ -604,16 +605,12 @@ export default function DiagnosticsConsole({ - - aria-label="Infrastructure diagnostic signals" - class="diagnostics-virtual-table" + + ariaLabel="Infrastructure diagnostic signals" + class="diagnostics-data-table" columns={infrastructureColumns} getKey={(row) => row.signal} - headerHeight={44} - overscan={3} - rowHeight={48} rows={infrastructureRows} - style={{ height: diagnosticTableHeight(infrastructureRows.length) }} /> @@ -627,16 +624,12 @@ export default function DiagnosticsConsole({ - - aria-label="Domain internal diagnostic counters" - class="diagnostics-virtual-table" + + ariaLabel="Domain internal diagnostic counters" + class="diagnostics-data-table" columns={domainColumns} getKey={(row) => row.domain} - headerHeight={44} - overscan={4} - rowHeight={48} rows={domainRows} - style={{ height: diagnosticTableHeight(domainRows.length) }} /> @@ -653,12 +646,12 @@ export default function DiagnosticsConsole({ row.capability}> {(row) => (
- + {row.capability} {row.status} - +

{row.owner}

{row.why}

@@ -689,16 +682,12 @@ export default function DiagnosticsConsole({ - aria-label="Diagnostic hotspots" - class="diagnostics-virtual-table" + + ariaLabel="Diagnostic hotspots" + class="diagnostics-data-table" columns={hotspotColumns} getKey={hotspotKey} - headerHeight={44} - overscan={4} - rowHeight={48} rows={hotspots} - style={{ height: diagnosticTableHeight(hotspots.length) }} /> } > @@ -723,16 +712,12 @@ export default function DiagnosticsConsole({ - aria-label="Suggested diagnostic queries" - class="diagnostics-virtual-table" + + ariaLabel="Suggested diagnostic queries" + class="diagnostics-data-table" columns={suggestedColumns} getKey={suggestedQueryKey} - headerHeight={44} - overscan={4} - rowHeight={48} rows={suggestedQueries} - style={{ height: diagnosticTableHeight(suggestedQueries.length) }} /> } > @@ -746,18 +731,18 @@ export default function DiagnosticsConsole({ - - + + Metric families Structured metric families for storage-adjacent, routing, pressure, and failure diagnostics. - + - + @@ -774,16 +759,12 @@ export default function DiagnosticsConsole({ - aria-label="Diagnostic metric families" - class="diagnostics-virtual-table" + + ariaLabel="Diagnostic metric families" + class="diagnostics-data-table" columns={metricColumns} getKey={(row) => row.name} - headerHeight={44} - overscan={8} - rowHeight={48} rows={familyRows} - style={{ height: diagnosticTableHeight(familyRows.length) }} /> } > @@ -795,6 +776,6 @@ export default function DiagnosticsConsole({ - + ); } diff --git a/ui/src/features/lease/lease-resource-page.tsx b/ui/src/features/lease/lease-resource-page.tsx index 0b9688c5..f87e8682 100644 --- a/ui/src/features/lease/lease-resource-page.tsx +++ b/ui/src/features/lease/lease-resource-page.tsx @@ -2,15 +2,8 @@ import { currentRoute } from "@askrjs/askr/router"; import { state } from "@askrjs/askr"; import { For, Show } from "@askrjs/askr/control"; import { task } from "@askrjs/askr/resources"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Stack, -} from "@askrjs/themes/components"; +import { Block, Card, CardContent, CardTitle } from "@askrjs/themes/components"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; @@ -61,61 +54,64 @@ function formatOwner(row: LeaseOwnershipSearchRow) { return row.ownerSessionId ?? row.ownerId ?? "--"; } -function LeaseResourceRowsTable(props: { rows: LeaseOwnershipSearchRow[]; now: () => number }) { +function LeaseOwnershipCards(props: { rows: LeaseOwnershipSearchRow[]; now: () => number }) { const totalWaiters = props.rows.reduce((sum, row) => sum + row.pendingWaiters, 0); return ( - - - Lease ownership rows - - Owner/session, queued token, waiters, age, remaining TTL, and expiry for this scope. - {` ${props.rows.length} row${props.rows.length === 1 ? "" : "s"}, ${totalWaiters} waiter${ - totalWaiters === 1 ? "" : "s" - }.`} - - - -

Scroll the table horizontally on narrow screens.

-
- - - - Owner / session - State - Queued token - Waiters - Age - Remaining TTL - Expiry - - - - - `${row.ownerSessionId}-${row.ownerId ?? "none"}-${row.queuedToken ?? "none"}-${row.area}-${row.realm}-${row.resource}-${row.state}` - } - > - {(row) => ( - - {formatOwner(row)} - {row.state} - {row.queuedToken ?? "--"} - {formatNumber(row.pendingWaiters)} - - {row.ageSeconds === null ? "--" : formatDurationSeconds(row.ageSeconds)} - - {() => formatRemaining(row.expiresAt, props.now())} - {row.expiresAt ? formatTimestamp(row.expiresAt) : "--"} - - )} - - -
-
-
-
+ + + + `${row.ownerSessionId}-${row.ownerId ?? "none"}-${row.queuedToken ?? "none"}-${row.area}-${row.realm}-${row.resource}-${row.state}` + } + > + {(row) => ( + + + + {formatOwner(row)} +
+
+
State
+
{row.state}
+
+
+
Queued token
+
{row.queuedToken ?? "--"}
+
+
+
Waiters
+
{formatNumber(row.pendingWaiters)}
+
+
+
Age
+
+ {row.ageSeconds === null ? "--" : formatDurationSeconds(row.ageSeconds)} +
+
+
+
Remaining TTL
+
+ {() => formatRemaining(row.expiresAt, props.now())} +
+
+
+
Expiry
+
{row.expiresAt ? formatTimestamp(row.expiresAt) : "--"}
+
+
+
+
+
+ )} +
+
+
); } @@ -156,7 +152,7 @@ export default function LeaseResourcePage() { return ( - + 0}> - + @@ -203,7 +199,7 @@ export default function LeaseResourcePage() { - + ); diff --git a/ui/src/features/metrics/metrics-page.tsx b/ui/src/features/metrics/metrics-page.tsx index b4bb1e31..f939e310 100644 --- a/ui/src/features/metrics/metrics-page.tsx +++ b/ui/src/features/metrics/metrics-page.tsx @@ -1,7 +1,7 @@ import { For, Show } from "@askrjs/askr/control"; import { currentRoute, updateRouteQuery } from "@askrjs/askr/router"; -import { Input, VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; -import { Button, Stack, Text } from "@askrjs/themes/components"; +import { Input } from "@askrjs/ui"; +import { Button, Text, Block } from "@askrjs/themes/components"; import { Card, CardContent, @@ -13,6 +13,7 @@ import { CollapsibleTrigger, } from "@askrjs/themes/components"; import DomainHeader from "@/components/shared/domain-header"; +import DataTable, { type DataTableColumn } from "@/components/shared/data-table"; import DomainMetricTable from "@/components/shared/domain-metric-table"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import { @@ -511,7 +512,7 @@ export default function MetricsPage() { 0, ); const shortcutCards = data ? buildSummaryShortcuts(data.families) : []; - const sampleColumns: readonly VirtualTableColumn[] = [ + const sampleColumns: readonly DataTableColumn[] = [ { id: "metric", header: "Metric", @@ -557,8 +558,6 @@ export default function MetricsPage() { ), }, ]; - const sampleTableHeight = Math.min(560, Math.max(176, 44 + sampleRows.length * 48)); - const detailSummary = data ? filterValue.length === 0 ? `${formatNumber(data.families.length)} families / ${formatNumber(sampleCount)} samples in the current snapshot.` @@ -590,7 +589,7 @@ export default function MetricsPage() { return ( - + {(data) => ( - +
@@ -730,16 +729,12 @@ export default function MetricsPage() { } /> ) : ( - - aria-label="Metric samples" - class="metrics-sample-virtual-table" + + ariaLabel="Metric samples" + class="metrics-sample-data-table" columns={sampleColumns} getKey={(row) => `${row.family}:${row.labels}:${row.value}`} - headerHeight={44} - overscan={12} - rowHeight={48} rows={sampleRows} - style={{ height: `${sampleTableHeight}px` }} /> )} @@ -763,10 +758,10 @@ export default function MetricsPage() {
-
+
)}
- + ); } diff --git a/ui/src/features/queue/queue-dead-letter-dialog.tsx b/ui/src/features/queue/queue-dead-letter-dialog.tsx index 7af96ffd..6cc3574b 100644 --- a/ui/src/features/queue/queue-dead-letter-dialog.tsx +++ b/ui/src/features/queue/queue-dead-letter-dialog.tsx @@ -1,13 +1,13 @@ import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogOverlay, - DialogPortal, - DialogTitle, + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, } from "@askrjs/ui"; -import { Alert, Button, Inline } from "@askrjs/themes/components"; +import { Alert, Button, Block } from "@askrjs/themes/components"; import type { DeadLetterMessage } from "@/features/queue/queue-models"; import { formatUnknownError } from "@/shared/errors/format"; @@ -55,14 +55,14 @@ export default function QueueDeadLetterDialog({ : null; return ( - - - + + + {confirmationMessage && copy ? ( - - {copy.title} + + {copy.title} - {copy.description} + {copy.description} {actionError ? ( ) : null} - - + + - + - - + + ) : null} - - + + ); } diff --git a/ui/src/features/queue/queue-resource-page.tsx b/ui/src/features/queue/queue-resource-page.tsx index aece03cf..1844db97 100644 --- a/ui/src/features/queue/queue-resource-page.tsx +++ b/ui/src/features/queue/queue-resource-page.tsx @@ -1,7 +1,7 @@ import { state } from "@askrjs/askr"; +import { Block } from "@askrjs/themes/components"; import { Show } from "@askrjs/askr/control"; import { currentRoute } from "@askrjs/askr/router"; -import { Stack } from "@askrjs/themes/components"; import DomainHeader from "@/components/shared/domain-header"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; @@ -89,10 +89,10 @@ export default function QueueResourcePage() { return ( - + resourceQuery.refresh(), }} status={headerStatus} @@ -147,7 +147,7 @@ export default function QueueResourcePage() { {(data) => ( - + void runDeadLetterAction(kind, message)} scopeLabel={scopeLabel} /> - + )}
- + ); } diff --git a/ui/src/features/queue/queue-resource-panels.tsx b/ui/src/features/queue/queue-resource-panels.tsx index c67dcf82..045b0371 100644 --- a/ui/src/features/queue/queue-resource-panels.tsx +++ b/ui/src/features/queue/queue-resource-panels.tsx @@ -1,17 +1,18 @@ import { For, Show } from "@askrjs/askr/control"; -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; import { Badge, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Inline, - Stack, + Block, + Item, + ItemActions, + ItemContent, + ItemFooter, + ItemGroup, + ItemTitle, + Text, } from "@askrjs/themes/components"; -import DomainMetricTable from "@/components/shared/domain-metric-table"; -import { QueryEmptyState } from "@/components/shared/query-state"; +import DomainDataSection from "@/components/shared/domain-data-section"; +import DomainSummaryStrip from "@/components/shared/domain-summary-strip"; +import { QueryCompactEmptyState } from "@/components/shared/query-state"; import QueueDeadLetterTable from "@/components/shared/queue-dead-letter-table"; import QueueInflightTable from "@/components/shared/queue-inflight-table"; import type { DeadLetterMessage } from "@/features/queue/queue-models"; @@ -21,6 +22,7 @@ import type { QueueResourceTimeline, QueueResourceTimelineEvent, } from "@/features/queue/queue-resource-models"; +import { formatTimestamp } from "@/shared/format"; import { formatRate, formatTimelineContext, @@ -30,10 +32,11 @@ import { export function QueueResourceCurrentValuesPanel({ detail }: { detail: QueueResourceDetail }) { return ( - - - - - Inflight - Live reservations currently owned by queue sessions. - - - {messages.length} {messages.length === 1 ? "entry" : "entries"} - - - - - - {messages.length === 0 ? ( - - ) : ( - -

- Scroll the table horizontally to inspect ownership and expiry details. -

- -
- )} -
- + + {messages.length} {messages.length === 1 ? "entry" : "entries"} + + } + > + {messages.length === 0 ? ( + + ) : ( + + + Scroll the table horizontally to inspect ownership and expiry details. + + + + )} + ); } @@ -107,145 +108,130 @@ export function QueueResourceDeadLettersPanel({ pendingMessageId: number | null; }) { return ( - - - - - Dead letters - - Durable messages returned by the current dead-letter inspection. This list and the - resource-summary counter can have different snapshot times. - - - 0 ? "warning" : "success"}> - {messages.length} {messages.length === 1 ? "message" : "messages"} returned - - - - - - {messages.length === 0 ? ( - - ) : ( - -

Scroll the table horizontally to reach all actions.

- -
- )} -
-
+ 0 ? "warning" : "success"}> + {messages.length} {messages.length === 1 ? "message" : "messages"} returned + + } + > + {messages.length === 0 ? ( + + ) : ( + + + Scroll the table horizontally to reach all actions. + + + + )} + ); } -const timelineColumns: readonly VirtualTableColumn[] = [ - { - id: "kind", - header: "Kind", - width: "14%", - cellComponent: ({ row }) => ( - - {formatTimelineKind(row.kind)} - - ), - }, - { - id: "summary", - header: "Summary", - width: "30%", - cellComponent: ({ row }) => ( - - {row.summary} - - ), - }, - { - id: "observed", - header: "Observed", - width: "18%", - cellComponent: ({ row }) => ( - - {row.observedAt} - - ), - }, - { - id: "age", - header: "Age", - width: "12%", - cellComponent: ({ row }) => ( - {row.ageSeconds == null ? "Unknown" : humanizeSeconds(row.ageSeconds)} - ), - }, - { - id: "context", - header: "Context", - width: "26%", - cellComponent: ({ row }) => { - const timelineContext = formatTimelineContext(row); +function timelineKindVariant(kind: QueueResourceTimelineEvent["kind"]) { + if (kind === "failure") return "danger" as const; + if (kind === "retry") return "warning" as const; + return "outline" as const; +} - return ( -
- 0}> - line}> - {(line) => {line}} - - - - Context unavailable - -
- ); - }, - }, -]; +function QueueTimelineItem({ event }: { event: QueueResourceTimelineEvent }) { + const context = formatTimelineContext(event); -export function QueueResourceTimelinePanel({ timeline }: { timeline: QueueResourceTimeline }) { return ( - - - - - Timeline - - {timeline.derived - ? "Derived transition evidence built from surrounding queue state." - : "Broker-observed queue transitions for this resource."} - - - - {timeline.derived ? "Derived" : "Live"} - - - + + + + + + {formatTimelineKind(event.kind)} + + + {event.summary} + + + + + + + + + + + + {event.ageSeconds == null ? "Age unknown" : `${humanizeSeconds(event.ageSeconds)} ago`} + + + + + ); +} - - {timeline.events.length === 0 ? ( - + {timeline.derived ? "Derived" : "Live"} + + } + > + 0} + fallback={ + - ) : ( - - aria-label="Queue resource timeline" - class="queue-resource-virtual-table" - columns={timelineColumns} - getKey={(event) => `${event.observedAt}:${event.summary}`} - headerHeight={44} - overscan={8} - rowHeight={56} - rows={timeline.events} - style={{ - height: `${Math.min(456, Math.max(156, 44 + timeline.events.length * 56))}px`, - }} - /> - )} - - + } + > + + + `${event.observedAt}:${event.kind}:${event.messageId ?? "none"}:${event.summary}` + } + > + {(event) => } + + + + ); } diff --git a/ui/src/features/search/search-results-panel.tsx b/ui/src/features/search/search-results-panel.tsx index d9db1115..b0bf161e 100644 --- a/ui/src/features/search/search-results-panel.tsx +++ b/ui/src/features/search/search-results-panel.tsx @@ -1,7 +1,6 @@ import { For, Show } from "@askrjs/askr/control"; import { Link } from "@askrjs/askr/router"; -import { Button } from "@askrjs/themes/components"; -import { Inline, Stack } from "@askrjs/themes/components"; +import { Button, Block } from "@askrjs/themes/components"; import { Badge, Card, @@ -10,7 +9,7 @@ import { CardHeader, CardTitle, } from "@askrjs/themes/components"; -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import DataTable, { type DataTableColumn } from "@/components/shared/data-table"; import { QueryEmptyState, QueryErrorState, @@ -55,19 +54,21 @@ function resultHref(result: AdminSearchResult, selectedRouteFamilyId: string) { function searchColumns( selectedRouteFamilyId: string, -): readonly VirtualTableColumn[] { +): readonly DataTableColumn[] { return [ { id: "result", header: "Result", width: "28%", cellComponent: ({ row }) => ( - - {row.title} + + + {row.title} + {titleCase(row.domain)} · {titleCase(row.kind)} - + ), }, { @@ -84,7 +85,11 @@ function searchColumns( id: "summary", header: "Summary", width: "28%", - cellComponent: ({ row }) => {row.summary}, + cellComponent: ({ row }) => ( + + {row.summary} + + ), }, { id: "health", @@ -125,20 +130,20 @@ export default function SearchResultsPanel({ return ( - - + + Search results {description} - - + + Truncated - - + + @@ -154,16 +159,12 @@ export default function SearchResultsPanel({ /> 0}> - - - aria-label="Admin search results" + + + ariaLabel="Admin search results" columns={searchColumns(routeFamilyId)} rows={results} getKey={(row) => row.id} - headerHeight={44} - overscan={6} - rowHeight={56} - style={{ height: "360px" }} />
result.id}> @@ -175,7 +176,7 @@ export default function SearchResultsPanel({ )}
-
+
diff --git a/ui/src/pages/app/_layout.tsx b/ui/src/pages/app/_layout.tsx index 28abb4c5..0a858ce0 100644 --- a/ui/src/pages/app/_layout.tsx +++ b/ui/src/pages/app/_layout.tsx @@ -316,7 +316,7 @@ export default function Layout({ children }: { children?: unknown }) { return ( - + @@ -364,7 +364,7 @@ export default function Layout({ children }: { children?: unknown }) { - + {hasRouteFamilyScope ? ( {children} ) : routeFamilyNotFound ? ( diff --git a/ui/src/pages/app/diagnostics.tsx b/ui/src/pages/app/diagnostics.tsx index 00f6b247..e4ca1d1c 100644 --- a/ui/src/pages/app/diagnostics.tsx +++ b/ui/src/pages/app/diagnostics.tsx @@ -2,7 +2,7 @@ import { state } from "@askrjs/askr"; import { Show } from "@askrjs/askr/control"; import { currentRoute, onRouteChange, updateRouteQuery } from "@askrjs/askr/router"; import { Input, Label } from "@askrjs/ui"; -import { Button, Inline, Stack } from "@askrjs/themes/components"; +import { Button, Block } from "@askrjs/themes/components"; import DomainHeader from "@/components/shared/domain-header"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import { QueryErrorState, QueryLoadingState } from "@/components/shared/query-state"; @@ -94,7 +94,7 @@ export default function DiagnosticsPage() { return ( - +
- +
- + @@ -188,7 +188,7 @@ export default function DiagnosticsPage() { {(data) => ( - + - + )} - + ); } diff --git a/ui/src/pages/app/home.tsx b/ui/src/pages/app/home.tsx index c3a65baf..704c0cfe 100644 --- a/ui/src/pages/app/home.tsx +++ b/ui/src/pages/app/home.tsx @@ -1,8 +1,7 @@ import { For, Show } from "@askrjs/askr/control"; import { Link } from "@askrjs/askr/router"; import { ArrowUpRightIcon, CheckCircle2Icon, CircleAlertIcon } from "@askrjs/lucide"; -import { Stack } from "@askrjs/themes/components"; -import { Alert, Badge } from "@askrjs/themes/components"; +import { Alert, Badge, Block } from "@askrjs/themes/components"; import DomainHeader from "@/components/shared/domain-header"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; @@ -188,7 +187,7 @@ export default function Home() { session.refresh()} /> - + - + ); diff --git a/ui/src/pages/app/kv-resource.tsx b/ui/src/pages/app/kv-resource.tsx index 1aee9adf..66b186d0 100644 --- a/ui/src/pages/app/kv-resource.tsx +++ b/ui/src/pages/app/kv-resource.tsx @@ -1,20 +1,22 @@ import { state } from "@askrjs/askr"; import { Show } from "@askrjs/askr/control"; import { currentRoute, Link, navigate } from "@askrjs/askr/router"; -import { Form, Input, Label, VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import { Form, Input, Label } from "@askrjs/ui"; import { Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Inline, - Stack, + Block, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemGroup, + ItemTitle, + Text, } from "@askrjs/themes/components"; import DomainHeader from "@/components/shared/domain-header"; import CopyTextButton from "@/components/shared/copy-text-button"; -import DomainMetricTable from "@/components/shared/domain-metric-table"; +import DataTable, { type DataTableColumn } from "@/components/shared/data-table"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; @@ -130,16 +132,7 @@ export default function KvResourcePage() { ); const valueQuery = concreteFamily !== null && lookup ? valueQueryCell : null; const valueResult = valueQuery?.data; - const exactLookupMetrics = - valueResult?.found && valueResult.value - ? [ - { label: "Key", value: bytePreview(valueResult.key) }, - { label: "Key bytes", value: valueResult.key.lenBytes }, - { label: "Value", value: bytePreview(valueResult.value) }, - { label: "Value bytes", value: valueResult.value.lenBytes }, - ] - : []; - const rowColumns: readonly VirtualTableColumn[] = [ + const rowColumns: readonly DataTableColumn[] = [ { id: "key-bytes", header: "Key bytes", @@ -165,7 +158,7 @@ export default function KvResourcePage() { { id: "value-preview", header: "Value preview", - width: "28%", + width: "36%", cellComponent: ({ row }) => ( {bytePreview(row.value)} ({bytePreviewKind(row.value)}) @@ -174,13 +167,10 @@ export default function KvResourcePage() { }, { id: "actions", - header: "Copy", - width: "22%", + header: "Action", + width: "14%", cellComponent: ({ row }) => ( - - - - + ), }, ]; @@ -223,7 +213,7 @@ export default function KvResourcePage() { return ( - + - - - Exact key lookup - - Read the current committed value for one UTF-8 or base64-encoded key. - - - + +
- - - + + + - +
- - + + - - + + @@ -313,34 +306,70 @@ export default function KvResourcePage() { /> - - - - - - - + + + + + Key + + + {valueResult ? bytePreview(valueResult.key) : ""} + + + + {valueResult ? formatNumber(valueResult.key.lenBytes) : "0"} bytes ·{" "} + {valueResult ? bytePreviewKind(valueResult.key) : "utf8"} + + + + + + + + + Value + + + {valueResult?.value ? bytePreview(valueResult.value) : ""} + + + + {valueResult?.value ? formatNumber(valueResult.value.lenBytes) : "0"} bytes ·{" "} + {valueResult?.value ? bytePreviewKind(valueResult.value) : "utf8"} + + + + + + + + - - - Row filters - Filter committed rows by key prefix and page size. - - + +
- - + + - - + + - + - +
-
-
+ + @@ -393,30 +422,27 @@ export default function KvResourcePage() { 0}> - - - Current authoritative KV rows - - Committed rows returned by the selected scope and filters. - - - - - aria-label="Committed KV rows" - class="domain-resource-virtual-table" + + +

+ Scroll horizontally to inspect every row field and action. +

+ + ariaLabel="Committed KV rows" + class="domain-resource-data-table" columns={rowColumns} getKey={(row) => row.key.base64} - headerHeight={44} - overscan={6} - rowHeight={52} rows={rows} - style={{ height: `${Math.min(420, Math.max(144, 44 + rows.length * 52))}px` }} /> -
-
+ +
- + First page @@ -447,8 +473,8 @@ export default function KvResourcePage() { - - + + ); } diff --git a/ui/src/pages/app/kv.tsx b/ui/src/pages/app/kv.tsx index 0485879a..fca73070 100644 --- a/ui/src/pages/app/kv.tsx +++ b/ui/src/pages/app/kv.tsx @@ -82,7 +82,7 @@ export default function KvPage() { { id: "read-latency", header: "Read p95 ms", - width: "9%", + width: "12%", cell: (row) => formatLatency(row.readLatencyP95Ms), sortValue: (row) => row.readLatencyP95Ms, available: inventoryRows.some((row) => row.readLatencyP95Ms !== undefined), @@ -90,7 +90,7 @@ export default function KvPage() { { id: "write-latency", header: "Write p95 ms", - width: "9%", + width: "12%", cell: (row) => formatLatency(row.writeLatencyP95Ms), sortValue: (row) => row.writeLatencyP95Ms, available: inventoryRows.some((row) => row.writeLatencyP95Ms !== undefined), diff --git a/ui/src/pages/app/notice-operation.tsx b/ui/src/pages/app/notice-operation.tsx index 83b404c9..d5cf0b56 100644 --- a/ui/src/pages/app/notice-operation.tsx +++ b/ui/src/pages/app/notice-operation.tsx @@ -1,20 +1,23 @@ import { For, Show } from "@askrjs/askr/control"; import { currentRoute } from "@askrjs/askr/router"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Stack, + Badge, + Block, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemGroup, + ItemTitle, + Text, } from "@askrjs/themes/components"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; import { - QueryEmptyState, + QueryCompactEmptyState, QueryErrorState, QueryLoadingState, QueryRefreshingState, @@ -48,24 +51,55 @@ function parseLimit(value: string | null) { return Math.max(1, Math.min(200, Math.floor(parsed))); } -function NoticeDeliveryTableRows(props: { rows: NoticeDeliveryRows["observations"] }) { +function NoticeDeliveryList(props: { rows: NoticeDeliveryRows["observations"] }) { return ( - - `${observation.sessionId ?? "session"}:${observation.subscriptionId ?? "none"}` - } + - {(observation) => ( - - {observation.status} - {observation.sessionId ?? "--"} - {formatNumber(observation.notificationsReceived)} - {formatNumber(observation.publishesPerMinute)} - {formatNumber(observation.publishesTotal)} - - )} - + + `${observation.sessionId ?? "session"}:${observation.subscriptionId ?? "none"}` + } + > + {(observation) => ( + + + + + {observation.sessionId ?? "--"} + + + + + {observation.subscriptionId == null ? null : ( + + Subscription: {formatNumber(observation.subscriptionId)} + + )} + + Notifications observed: {formatNumber(observation.notificationsReceived)} + + + Current publishes / min: {formatNumber(observation.publishesPerMinute)} + + + Observed publish total: {formatNumber(observation.publishesTotal)} + + + + + + + {observation.status} + + + + )} + + ); } @@ -95,7 +129,7 @@ export default function NoticeOperationPage(props: { const activeSubscribers = countActiveSubscribers(deliveries); return ( - + {(data) => ( - + - - - Delivery evidence - - Live subscription observations for this operation route; not delivery - history. The API does not report a reset scope for total counters. - - - -
- - - - Status - Session - Notifications observed - Current publishes / min - Observed publish total - - - - - -
-
-
- - } + - -
-
+ } + > + + + +
)} -
+
); } diff --git a/ui/src/pages/app/notice.tsx b/ui/src/pages/app/notice.tsx index e10d7bcb..926007fe 100644 --- a/ui/src/pages/app/notice.tsx +++ b/ui/src/pages/app/notice.tsx @@ -1,14 +1,7 @@ import { For, Show } from "@askrjs/askr/control"; import { currentRoute, Link } from "@askrjs/askr/router"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Stack, -} from "@askrjs/themes/components"; +import { Block, Item, ItemContent, ItemGroup, ItemTitle } from "@askrjs/themes/components"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; import DomainInventoryPage from "@/components/shared/domain-inventory-page"; import type { DomainResourceMetricColumn } from "@/components/shared/domain-resource-inventory-table"; @@ -16,7 +9,7 @@ import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; import { - QueryEmptyState, + QueryCompactEmptyState, QueryErrorState, QueryLoadingState, QueryRefreshingState, @@ -156,41 +149,57 @@ function NoticeLandingPage() { ); } -function NoticeOperationTableRows(props: { data: NoticeResourceOperationRows }) { +function NoticeOperationList(props: { data: NoticeResourceOperationRows }) { return ( - row.operation}> - {(row) => { - const route = row.operation.startsWith("notice://") - ? row.operation - : formatFitzRoute("notice", { - area: props.data.area, - operation: row.operation, - realm: props.data.realm, - resource: props.data.resource, - }); + + row.operation}> + {(row) => { + const route = row.operation.startsWith("notice://") + ? row.operation + : formatFitzRoute("notice", { + area: props.data.area, + operation: row.operation, + realm: props.data.realm, + resource: props.data.resource, + }); - return ( - - - - {route} - - - {formatNumber(row.activeSubscribers)} - {formatNumber(row.rollingMessageCount)} - - ); - }} - + return ( + + + + + {route} + + +
+
+
Active subscribers
+
{formatNumber(row.activeSubscribers)}
+
+
+
Publishes / min
+
{formatNumber(row.rollingMessageCount)}
+
+
+
+
+ ); + }} +
+ ); } @@ -216,10 +225,10 @@ function NoticeResourcePage(props: { realm: string; area: string; resource: stri return ( - + - + - - - Notice operations - - Live operation routes with active subscribers and route publish rates. - - - -
- - - - Route - Active subscribers - Publishes / min - - - - - -
-
-
- - ) : null - } + - -
-
+ : null} + > + + + +
-
+
); } diff --git a/ui/src/pages/app/route-family.tsx b/ui/src/pages/app/route-family.tsx index bf1c187d..d4d1a3a4 100644 --- a/ui/src/pages/app/route-family.tsx +++ b/ui/src/pages/app/route-family.tsx @@ -2,7 +2,7 @@ import { For } from "@askrjs/askr/control"; import { task } from "@askrjs/askr/resources"; import { Link } from "@askrjs/askr/router"; import { NetworkIcon } from "@askrjs/lucide"; -import { Button, Main, PageHeader, Stack } from "@askrjs/themes/components"; +import { Button, Main, PageHeader, Block } from "@askrjs/themes/components"; import { pathWithRouteFamily } from "@/shared/navigation/domains"; import { manageRoutePageContext } from "@/components/shared/domain-page-frame"; import { @@ -54,18 +54,20 @@ export function RouteFamilyNotFoundPage() {
- + - +
); } @@ -79,13 +81,15 @@ export default function RouteFamilySelectorPage() {
- + {operator.routeFamilyState === "loading" ? ( @@ -107,7 +111,7 @@ export default function RouteFamilySelectorPage() { {operator.routeFamilyState === "ready" ? ( ) : null} - +
); } diff --git a/ui/src/pages/app/rpc-operation.tsx b/ui/src/pages/app/rpc-operation.tsx index c5cdc734..9d748056 100644 --- a/ui/src/pages/app/rpc-operation.tsx +++ b/ui/src/pages/app/rpc-operation.tsx @@ -1,21 +1,24 @@ import { For, Show } from "@askrjs/askr/control"; import { currentRoute } from "@askrjs/askr/router"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Stack, + Badge, + Block, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemGroup, + ItemTitle, + Text, } from "@askrjs/themes/components"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; -import DomainMetricTable from "@/components/shared/domain-metric-table"; import DomainPageFrame from "@/components/shared/domain-page-frame"; +import DomainSummaryStrip from "@/components/shared/domain-summary-strip"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; import { - QueryEmptyState, + QueryCompactEmptyState, QueryErrorState, QueryLoadingState, QueryRefreshingState, @@ -51,22 +54,44 @@ function formatObservationState(value: string) { .join(" "); } -function RpcCallEvidenceRows(props: { rows: RpcCallObservation[] }) { +function RpcCallEvidenceList(props: { rows: RpcCallObservation[] }) { return ( - row.correlation_id ?? `${row.worker_session_id}:${index}`} - > - {(row) => ( - - {formatObservationState(row.state)} - {row.worker_session_id ?? "--"} - {row.correlation_id ?? "--"} - {formatNumber(row.requests_handled ?? 0)} - {formatLatency(row.average_latency_ms)} - - )} - + + row.correlation_id ?? `${row.worker_session_id}:${index}`} + > + {(row) => ( + + + + + {row.correlation_id ?? row.worker_session_id ?? "--"} + + + + + + Worker: {row.worker_session_id ?? "--"} + + + Observed handled total: {formatNumber(row.requests_handled ?? 0)} + + + Latency: {formatLatency(row.average_latency_ms)} + + + + + + + {formatObservationState(row.state)} + + + + )} + + ); } @@ -84,7 +109,7 @@ export default function RpcOperationPage() { return ( - + {(detail) => ( - + - - - - Live call evidence - - Broker-local worker registrations, pending calls, and correlation rows. - - - - - - - - State - Worker - Correlation - Observed handled total - Latency - - - - - -
-
- } - > - - -
-
-
+ + }> + + + + )} - +
); } diff --git a/ui/src/pages/app/rpc-resource.tsx b/ui/src/pages/app/rpc-resource.tsx index 34c02085..53698d2f 100644 --- a/ui/src/pages/app/rpc-resource.tsx +++ b/ui/src/pages/app/rpc-resource.tsx @@ -1,20 +1,13 @@ -import { Show } from "@askrjs/askr/control"; +import { For, Show } from "@askrjs/askr/control"; import { currentRoute, Link } from "@askrjs/askr/router"; -import { VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Stack, -} from "@askrjs/themes/components"; +import { Block, Item, ItemContent, ItemGroup, ItemTitle } from "@askrjs/themes/components"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; import { - QueryEmptyState, + QueryCompactEmptyState, QueryErrorState, QueryLoadingState, QueryRefreshingState, @@ -24,8 +17,6 @@ import type { RpcResourceOperationRows } from "@/features/rpc/rpc-models"; import { formatCount, formatNumber } from "@/shared/format"; import { domainScopeHref, formatFitzRoute } from "@/shared/navigation/domains"; -type RpcResourceOperationRow = RpcResourceOperationRows["operations"][number]; - function decodeParam(value: string | undefined) { if (!value) return ""; @@ -40,67 +31,60 @@ function formatLatency(value: number | null) { return value == null ? "--" : `${formatNumber(value)} ms`; } -function rpcOperationColumns( - data: RpcResourceOperationRows, -): readonly VirtualTableColumn[] { - return [ - { - id: "route", - header: "Route", - width: "44%", - cellComponent: ({ row }) => { - const route = formatFitzRoute("rpc", { - area: data.area, - operation: row.operation, - realm: data.realm, - resource: data.resource, - }); - - return ( - - {route} - - ); - }, - }, - { - id: "workers", - header: "Workers", - width: "12%", - cellComponent: ({ row }) => {formatNumber(row.workers)}, - }, - { - id: "pending", - header: "Pending requests", - width: "16%", - cellComponent: ({ row }) => {formatNumber(row.pendingRequests)}, - }, - { - id: "handled", - header: "Requests handled", - width: "16%", - cellComponent: ({ row }) => {formatNumber(row.requestsHandled)}, - }, - { - id: "latency", - header: "Latency", - width: "12%", - cellComponent: ({ row }) => {formatLatency(row.averageLatencyMs)}, - }, - ]; -} +function RpcOperationList(props: { data: RpcResourceOperationRows }) { + return ( + + row.operation}> + {(row) => { + const route = formatFitzRoute("rpc", { + area: props.data.area, + operation: row.operation, + realm: props.data.realm, + resource: props.data.resource, + }); -function rpcOperationTableHeight(rowCount: number) { - return `${Math.min(240, Math.max(144, 44 + rowCount * 48))}px`; + return ( + + + + + {route} + + +
+
+
Workers
+
{formatNumber(row.workers)}
+
+
+
Pending requests
+
{formatNumber(row.pendingRequests)}
+
+
+
Requests handled
+
{formatNumber(row.requestsHandled)}
+
+
+
Average latency
+
{formatLatency(row.averageLatencyMs)}
+
+
+
+
+ ); + }} +
+
+ ); } export default function RpcResourcePage() { @@ -115,7 +99,7 @@ export default function RpcResourcePage() { return ( - + - + - - - RPC operations - - Live operation evidence: workers, handled calls, latency, and in-memory - pending request evidence. - - - - - aria-label="RPC operations" - class="rpc-operation-virtual-table" - columns={rpcOperationColumns(data)} - getKey={(row) => row.operation} - headerHeight={44} - overscan={4} - rowHeight={48} - rows={data.operations} - style={{ height: rpcOperationTableHeight(data.operations.length) }} - /> - - - ) : null - } + - - - + : null} + > + + + + - + ); } diff --git a/ui/src/pages/app/schedule-resource.tsx b/ui/src/pages/app/schedule-resource.tsx index eb5225cb..0c632956 100644 --- a/ui/src/pages/app/schedule-resource.tsx +++ b/ui/src/pages/app/schedule-resource.tsx @@ -1,22 +1,26 @@ import { For, Show } from "@askrjs/askr/control"; import { currentRoute } from "@askrjs/askr/router"; -import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@askrjs/ui"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Stack, + Badge, + Block, + Item, + ItemActions, + ItemContent, + ItemDescription, + ItemFooter, + ItemGroup, + ItemTitle, + Text, } from "@askrjs/themes/components"; import type { ScheduleExecutionObservation, ScheduleMissedObservation } from "@/adapters"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; import DomainMetricTable from "@/components/shared/domain-metric-table"; import DomainPageFrame from "@/components/shared/domain-page-frame"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; import { - QueryEmptyState, + QueryCompactEmptyState, QueryErrorState, QueryLoadingState, QueryRefreshingState, @@ -24,8 +28,8 @@ import { import { createScheduleResourceQuery } from "@/features/schedule/schedule-query"; import type { ScheduleResourceView } from "@/features/schedule/schedule-models"; import { - formatDurationSeconds, formatCount, + formatDurationSeconds, formatNumber, formatRelativeTime, formatTimestamp, @@ -46,6 +50,15 @@ function formatMaybeTimestamp(value?: string | null) { return value ? formatTimestamp(value) : "--"; } +function formatObservationStatus(value: string) { + const label = value.replace(/_/g, " ").trim(); + return label.length > 0 ? `${label.charAt(0).toUpperCase()}${label.slice(1)}` : "Unknown"; +} + +function formatDeliveryMode(value?: string | null) { + return value ? `${formatObservationStatus(value)} delivery` : "Delivery mode unknown"; +} + export function formatScheduleTiming(value?: string | null, reference = Date.now()) { if (!value) return "No next run scheduled"; @@ -103,64 +116,67 @@ function scheduleTimingMetric(value?: string | null) { }; } -function ScheduleCard(props: { children: unknown; description?: string; title: string }) { - return ( - - - {props.title} - {props.description ? {props.description} : null} - - {props.children} - - ); -} - function ExecutionRows(props: { rows: ScheduleExecutionObservation[] }) { return ( 0} fallback={ - + } > -
-

Scroll the table horizontally on narrow screens.

-
- - - - Route - Status - Mode - Scheduled time - Last handoff - Handoff count - - - - `${row.operation}:${row.next_run}`}> - {(row) => ( - - - - {formatFitzRoute("schedule", row)} - - - {row.status} - {row.delivery_mode} - {formatMaybeTimestamp(row.next_run)} - {formatMaybeTimestamp(row.last_run)} - {formatNumber(row.executions_total)} - - )} - - -
-
-
+ + `${row.route_family}:${row.operation}:${row.next_run}`}> + {(row) => ( + + + + + {formatFitzRoute("schedule", row)} + + + + Route Family {formatNumber(row.route_family)} ·{" "} + {formatDeliveryMode(row.delivery_mode)} + + + + + {formatObservationStatus(row.status)} + + + )} + +
); } @@ -170,47 +186,58 @@ function MissedRows(props: { rows: ScheduleMissedObservation[] }) { 0} fallback={ - + } > -
-

Scroll the table horizontally on narrow screens.

-
- - - - Route - Status - Mode - Fire at - Claimed at - Age - - - - `${row.operation}:${row.fire_ms}`}> - {(row) => ( - - - - {formatFitzRoute("schedule", row)} - - - {row.status} - {row.delivery_mode} - {formatTimestamp(row.fire_at)} - {formatTimestamp(row.claimed_at)} - {formatDurationSeconds(row.age_seconds)} - - )} - - -
-
-
+ + `${row.route_family}:${row.operation}:${row.fire_ms}`}> + {(row) => ( + + + + + {formatFitzRoute("schedule", row)} + + + + Route Family {formatNumber(row.route_family)} ·{" "} + {formatDeliveryMode(row.delivery_mode)} + + + + + {formatObservationStatus(row.status)} + + + )} + +
); } @@ -228,15 +255,15 @@ export default function ScheduleResourcePage() { const timingMetric = data ? scheduleTimingMetric(data.detail.next_run) : null; return ( - + query.refresh(), }} status={queryHeaderStatus( @@ -275,7 +302,7 @@ export default function ScheduleResourcePage() { {(current) => ( - + @@ -298,19 +325,33 @@ export default function ScheduleResourcePage() { ]} /> - + {formatCount(current.executionObservations.observations.length, "observation")} + + } > - + - 0 ? "warning" : "success"} + > + {formatCount(current.missedHandoffs.observations.length, "claim")} + + } > - + - + )} - + ); } diff --git a/ui/src/pages/app/sessions.tsx b/ui/src/pages/app/sessions.tsx index 45168ee0..3a89e599 100644 --- a/ui/src/pages/app/sessions.tsx +++ b/ui/src/pages/app/sessions.tsx @@ -1,8 +1,8 @@ import { Show } from "@askrjs/askr/control"; -import { Stack } from "@askrjs/themes/components"; +import { Block } from "@askrjs/themes/components"; import DomainHeader from "@/components/shared/domain-header"; -import DomainMetricTable from "@/components/shared/domain-metric-table"; import DomainPageFrame from "@/components/shared/domain-page-frame"; +import DomainSummaryStrip from "@/components/shared/domain-summary-strip"; import { QueryErrorState, QueryLoadingState } from "@/components/shared/query-state"; import SessionTable from "@/components/shared/session-table"; import { createActiveSessionsQuery } from "@/features/session/session-query"; @@ -110,7 +110,7 @@ export default function SessionsPage() { return ( - + {(data) => ( - - + 0 ? "Maximum reported duration" : "No live sessions", }, ]} /> - + )} - + ); } diff --git a/ui/src/pages/app/stream-resource.tsx b/ui/src/pages/app/stream-resource.tsx index fbb4bdcd..4651fe98 100644 --- a/ui/src/pages/app/stream-resource.tsx +++ b/ui/src/pages/app/stream-resource.tsx @@ -1,22 +1,15 @@ import { state } from "@askrjs/askr"; import { Show } from "@askrjs/askr/control"; import { currentRoute, Link, navigate } from "@askrjs/askr/router"; -import { - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Inline, - Stack, -} from "@askrjs/themes/components"; -import { Form, Input, Label, VirtualTable, type VirtualTableColumn } from "@askrjs/ui"; +import { Button, Block } from "@askrjs/themes/components"; +import { Form, Input, Label } from "@askrjs/ui"; import type { StreamAdminRecord } from "@/adapters"; import CopyTextButton from "@/components/shared/copy-text-button"; +import DataTable, { type DataTableColumn } from "@/components/shared/data-table"; +import DomainDataSection from "@/components/shared/domain-data-section"; import DomainHeader from "@/components/shared/domain-header"; -import DomainMetricTable from "@/components/shared/domain-metric-table"; import DomainPageFrame from "@/components/shared/domain-page-frame"; +import DomainSummaryStrip from "@/components/shared/domain-summary-strip"; import OperatorScopeStrip from "@/components/shared/operator-scope-strip"; import { queryFreshness, queryHeaderStatus } from "@/components/shared/query-header-status"; import { @@ -27,7 +20,7 @@ import { } from "@/components/shared/query-state"; import { createStreamResourceQuery } from "@/features/stream/stream-query"; import { formatCount, formatNumber, formatTimestampMs } from "@/shared/format"; -import { domainResourceHref, formatFitzRoute } from "@/shared/navigation/domains"; +import { domainResourceHref } from "@/shared/navigation/domains"; const DEFAULT_LIMIT = 50; @@ -66,37 +59,17 @@ function recordsHref( return queryString ? `${href}?${queryString}` : href; } -const recordColumns: readonly VirtualTableColumn[] = [ - { - id: "route", - header: "Route", - width: "28%", - cellComponent: ({ row }) => { - const route = formatFitzRoute("stream", row); - - return ( - - {route} - - ); - }, - }, +const recordColumns: readonly DataTableColumn[] = [ { id: "offset", header: "Offset", - width: "9%", + width: "12%", cellComponent: ({ row }) => {formatNumber(row.resource_offset)}, }, - { - id: "family", - header: "Family", - width: "8%", - cellComponent: ({ row }) => {formatNumber(row.route_family)}, - }, { id: "created", header: "Created", - width: "18%", + width: "24%", cellComponent: ({ row }) => ( {formatTimestampMs(row.created_at_ms)} @@ -106,7 +79,7 @@ const recordColumns: readonly VirtualTableColumn[] = [ { id: "body", header: "Body", - width: "25%", + width: "44%", cellComponent: ({ row }) => ( {row.body.utf8 ?? row.body.base64} @@ -115,8 +88,8 @@ const recordColumns: readonly VirtualTableColumn[] = [ }, { id: "actions", - header: "Copy", - width: "12%", + header: "Action", + width: "20%", cellComponent: ({ row }) => ( [] = [ }, ]; -function recordTableHeight(rowCount: number) { - return `${Math.min(432, Math.max(144, 44 + rowCount * 48))}px`; -} - export default function StreamResourcePage() { const route = currentRoute(); const scope = { @@ -169,7 +138,7 @@ export default function StreamResourcePage() { return ( - + {data ? ( - ) : null} - - - Record filters - - Read committed records by from offset, optional discriminator, and limit. - - - -
- - - - )} - type="number" - value={fromOffsetDraft()} - onInput={(event: Event) => - setFromOffsetDraft((event.target as HTMLInputElement).value) - } - /> - - - - - setDiscriminatorDraft((event.target as HTMLInputElement).value) - } - /> - - - - )} - type="number" - value={limitDraft()} - onInput={(event: Event) => - setLimitDraft((event.target as HTMLInputElement).value) - } - /> - - - -
-
-
+ +
+ + + + )} + type="number" + value={fromOffsetDraft()} + onInput={(event: Event) => + setFromOffsetDraft((event.target as HTMLInputElement).value) + } + /> + + + + + setDiscriminatorDraft((event.target as HTMLInputElement).value) + } + /> + + + + )} + type="number" + value={limitDraft()} + onInput={(event: Event) => + setLimitDraft((event.target as HTMLInputElement).value) + } + /> + + + +
+
@@ -282,29 +253,26 @@ export default function StreamResourcePage() { 0}> - - - Committed records - - Durable stream records returned by the current read window. - - - - - aria-label="Stream records" - class="stream-resource-virtual-table" + + +

+ Scroll horizontally to inspect every record field and action. +

+ + ariaLabel="Stream records" + class="stream-record-table" columns={recordColumns} - getKey={(record) => `${record.route_family}:${record.resource_offset}`} - headerHeight={44} - overscan={8} - rowHeight={48} + getKey={(record) => record.resource_offset} rows={records} - style={{ height: recordTableHeight(records.length) }} /> -
-
+
+ - + 0}> - -
+ +
); } diff --git a/ui/src/pages/auth/_layout.tsx b/ui/src/pages/auth/_layout.tsx index 39371bad..2da9dc4e 100644 --- a/ui/src/pages/auth/_layout.tsx +++ b/ui/src/pages/auth/_layout.tsx @@ -9,7 +9,7 @@ export default function Layout({ children }: { children?: unknown }) { background="canvas" tabIndex={-1} > - + {children} diff --git a/ui/src/pages/auth/login.tsx b/ui/src/pages/auth/login.tsx index 5ffa2811..4f273f94 100644 --- a/ui/src/pages/auth/login.tsx +++ b/ui/src/pages/auth/login.tsx @@ -31,7 +31,7 @@ function resolveNextTarget() { } export default function Login() { - const [username, setUsername] = state("root"); + const [username, setUsername] = state(""); const [password, setPassword] = state(""); task(() => manageRoutePageContext("Sign in")); diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index f94c8368..10e816ef 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -1,62 +1,25 @@ :root { color-scheme: light dark; - --fitz-border: color-mix( - in srgb, - var(--ak-color-border, var(--ak-color-text, CanvasText)) 82%, - transparent - ); - --fitz-border-subtle: color-mix( - in srgb, - var(--ak-color-border, var(--ak-color-text, CanvasText)) 58%, - transparent - ); - --fitz-surface: var(--ak-color-surface, Canvas); - --fitz-surface-muted: var( - --ak-color-surface-muted, - color-mix(in srgb, var(--ak-color-surface, Canvas) 94%, var(--ak-color-text, CanvasText) 6%) - ); - --fitz-surface-soft: color-mix( - in srgb, - var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)) 88%, - var(--ak-color-surface, Canvas) 12% - ); - --fitz-text-muted: var( - --ak-color-muted, - var( - --ak-color-text-muted, - color-mix(in srgb, var(--ak-color-text, CanvasText) 64%, transparent) - ) - ); - --fitz-focus-ring: var( - --ak-color-focus-ring, - var(--ak-color-accent, var(--ak-color-text, CanvasText)) - ); - --fitz-page-background: var(--ak-color-bg, Canvas); - --fitz-page-gutter: var(--ak-space-4, 1rem); - --fitz-page-padding-block: var(--ak-space-3, 0.75rem); - --fitz-page-gap: var(--ak-space-3, 0.75rem); - --fitz-section-gap: var(--ak-space-3, 0.75rem); - --fitz-panel-padding: var(--ak-space-3, 0.75rem); - --fitz-control-gap: var(--ak-space-2, 0.5rem); - --fitz-title-size: 1.5rem; - --fitz-section-title-size: 1rem; - --fitz-body-size: 0.875rem; - --fitz-caption-size: 0.75rem; - --fitz-body-line-height: 1.4; - --fitz-title-line-height: 1.2; - --fitz-card-radius: var(--ak-radius-2, 0.5rem); - --fitz-row-radius: var(--ak-radius-1, 0.375rem); } body { min-height: 100vh; margin: 0; - background: var(--fitz-page-background); + background: var(--ak-color-bg, Canvas); font-feature-settings: "tnum" 0; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } +/* Workaround for an @askrjs/themes >=0.2.3 regression: PageHeader's internal + [data-slot="page-header-copy"] wrapper (title + description) is a hardcoded + Block with no direction prop and no prop pass-through, so it now inherits + Block's row initial value instead of stacking title above description. + Remove once askr-themes fixes this upstream (askrjs/askr-themes#98). */ +[data-slot="page-header-copy"] { + flex-direction: column; +} + a { color: inherit; } @@ -76,9 +39,9 @@ a { inset-inline-start: var(--ak-space-2, 0.5rem); z-index: calc(var(--ak-z-sticky, 20) + 1); padding: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-row-radius); - background: var(--fitz-surface); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-sm, 0.375rem); + background: var(--ak-color-surface, Canvas); box-shadow: 0 0.5rem 1.5rem color-mix(in srgb, var(--ak-color-text, CanvasText) 14%, transparent); color: var(--ak-color-text, CanvasText); font-weight: 700; @@ -91,7 +54,8 @@ a { .skip-link:focus-visible { opacity: 1; pointer-events: auto; - outline: 2px solid var(--fitz-focus-ring); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: 2px; } @@ -109,8 +73,9 @@ a { } .text-link:focus-visible { - border-radius: var(--fitz-row-radius); - outline: 2px solid var(--fitz-focus-ring); + border-radius: var(--ak-radius-sm, 0.375rem); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: 2px; } diff --git a/ui/src/styles/dashboard.css b/ui/src/styles/dashboard.css index b76c451d..2c359abe 100644 --- a/ui/src/styles/dashboard.css +++ b/ui/src/styles/dashboard.css @@ -1,294 +1,13 @@ -.dashboard-status-strip { - display: grid; - gap: var(--fitz-section-gap); -} - -.dashboard-status-summary { - display: flex; - align-items: start; - justify-content: space-between; - gap: var(--fitz-section-gap); -} - -.dashboard-status-summary h2 { - margin: 0; - font-size: var(--fitz-title-size); - font-weight: 720; - letter-spacing: 0; - line-height: var(--fitz-title-line-height); -} - -.dashboard-status-summary p { - max-width: 72ch; - margin: var(--ak-space-1, 0.25rem) 0 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); -} - -.dashboard-status-metrics { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); - gap: var(--fitz-control-gap); - margin: 0; -} - -.dashboard-status-metrics div { - min-width: 0; - padding-block-start: var(--fitz-control-gap); -} - -.dashboard-status-metrics dt, -.dashboard-status-metrics dd { - margin: 0; -} - -.dashboard-status-metrics dt { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); - font-weight: 700; - text-transform: uppercase; -} - -.dashboard-status-metrics dd { - margin-top: 0.125rem; - font-weight: 650; - overflow-wrap: anywhere; -} - -.dashboard-signal-section { - display: grid; - gap: var(--fitz-control-gap); -} - -.dashboard-signal-list { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 0.2rem; -} - -.dashboard-signal-item { - list-style: none; - display: block; - min-width: 0; - padding: 0; -} - -.dashboard-signal-link { - display: inline-flex; - align-items: center; - gap: 0.15rem; - font-size: 0.6875rem; - font-weight: 650; - border-radius: var(--fitz-row-radius); - color: var(--fitz-text-muted); - justify-self: end; -} - -.dashboard-signal-row { - display: grid; - grid-template-columns: minmax(0, 1fr) max-content; - align-items: start; - gap: 0.5rem; - padding: 0.4rem 0.45rem; - position: relative; - min-width: 0; -} - -.dashboard-signal-row::before { - content: ""; - position: absolute; - inset-block: 0.2rem; - inset-inline-start: 0; - width: 2px; - min-width: 0; - border-radius: 999px; - background: color-mix(in srgb, var(--fitz-border) 70%, transparent); -} - -.dashboard-signal-link:focus-visible { - outline: 2px solid var(--fitz-focus-ring); - outline-offset: 2px; -} - -.dashboard-signal-row, -.dashboard-signal-row * { - min-width: 0; - overflow-wrap: anywhere; -} - -.dashboard-signal-body { - display: grid; - gap: 0.3rem; - min-width: 0; -} - -.dashboard-signal-title { - font-size: var(--fitz-body-size); - font-weight: 700; - letter-spacing: 0; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.dashboard-signal-description { - font-size: var(--fitz-caption-size); - color: var(--fitz-text-muted); - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dashboard-signal-item-quiet .dashboard-signal-row { - background-color: transparent; -} - -.dashboard-signal-item-quiet .dashboard-signal-row::before { - background: color-mix(in srgb, var(--fitz-border) 84%, transparent); -} - -.dashboard-signal-item-flowing .dashboard-signal-row::before { - background: var(--ak-color-success, #16a34a); -} - -.dashboard-signal-item-pressure .dashboard-signal-row::before { - background: var(--ak-color-warning, #ca8a04); -} - -.dashboard-signal-item-blocked .dashboard-signal-row::before { - background: var(--ak-color-danger, #dc2626); -} - -.dashboard-signal-heading { - gap: var(--ak-space-1-5, 0.375rem); - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; -} - -.dashboard-signal-title-row { - display: inline-flex; - align-items: center; - gap: var(--fitz-control-gap); - min-width: 0; -} - -.dashboard-signal-title-row svg { - flex: none; - color: var(--fitz-text-muted); -} - -.dashboard-signal-metrics { - display: flex; - flex-wrap: wrap; - gap: 0.35rem var(--fitz-control-gap); - margin: 0; -} - -.dashboard-signal-metric { - min-width: 0; - display: inline-flex; - align-items: baseline; - gap: 0.25rem; - min-inline-size: 0; - white-space: nowrap; -} - -.dashboard-signal-metrics dt, -.dashboard-signal-metrics dd { - margin: 0; -} - -.dashboard-signal-metrics dt { - color: var(--fitz-text-muted); - font-size: 0.6875rem; - font-weight: 700; - letter-spacing: 0; - text-transform: none; - text-wrap: nowrap; -} - -.dashboard-signal-metrics dd { - margin-top: 0; - font-weight: 650; - font-variant-numeric: tabular-nums; - font-size: var(--fitz-caption-size); - line-height: 1.1; - white-space: nowrap; -} - -.dashboard-signal-link:hover svg { - transform: translate(1px, -1px); -} - -.dashboard-behavior-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); - gap: var(--fitz-section-gap); - margin-top: var(--fitz-section-gap); -} - -.dashboard-behavior-list { - display: grid; - gap: var(--fitz-control-gap); -} - -.dashboard-behavior-row { - display: grid; - gap: 0.125rem; - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-row-radius); - background: var(--fitz-surface-soft); - text-decoration: none; - transition: - background-color 120ms var(--ak-ease-standard, ease), - border-color 120ms var(--ak-ease-standard, ease), - box-shadow 120ms var(--ak-ease-standard, ease); -} - -.dashboard-behavior-row:hover { - background: var(--fitz-surface-muted); - border-color: color-mix(in srgb, var(--ak-color-accent, CanvasText) 28%, transparent); - box-shadow: 0 1px 3px color-mix(in srgb, CanvasText 9%, transparent); -} - -.dashboard-behavior-row:focus-visible { - outline: 2px solid var(--fitz-focus-ring); - outline-offset: 2px; -} - -.dashboard-behavior-row span, -.dashboard-behavior-row strong, -.dashboard-behavior-row small { - min-width: 0; - overflow-wrap: anywhere; -} - -.dashboard-behavior-row span { - font-weight: 650; -} - -.dashboard-behavior-row small { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); - line-height: var(--fitz-body-line-height); -} - .overview-status-band { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; - gap: var(--fitz-control-gap) var(--fitz-section-gap); - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); + gap: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); border-inline-start-width: 4px; - border-radius: var(--fitz-card-radius); - background: var(--fitz-surface); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface, Canvas); } .overview-status-band > [data-slot="badge"] { @@ -320,7 +39,7 @@ block-size: 1.75rem; border-radius: 999px; color: var(--ak-color-text, CanvasText); - background: var(--fitz-surface-soft); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .overview-status-copy { @@ -329,34 +48,34 @@ .overview-status-copy h2 { margin: 0; - font-size: var(--fitz-section-title-size); + font-size: var(--ak-font-size-md, 1rem); font-weight: 720; - letter-spacing: 0; - line-height: var(--fitz-title-line-height); + line-height: var(--ak-line-height-tight, 1.2); } .overview-status-copy p { max-width: 72ch; margin: var(--ak-space-1, 0.25rem) 0 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); } .overview-issues-section { display: grid; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .overview-empty-state { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; - gap: var(--fitz-section-gap); - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border-subtle); - border-radius: var(--fitz-row-radius); - background: var(--fitz-surface-soft); + gap: var(--ak-space-3, 0.75rem); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid + var(--ak-color-border-subtle, var(--ak-color-border, var(--ak-color-text, CanvasText))); + border-radius: var(--ak-radius-sm, 0.375rem); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .overview-empty-state strong, @@ -367,7 +86,7 @@ .overview-empty-state p { margin: var(--ak-space-1, 0.25rem) 0 0; - color: var(--fitz-text-muted); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); } .overview-empty-state svg { @@ -376,7 +95,7 @@ .overview-issue-list { display: grid; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); padding: 0; margin: 0; list-style: none; @@ -385,13 +104,13 @@ .overview-issue-item { display: grid; grid-template-columns: minmax(0, 1fr) max-content; - gap: var(--fitz-section-gap); + gap: var(--ak-space-3, 0.75rem); align-items: start; - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); border-inline-start-width: 3px; - border-radius: var(--fitz-row-radius); - background: var(--fitz-surface); + border-radius: var(--ak-radius-sm, 0.375rem); + background: var(--ak-color-surface, Canvas); } .overview-issue-item-warning { @@ -425,14 +144,14 @@ .overview-issue-main p { margin: 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); } .overview-issue-main > span { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); } .overview-issue-heading, @@ -440,7 +159,7 @@ display: flex; align-items: center; justify-content: space-between; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); min-width: 0; } @@ -456,13 +175,14 @@ align-items: center; gap: 0.2rem; color: var(--ak-color-text, CanvasText); - font-size: var(--fitz-caption-size); + font-size: var(--ak-font-size-xs, 0.75rem); font-weight: 650; text-decoration: none; } .overview-action-link:focus-visible { - outline: 2px solid var(--fitz-focus-ring); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: 2px; } @@ -473,9 +193,9 @@ .overview-domain-grid { display: flex; flex-wrap: wrap; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); padding: 0; - margin: var(--fitz-section-gap) 0 0; + margin: var(--ak-space-3, 0.75rem) 0 0; list-style: none; } @@ -485,13 +205,13 @@ .overview-domain-card { display: grid; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); min-width: 0; - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); border-block-start-width: 3px; - border-radius: var(--fitz-row-radius); - background: var(--fitz-surface); + border-radius: var(--ak-radius-sm, 0.375rem); + background: var(--ak-color-surface, Canvas); } .overview-domain-card-success { @@ -499,7 +219,7 @@ } .overview-domain-card-info { - border-block-start-color: var(--fitz-border); + border-block-start-color: var(--ak-color-border, var(--ak-color-text, CanvasText)); } .overview-domain-card-warning { @@ -513,22 +233,22 @@ .overview-domain-card p { min-width: 0; margin: 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); overflow-wrap: anywhere; } .overview-vitals-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); - gap: var(--fitz-control-gap); - margin: var(--fitz-section-gap) 0 0; + gap: var(--ak-space-2, 0.5rem); + margin: var(--ak-space-3, 0.75rem) 0 0; } .overview-vitals-grid div { min-width: 0; - padding-block-start: var(--fitz-control-gap); + padding-block-start: var(--ak-space-2, 0.5rem); } .overview-vitals-grid dt, @@ -537,20 +257,19 @@ } .overview-vitals-grid dt { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); font-weight: 700; - text-transform: uppercase; } .overview-vitals-grid dd { margin-top: 0.125rem; - font-size: var(--fitz-section-title-size); + font-size: var(--ak-font-size-md, 1rem); font-weight: 720; } .overview-vitals-grid small { display: block; margin-top: 0.125rem; - color: var(--fitz-text-muted); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); } diff --git a/ui/src/styles/domain-pages.css b/ui/src/styles/domain-pages.css index fae87cfb..734ec9aa 100644 --- a/ui/src/styles/domain-pages.css +++ b/ui/src/styles/domain-pages.css @@ -14,8 +14,8 @@ .domain-section { display: grid; - gap: var(--fitz-control-gap); - padding-block: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); + padding-block: var(--ak-space-2, 0.5rem); } .domain-section-header { @@ -28,12 +28,10 @@ white-space: normal; } -.domain-stat-grid [data-slot="card"], .domain-section [data-slot="card"] { min-width: 0; } -.domain-stat-value, .domain-resource-metric, .domain-metric-value { font-variant-numeric: tabular-nums; @@ -41,15 +39,15 @@ .domain-metric-value, .domain-resource-metric { - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); } .domain-inventory-scroll-hint { display: none; margin: 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); } .domain-table-wrap { @@ -57,77 +55,52 @@ overflow-x: auto; } -.domain-table-wrap [data-slot="table"] { - min-inline-size: 42rem; -} - -.notice-resource-table-wrap table, -.notice-operation-table-wrap table, -.rpc-operation-table-wrap table, -.notice-operation-table-wrap [data-slot="table"] { - min-inline-size: 52rem; -} - -.schedule-observation-table-wrap [data-slot="table"], -.lease-resource-table-wrap [data-slot="table"] { - min-inline-size: 58rem; - table-layout: fixed; -} - -.schedule-observation-table-wrap :is(th, td):nth-child(1) { - width: 34%; +.lease-ownership-card [data-slot="card-title"] { + overflow-wrap: anywhere; } -.schedule-observation-table-wrap :is(th, td):nth-child(2) { - width: 10%; +.lease-ownership-card { + gap: 0; + padding-block: var(--ak-space-4, 1rem); } -.schedule-observation-table-wrap :is(th, td):nth-child(3), -.schedule-observation-table-wrap :is(th, td):nth-child(4) { - width: 20%; +.lease-ownership-card [data-slot="card-content"] { + padding-inline: var(--ak-space-4, 1rem); } -.schedule-observation-table-wrap :is(th, td):nth-child(5) { - width: 16%; +.lease-ownership-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: var(--ak-space-3, 0.75rem) var(--ak-space-4, 1rem); + margin: 0; } -.lease-resource-table-wrap :is(th, td):first-child { - width: 24%; +.lease-ownership-details > div { + min-width: 0; } -.lease-resource-table-wrap :is(th, td):last-child { - width: 20%; +.lease-ownership-details dt { + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); } -.notice-resource-table-wrap th, -.notice-resource-table-wrap td, -.notice-operation-table-wrap [data-slot="table-header-cell"], -.notice-operation-table-wrap [data-slot="table-cell"], -.notice-operation-table-wrap th, -.notice-operation-table-wrap td, -.rpc-operation-table-wrap th, -.rpc-operation-table-wrap td { - white-space: nowrap; +.lease-ownership-details dd { + margin: var(--ak-space-1, 0.25rem) 0 0; + overflow-wrap: anywhere; + font-variant-numeric: tabular-nums; } -.domain-table-wrap [data-slot="table-header-cell"], -.domain-table-wrap th { +.domain-table-wrap [data-slot="table-header-cell"] { white-space: nowrap; } .domain-metric-table-wrap [data-slot="table-head"], -.domain-metric-table-wrap [data-slot="table-body"], -.notice-operation-table-wrap [data-slot="table-head"], -.notice-operation-table-wrap [data-slot="table-body"], -.rpc-operation-table-wrap [data-slot="table-head"], -.rpc-operation-table-wrap [data-slot="table-body"] { +.domain-metric-table-wrap [data-slot="table-body"] { display: block; inline-size: 100%; } -.domain-metric-table-wrap [data-slot="table-row"], -.notice-operation-table-wrap [data-slot="table-row"], -.rpc-operation-table-wrap [data-slot="table-row"] { +.domain-metric-table-wrap [data-slot="table-row"] { display: grid; inline-size: 100%; } @@ -136,32 +109,22 @@ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } -.notice-operation-table-wrap [data-slot="table-row"], -.rpc-operation-table-wrap [data-slot="table-row"] { - grid-template-columns: repeat(5, minmax(0, 1fr)); -} - -.domain-resource-virtual-table[data-has-metrics="true"] [data-slot="virtual-table-table"], -.queue-resource-virtual-table [data-slot="virtual-table-table"], -.stream-record-virtual-table [data-slot="virtual-table-table"], -.rpc-operation-virtual-table [data-slot="virtual-table-table"], -.lease-resource-virtual-table [data-slot="virtual-table-table"], -.schedule-resource-virtual-table [data-slot="virtual-table-table"] { +.domain-resource-data-table[data-has-metrics="true"] [data-slot="table"], +.queue-resource-data-table [data-slot="table"], +.stream-record-table [data-slot="table"] { min-inline-size: 48rem; } -.queue-resource-virtual-table [data-slot="virtual-table-table"] { +.queue-resource-data-table [data-slot="table"] { min-inline-size: 58rem; } -.domain-resource-virtual-table [data-slot="virtual-table-header-cell"], -.domain-resource-virtual-table [data-slot="virtual-table-cell"], -.queue-resource-virtual-table [data-slot="virtual-table-header-cell"], -.queue-resource-virtual-table [data-slot="virtual-table-cell"], -.stream-record-virtual-table [data-slot="virtual-table-header-cell"], -.stream-record-virtual-table [data-slot="virtual-table-cell"], -.rpc-operation-virtual-table [data-slot="virtual-table-header-cell"], -.rpc-operation-virtual-table [data-slot="virtual-table-cell"] { +.domain-resource-data-table [data-slot="table-header-cell"], +.domain-resource-data-table [data-slot="table-cell"], +.queue-resource-data-table [data-slot="table-header-cell"], +.queue-resource-data-table [data-slot="table-cell"], +.stream-record-table [data-slot="table-header-cell"], +.stream-record-table [data-slot="table-cell"] { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -190,9 +153,6 @@ .domain-header-title-row, .domain-inventory-toolbar, .domain-inventory-search-shell, - .domain-header-actions, - .domain-header-actions > button, - .domain-query-mode-grid, .kv-encoding-controls { width: 100%; } @@ -212,25 +172,13 @@ overflow-wrap: anywhere; } - .domain-table-wrap { - max-width: 100%; - overflow-x: auto; - } - - .domain-table-wrap [data-slot="table"] { - min-inline-size: 38rem; - } - - .domain-resource-virtual-table[data-has-metrics="true"] [data-slot="virtual-table-table"], - .queue-resource-virtual-table [data-slot="virtual-table-table"], - .stream-record-virtual-table [data-slot="virtual-table-table"], - .rpc-operation-virtual-table [data-slot="virtual-table-table"], - .lease-resource-virtual-table [data-slot="virtual-table-table"], - .schedule-resource-virtual-table [data-slot="virtual-table-table"] { + .domain-resource-data-table[data-has-metrics="true"] [data-slot="table"], + .queue-resource-data-table [data-slot="table"], + .stream-record-table [data-slot="table"] { min-inline-size: 42rem; } - .queue-resource-virtual-table [data-slot="virtual-table-table"] { + .queue-resource-data-table [data-slot="table"] { min-inline-size: 52rem; } } diff --git a/ui/src/styles/domain.css b/ui/src/styles/domain.css index 39b43859..2481abf1 100644 --- a/ui/src/styles/domain.css +++ b/ui/src/styles/domain.css @@ -1,6 +1,6 @@ .domain-header, .domain-section-header { - gap: var(--fitz-section-gap); + gap: var(--ak-space-3, 0.75rem); } .domain-header { @@ -26,34 +26,31 @@ .domain-header-kicker { margin: 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); - font-weight: 700; - letter-spacing: 0; - text-transform: uppercase; + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); + font-weight: 600; } .domain-header-title-row { display: flex; align-items: center; flex-wrap: wrap; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .domain-header h1, .domain-section-header h2 { margin: 0; - letter-spacing: 0; - line-height: var(--fitz-title-line-height); + line-height: var(--ak-line-height-tight, 1.2); } .domain-header h1 { - font-size: var(--fitz-title-size); + font-size: var(--ak-font-size-2xl, 1.5rem); font-weight: 720; } .domain-section-header h2 { - font-size: var(--fitz-section-title-size); + font-size: var(--ak-font-size-md, 1rem); font-weight: 700; } @@ -63,9 +60,9 @@ .domain-muted, .domain-link-row > span:last-child { margin: var(--ak-space-1, 0.25rem) 0 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); overflow-wrap: anywhere; word-break: break-word; } @@ -77,19 +74,9 @@ white-space: nowrap; } -.domain-header-actions { - align-items: flex-end; -} - -.domain-header-actions > button { - flex-shrink: 0; - white-space: nowrap; - width: max-content; -} - .domain-section-header span { flex-shrink: 0; - font-size: var(--fitz-body-size); + font-size: var(--ak-font-size-sm, 0.875rem); } .domain-inventory-toolbar { @@ -97,8 +84,8 @@ align-items: center; justify-content: flex-start; flex-wrap: wrap; - gap: var(--fitz-control-gap); - margin-top: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); + margin-top: var(--ak-space-2, 0.5rem); min-width: 0; } @@ -113,7 +100,7 @@ inset-inline-start: 0.75rem; top: 50%; z-index: 1; - color: var(--fitz-text-muted); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); pointer-events: none; transform: translateY(-50%); } @@ -126,16 +113,17 @@ .operator-scope-strip, .page-action-bar { min-width: 0; - padding: var(--fitz-control-gap) var(--fitz-panel-padding); - border: 1px solid var(--fitz-border-subtle); - border-radius: var(--fitz-row-radius); - background: var(--fitz-surface-soft); + padding: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); + border: 1px solid + var(--ak-color-border-subtle, var(--ak-color-border, var(--ak-color-text, CanvasText))); + border-radius: var(--ak-radius-sm, 0.375rem); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .operator-scope-strip dl { display: flex; flex-wrap: wrap; - gap: var(--fitz-control-gap) var(--fitz-section-gap); + gap: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); margin: 0; } @@ -146,19 +134,17 @@ } .operator-scope-strip dt { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); - font-weight: 700; - letter-spacing: 0; - text-transform: uppercase; + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); + font-weight: 600; } .operator-scope-strip dd { margin: 0; min-width: 0; overflow-wrap: anywhere; - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); } .page-action-bar { @@ -166,15 +152,15 @@ align-items: center; justify-content: space-between; flex-wrap: wrap; - gap: var(--fitz-control-gap) var(--fitz-section-gap); + gap: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); } .page-action-bar p { max-width: 68ch; margin: 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); overflow-wrap: anywhere; } @@ -183,7 +169,7 @@ align-items: center; justify-content: flex-end; flex-wrap: wrap; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .page-action-link { @@ -191,48 +177,151 @@ min-height: 2.25rem; align-items: center; justify-content: center; - padding: 0 var(--fitz-control-gap); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-row-radius); + padding: 0 var(--ak-space-2, 0.5rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-sm, 0.375rem); color: var(--ak-color-text, CanvasText); - font-size: var(--fitz-body-size); + font-size: var(--ak-font-size-sm, 0.875rem); font-weight: 650; text-decoration: none; } .page-action-link:hover { - background: var(--fitz-surface-muted); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .page-action-link:focus-visible { - outline: 2px solid var(--fitz-focus-ring); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: 2px; } .domain-section { - padding-block: var(--fitz-control-gap); + padding-block: var(--ak-space-2, 0.5rem); +} + +.domain-summary-header h2 { + margin: 0; + font-size: var(--ak-font-size-lg); + line-height: var(--ak-line-height-tight); } -.domain-stat-grid { +.domain-summary-header, +.query-compact-layout, +.lease-ownership-list { + display: flex; + flex-direction: column; +} + +.domain-summary-header, +.query-compact-layout { + gap: var(--ak-space-1, 0.25rem); +} + +.lease-ownership-list { + gap: var(--ak-space-3, 0.75rem); +} + +.domain-summary-items { + display: flex; + flex-flow: row wrap; + gap: var(--ak-space-3, 0.75rem); +} + +.domain-summary-item { + flex: 1 1 13rem; + gap: var(--ak-space-3, 0.75rem); + padding-block: var(--ak-space-4, 1rem); +} + +.domain-summary-item [data-slot="card-content"] { + padding-inline: var(--ak-space-4, 1rem); +} + +.domain-summary-item [data-slot="stat"] { + gap: var(--ak-space-1, 0.25rem); +} + +.domain-operation-metrics { + display: flex; + flex-wrap: wrap; + gap: var(--ak-space-2, 0.5rem) var(--ak-space-5, 1.25rem); + margin: var(--ak-space-2, 0.5rem) 0 0; +} + +.domain-operation-metrics > div { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: auto auto; + gap: var(--ak-space-1, 0.25rem); + align-items: baseline; +} + +.domain-operation-metrics dt { + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); +} + +.domain-operation-metrics dd { + margin: 0; + font-family: var(--ak-font-family-mono, monospace); + font-size: var(--ak-font-size-sm, 0.875rem); + font-variant-numeric: tabular-nums; +} + +.domain-divided-list { + margin: 0; + padding: 0; + border: 1px solid var(--ak-color-border); + border-radius: var(--ak-radius-md); + background: var(--ak-color-surface); + list-style: none; + overflow: hidden; +} + +.domain-divided-list > * { min-width: 0; - gap: var(--ak-space-md, 1rem); + padding: var(--ak-space-md); } -.chart-grid { +.domain-divided-list > * + * { + border-top: 1px solid var(--ak-color-border); +} + +.domain-divided-list [data-slot="item-content"] { + flex: 1 1 auto; +} + +.domain-divided-list [data-slot="item-actions"] { + flex: none; +} + +.schedule-evidence-metadata dl { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); - align-items: start; - gap: var(--fitz-section-gap); + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--ak-space-sm); + width: 100%; + margin: 0; } -.domain-stat-value { - display: block; +.schedule-evidence-metadata dl > div { min-width: 0; +} + +.schedule-evidence-metadata dt { + color: var(--ak-color-text-muted); + font-size: var(--ak-font-size-sm); +} + +.schedule-evidence-metadata dd { + margin: 0; overflow-wrap: anywhere; - font-size: 1.125rem; - line-height: var(--fitz-title-line-height); +} + +.chart-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); + align-items: start; + gap: var(--ak-space-3, 0.75rem); } .domain-table-wrap { @@ -245,21 +334,10 @@ font-variant-numeric: tabular-nums; } -.domain-query-mode-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.5rem), 1fr)); - gap: var(--fitz-control-gap); -} - -.domain-query-mode-grid button { - width: 100%; - justify-content: center; -} - .kv-encoding-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .kv-encoding-controls button { @@ -268,7 +346,7 @@ } .kv-query-actions { - margin-top: var(--fitz-section-gap); + margin-top: var(--ak-space-3, 0.75rem); } .kv-query-actions .domain-muted { @@ -277,23 +355,23 @@ .kv-query-result { display: grid; - gap: var(--fitz-section-gap); - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-card-radius); - background: var(--fitz-surface); + gap: var(--ak-space-3, 0.75rem); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface, Canvas); } .kv-query-result-grid { display: grid; grid-template-columns: minmax(7rem, max-content) minmax(0, 1fr); - gap: var(--fitz-control-gap) var(--fitz-section-gap); + gap: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); align-items: center; } .kv-query-result-grid > span { - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); } .kv-query-result-grid > strong { @@ -306,7 +384,7 @@ .diagnostics-summary-grid, .diagnostics-contract-grid { display: grid; - gap: var(--fitz-section-gap); + gap: var(--ak-space-3, 0.75rem); } .diagnostics-summary-grid { @@ -322,10 +400,10 @@ display: grid; gap: var(--ak-space-1, 0.25rem); min-width: 0; - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-card-radius); - background: var(--fitz-surface); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface, Canvas); } .diagnostics-summary-card strong, @@ -337,76 +415,45 @@ .diagnostics-summary-card span:last-child, .diagnostics-contract-card p { margin: 0; - color: var(--fitz-text-muted); - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); overflow-wrap: anywhere; } -.domain-state-compact { - padding: var(--fitz-control-gap); -} - -.domain-state-compact [data-slot="empty-state"] { - min-height: 0; - padding: var(--fitz-control-gap); -} - .metrics-structured-payload [data-slot="collapsible-content"] { min-width: 0; } -.dashboard-hotspot-virtual-table, -.diagnostics-virtual-table, -.domain-resource-virtual-table, -.kv-resource-virtual-table, -.lease-resource-virtual-table, -.metrics-sample-virtual-table, -.queue-resource-virtual-table, -.resource-related-virtual-table, -.schedule-resource-virtual-table, -.session-virtual-table, -.stream-resource-virtual-table { +.diagnostics-data-table, +.domain-resource-data-table, +.metrics-sample-data-table, +.queue-resource-data-table, +.stream-record-table { font-variant-numeric: tabular-nums; } -.dashboard-hotspot-virtual-table [data-slot="virtual-table-table"], -.resource-related-virtual-table [data-slot="virtual-table-table"] { - min-inline-size: 68rem; -} - -.diagnostics-virtual-table [data-slot="virtual-table-table"] { +.diagnostics-data-table [data-slot="table"] { min-inline-size: 56rem; } -.metrics-sample-virtual-table [data-slot="virtual-table-table"] { +.metrics-sample-data-table [data-slot="table"] { min-inline-size: 60rem; } -.session-virtual-table [data-slot="virtual-table-table"] { - min-inline-size: 62rem; -} - -.dashboard-hotspot-virtual-table [data-slot="virtual-table-header-cell"], -.dashboard-hotspot-virtual-table [data-slot="virtual-table-cell"], -.diagnostics-virtual-table [data-slot="virtual-table-header-cell"], -.diagnostics-virtual-table [data-slot="virtual-table-cell"], -.metrics-sample-virtual-table [data-slot="virtual-table-header-cell"], -.metrics-sample-virtual-table [data-slot="virtual-table-cell"], -.queue-resource-virtual-table [data-slot="virtual-table-header-cell"], -.queue-resource-virtual-table [data-slot="virtual-table-cell"], -.resource-related-virtual-table [data-slot="virtual-table-header-cell"], -.resource-related-virtual-table [data-slot="virtual-table-cell"], -.session-virtual-table [data-slot="virtual-table-header-cell"], -.session-virtual-table [data-slot="virtual-table-cell"] { +.diagnostics-data-table [data-slot="table-header-cell"], +.diagnostics-data-table [data-slot="table-cell"], +.metrics-sample-data-table [data-slot="table-header-cell"], +.metrics-sample-data-table [data-slot="table-cell"], +.queue-resource-data-table [data-slot="table-header-cell"], +.queue-resource-data-table [data-slot="table-cell"] { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.dashboard-hotspot-virtual-table .text-link, -.diagnostics-virtual-table .text-link, -.queue-resource-virtual-table .text-link { +.diagnostics-data-table .text-link, +.queue-resource-data-table .text-link { display: inline-flex; min-width: 0; max-width: 100%; @@ -435,24 +482,35 @@ Monaco, monospace ); - font-size: var(--fitz-body-size); + font-size: var(--ak-font-size-sm, 0.875rem); } .diagnostics-value-cell { font-weight: 600; } -.diagnostics-virtual-table [data-slot="virtual-table-cell"], -.dashboard-hotspot-virtual-table [data-slot="virtual-table-cell"], -.domain-resource-virtual-table [data-slot="virtual-table-cell"], -.kv-resource-virtual-table [data-slot="virtual-table-cell"], -.lease-resource-virtual-table [data-slot="virtual-table-cell"], -.metrics-sample-virtual-table [data-slot="virtual-table-cell"], -.queue-resource-virtual-table [data-slot="virtual-table-cell"], -.resource-related-virtual-table [data-slot="virtual-table-cell"], -.schedule-resource-virtual-table [data-slot="virtual-table-cell"], -.session-virtual-table [data-slot="virtual-table-cell"], -.stream-resource-virtual-table [data-slot="virtual-table-cell"] { +/* Inventory tables (route + numeric metric columns): right-align every + metric column so digits line up for at-a-glance comparison. Scoped to + [data-has-metrics="true"], which only the route+metrics inventory table + sets -- other domain-resource-data-table consumers (e.g. the KV + committed-rows table) mix text and numeric columns and are unaffected. */ +.domain-resource-data-table[data-has-metrics="true"] + [data-slot="table-header-cell"]:not(:first-child), +.domain-resource-data-table[data-has-metrics="true"] [data-slot="table-cell"]:not(:first-child) { + text-align: end; +} + +.domain-resource-data-table[data-has-metrics="true"] + [data-slot="table-header-cell"]:not(:first-child) + .domain-sort-button { + justify-content: flex-end; +} + +.diagnostics-data-table [data-slot="table-cell"], +.domain-resource-data-table [data-slot="table-cell"], +.metrics-sample-data-table [data-slot="table-cell"], +.queue-resource-data-table [data-slot="table-cell"], +.stream-record-table [data-slot="table-cell"] { vertical-align: middle; } @@ -465,10 +523,19 @@ white-space: nowrap; } +.domain-table-cell-wrap { + display: block; + min-width: 0; + max-width: 100%; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; +} + .domain-scroll-hint { - margin: 0 0 var(--fitz-control-gap); - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + margin: 0 0 var(--ak-space-2, 0.5rem); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); } .queue-dead-letter-reason { @@ -497,15 +564,14 @@ .queue-action-cell { display: flex; flex-wrap: wrap; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); min-width: 0; } .domain-link-cell { - display: inline-flex; + display: block; min-width: 0; max-width: 100%; - align-items: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -527,13 +593,14 @@ } .domain-sort-button:focus-visible { - outline: 2px solid var(--fitz-focus-ring); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: 2px; } .domain-sort-indicator { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); } .metrics-table-wrap { @@ -561,8 +628,8 @@ max-width: 24rem; overflow: hidden; text-overflow: ellipsis; - font-size: var(--fitz-caption-size); - color: var(--fitz-text-muted); + font-size: var(--ak-font-size-xs, 0.75rem); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); } .metrics-value-cell { @@ -582,27 +649,6 @@ width: 8rem; } -.session-table-desktop { - display: block; - max-inline-size: 100%; - overflow-x: auto; - scrollbar-gutter: stable; -} - -.session-table-mobile { - display: none; -} - -.session-table-cell-truncate { - display: inline-flex; - align-items: baseline; - min-width: 0; - max-width: 18rem; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - .session-table-cell-wrap { display: inline-flex; align-items: baseline; @@ -612,64 +658,138 @@ word-break: break-word; } -.session-mobile-list { +.session-list-section { + display: grid; + gap: var(--ak-space-3, 0.75rem); + min-width: 0; +} + +.session-list-section-header { + display: grid; + gap: var(--ak-space-1, 0.25rem); +} + +.session-list-section-header h2, +.session-list-section-header p { + margin: 0; +} + +.session-list-section-header h2 { + font-size: var(--ak-font-size-2xl, 1.5rem); +} + +.session-list-section-header p { + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); +} + +.session-list { margin: 0; padding: 0; list-style: none; - display: grid; - gap: var(--fitz-section-gap); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface, Canvas); + overflow: hidden; } -.session-mobile-row { - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-card-radius); - padding: var(--fitz-panel-padding); - background: var(--fitz-surface); +.session-list-item { + padding: var(--ak-space-3, 0.75rem); + background: var(--ak-color-surface, Canvas); } -.session-mobile-grid { - margin: 0; - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--fitz-control-gap); - min-width: 0; +.session-list-item + .session-list-item { + border-top: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); } .diagnostics-link-grid { display: grid; - gap: var(--fitz-section-gap); + gap: var(--ak-space-3, 0.75rem); } .diagnostics-link-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); } -.session-mobile-grid > div { - display: grid; - gap: 0.2rem; +.session-list-heading { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--ak-space-2, 0.5rem); min-width: 0; } -.session-mobile-grid dt { - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); - line-height: 1.35; +.session-list-badge { + display: inline-flex; + align-items: center; + min-height: 1.5rem; + padding-inline: var(--ak-space-2, 0.5rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: 999px; + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); + line-height: 1; } -.session-mobile-grid dd { +.session-list-description { + display: flex; + flex-wrap: wrap; + gap: var(--ak-space-1, 0.25rem); + margin: var(--ak-space-2, 0.5rem) 0 0; + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-sm, 0.875rem); +} + +.session-list-metadata { + display: flex; + flex-wrap: wrap; + gap: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); + margin: var(--ak-space-3, 0.75rem) 0 0; + min-width: 0; +} + +.session-list-metadata > div { + display: inline-flex; + align-items: baseline; + gap: var(--ak-space-1, 0.25rem); + min-width: 0; +} + +.session-list-metadata dt { + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); +} + +.session-list-metadata dd { margin: 0; min-width: 0; + color: var(--ak-color-text, CanvasText); + font-size: var(--ak-font-size-xs, 0.75rem); } -.session-mobile-row { - display: grid; - gap: var(--fitz-control-gap); +.session-list-id { + font-family: var( + --ak-font-family-mono, + ui-monospace, + Consolas, + "SFMono-Regular", + Menlo, + Monaco, + monospace + ); + min-width: 0; + overflow: hidden; + color: var(--ak-color-primary, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-2xl, 1.5rem); + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; } .domain-link-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); - gap: var(--fitz-section-gap); + gap: var(--ak-space-3, 0.75rem); padding: 0; margin: 0; border: 0; @@ -687,10 +807,10 @@ display: grid; gap: var(--ak-space-1, 0.25rem); min-height: 4.25rem; - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-card-radius); - background: var(--fitz-surface); + padding: var(--ak-space-3, 0.75rem); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface, Canvas); text-decoration: none; transition: background-color 120ms var(--ak-ease-standard, ease), @@ -699,49 +819,50 @@ } .domain-link-row:hover { - background: var(--fitz-surface-muted); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); border-color: color-mix(in srgb, var(--ak-color-accent, CanvasText) 28%, transparent); box-shadow: 0 1px 3px color-mix(in srgb, CanvasText 9%, transparent); } .domain-link-row:focus-visible { - outline: 2px solid var(--fitz-focus-ring); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: 2px; } .domain-link-title { display: inline-flex; align-items: center; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); font-weight: 600; } .domain-link-row > span:last-child { - font-size: var(--fitz-body-size); + font-size: var(--ak-font-size-sm, 0.875rem); } .domain-state { margin: 0; - padding: var(--fitz-panel-padding); - border: 1px dashed var(--fitz-border); - border-radius: var(--fitz-card-radius); - background: var(--fitz-surface-soft); + padding: var(--ak-space-3, 0.75rem); + border: 1px dashed var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .domain-state-inline { display: flex; align-items: center; - gap: var(--fitz-section-gap); - padding: var(--fitz-control-gap) var(--fitz-panel-padding); + gap: var(--ak-space-3, 0.75rem); + padding: var(--ak-space-2, 0.5rem) var(--ak-space-3, 0.75rem); border-style: solid; - border-radius: var(--fitz-card-radius); - background: var(--fitz-surface); + border-radius: var(--ak-radius-md, 0.5rem); + background: var(--ak-color-surface, Canvas); } .domain-state-inline p { margin: 0; - font-size: var(--fitz-body-size); - line-height: var(--fitz-body-line-height); + font-size: var(--ak-font-size-sm, 0.875rem); + line-height: var(--ak-line-height-normal, 1.4); } .domain-metric-value { @@ -751,29 +872,60 @@ .domain-metric-caption { display: block; margin-top: 0.125rem; - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); } .search-match-strip { display: flex; flex-wrap: wrap; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .search-match-chip { max-width: 100%; overflow: hidden; padding: 0.25rem 0.5rem; - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-row-radius); - color: var(--fitz-text-muted); - font-size: var(--fitz-caption-size); + border: 1px solid var(--ak-color-border, var(--ak-color-text, CanvasText)); + border-radius: var(--ak-radius-sm, 0.375rem); + color: var(--ak-color-text-muted, var(--ak-color-text, CanvasText)); + font-size: var(--ak-font-size-xs, 0.75rem); text-overflow: ellipsis; white-space: nowrap; } @media (max-width: 47.999rem) { + .domain-summary-strip:not(.domain-inventory-summary) .domain-summary-items { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .domain-summary-strip:not(.domain-inventory-summary) .domain-summary-item { + width: 100%; + } + + .domain-inventory-summary .domain-summary-item { + flex-basis: 100%; + } + + .schedule-evidence-metadata dl { + grid-template-columns: minmax(0, 1fr); + } + + .queue-timeline-item { + flex-direction: column; + align-items: stretch; + } + + .queue-timeline-metadata { + flex-wrap: wrap; + } + + .queue-timeline-time { + width: 100%; + margin-inline-start: 0; + } + .metrics-table-wrap { max-width: 100%; } @@ -798,11 +950,13 @@ max-width: 12rem; } - .session-table-desktop { - display: none; + .session-table-cell-wrap { + display: block; + max-width: 100%; } - .session-table-mobile { - display: block; + .session-list-metadata { + display: grid; + grid-template-columns: minmax(0, 1fr); } } diff --git a/ui/src/styles/forms.css b/ui/src/styles/forms.css index 57787988..40081fbb 100644 --- a/ui/src/styles/forms.css +++ b/ui/src/styles/forms.css @@ -9,6 +9,9 @@ } .auth-panel { + display: flex; + flex-direction: column; + gap: var(--ak-space-4, 1rem); inline-size: 100%; max-inline-size: 24rem; } @@ -34,7 +37,7 @@ .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); - gap: var(--fitz-section-gap); + gap: var(--ak-space-3, 0.75rem); } .metrics-filter { @@ -44,38 +47,16 @@ .metrics-toolbar { display: grid; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .metrics-shortcuts { display: flex; flex-wrap: wrap; - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); align-items: center; } .metrics-shortcuts > [data-slot="button"] { white-space: nowrap; } - -.dialog-overlay { - position: fixed; - inset: 0; - background: color-mix(in srgb, var(--ak-color-bg, Canvas) 28%, CanvasText); -} - -.dialog-content { - position: fixed; - left: 50%; - top: 50%; - width: min(92vw, 30rem); - display: flex; - flex-direction: column; - gap: var(--fitz-section-gap); - padding: var(--fitz-panel-padding); - border: 1px solid var(--fitz-border); - border-radius: var(--fitz-card-radius); - background: var(--ak-color-surface, Canvas); - transform: translate(-50%, -50%); - box-shadow: var(--ak-shadow-4, 0 1rem 3rem color-mix(in srgb, CanvasText 20%, transparent)); -} diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index c57248d3..c799ba43 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -1,8 +1,10 @@ .domain-page-frame { - padding-block-start: var(--fitz-page-padding-block); + padding-block-start: var(--ak-space-3, 0.75rem); } .page-frame-main { + display: flex; + flex-direction: column; inline-size: 100%; max-inline-size: 100%; min-width: 0; diff --git a/ui/src/styles/responsive.css b/ui/src/styles/responsive.css index 2b2c2021..16004795 100644 --- a/ui/src/styles/responsive.css +++ b/ui/src/styles/responsive.css @@ -1,6 +1,6 @@ @media (max-width: 47.999rem) { :where([data-slot="navbar"][data-collapse-at] [data-slot="navbar-content"]) { - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); padding: var(--ak-space-sm, 0.625rem); max-block-size: calc( 100dvh - var(--ak-layout-navbar-height) - var(--ak-space-sm) - env(safe-area-inset-bottom) @@ -9,36 +9,27 @@ } :where([data-slot="navbar"][data-collapse-at] [data-slot="navbar-toggle"]) { - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } :where( [data-slot="navbar"][data-collapse-at] [data-slot="navbar-content"] [data-slot="nav-group-body"] ) { - gap: var(--fitz-control-gap); + gap: var(--ak-space-2, 0.5rem); } .domain-header, - .domain-section-header, - .dashboard-status-summary { + .domain-section-header { align-items: stretch; flex-direction: column; } - .domain-header-actions { - align-items: flex-start; - } - - .domain-header-actions > button { - align-self: start; - } - .domain-header h1 { font-size: 1.375rem; } .domain-section-header h2 { - font-size: var(--fitz-section-title-size); + font-size: var(--ak-font-size-md, 1rem); } .overview-status-band, diff --git a/ui/src/styles/shell.css b/ui/src/styles/shell.css index 0756a755..d7fd1eb7 100644 --- a/ui/src/styles/shell.css +++ b/ui/src/styles/shell.css @@ -20,7 +20,7 @@ inset-block-start: calc(var(--ak-layout-navbar-height, 3.75rem) + 1px); align-self: start; block-size: calc(100dvh - var(--ak-layout-navbar-height, 3.75rem) - 1px); - background: var(--fitz-page-background); + background: var(--ak-color-bg, Canvas); } .operator-sidebar-toggle { @@ -55,9 +55,10 @@ margin: 0; padding: 0; overflow: hidden; - border: 1px solid var(--fitz-border-subtle); + border: 1px solid + var(--ak-color-border-subtle, var(--ak-color-border, var(--ak-color-text, CanvasText))); border-radius: var(--ak-radius-3, 0.75rem); - background: var(--fitz-surface); + background: var(--ak-color-surface, Canvas); list-style: none; } @@ -68,9 +69,12 @@ .route-family-list > li + li::before { position: absolute; inset-block-start: 0; - inset-inline: var(--ak-space-4, 1rem) 0; + inset-inline: var(--ak-space-4, 1rem); block-size: 1px; - background: var(--fitz-border-subtle); + background: var( + --ak-color-border-subtle, + var(--ak-color-border, var(--ak-color-text, CanvasText)) + ); content: ""; } @@ -128,15 +132,16 @@ } .route-family-list-link:hover { - background: var(--fitz-surface-muted); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .route-family-list-link:active { - background: var(--fitz-surface-soft); + background: var(--ak-color-surface-muted, var(--ak-color-surface, Canvas)); } .route-family-list-link:focus-visible { - outline: 2px solid var(--fitz-focus-ring); + outline: 2px solid + var(--ak-color-focus-ring, var(--ak-color-accent, var(--ak-color-text, CanvasText))); outline-offset: -2px; } @@ -152,7 +157,7 @@ .operator-breadcrumbs [data-slot="breadcrumb-list"] { flex-wrap: wrap; row-gap: var(--ak-space-1, 0.25rem); - font-size: var(--fitz-body-size); + font-size: var(--ak-font-size-sm, 0.875rem); } .operator-breadcrumbs [data-slot="breadcrumb-page"], @@ -200,3 +205,7 @@ grid-template-columns: 13rem minmax(0, 1fr); } } +.operator-context-root { + display: flex; + flex-direction: column; +} diff --git a/ui/tests/app.test.tsx b/ui/tests/app.test.tsx index afa82ea9..03795efd 100644 --- a/ui/tests/app.test.tsx +++ b/ui/tests/app.test.tsx @@ -98,13 +98,6 @@ describe("Admin UI", () => { expect(typeof MetricsPage).toBe("function"); }); - it("defines the admin home page", () => { - expect(Home).toBeDefined(); - expect(typeof Home).toBe("function"); - }); - - it("defines the queue dead-letter sample component", () => {}); - it("defines the shared domain primitives", () => { expect(DomainHeader).toBeDefined(); expect(typeof DomainHeader).toBe("function"); diff --git a/ui/tests/e2e/shell-dashboard.spec.ts b/ui/tests/e2e/shell-dashboard.spec.ts index 23655bf1..56c60758 100644 --- a/ui/tests/e2e/shell-dashboard.spec.ts +++ b/ui/tests/e2e/shell-dashboard.spec.ts @@ -6,34 +6,6 @@ import { } from "./shell/api-fixtures"; import { openDashboard } from "./shell/chrome"; -test("captures the desktop dashboard shell", async ({ page }, testInfo) => { - await page.setViewportSize({ width: 1440, height: 1200 }); - await openDashboard(page); - - await expect(page.getByRole("heading", { name: "Fitz status" })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Issues" })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Domain health" })).toBeVisible(); - await page.screenshot({ - fullPage: true, - path: testInfo.outputPath("dashboard-desktop.png"), - animations: "disabled", - }); -}); - -test("captures the tablet dashboard shell", async ({ page }, testInfo) => { - await page.setViewportSize({ width: 1024, height: 1200 }); - await openDashboard(page); - - await expect(page.getByRole("heading", { name: "Fitz status" })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Issues" })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Domain health" })).toBeVisible(); - await page.screenshot({ - fullPage: true, - path: testInfo.outputPath("dashboard-tablet.png"), - animations: "disabled", - }); -}); - test("uses mobile below 48rem and desktop at 48rem", async ({ page }) => { await page.setViewportSize({ width: 767, height: 900 }); await openDashboard(page); @@ -94,19 +66,6 @@ test("renders unavailable base and nested route families as SPA 404 pages", asyn } }); -test("captures the desktop dashboard shell in dark mode", async ({ page }, testInfo) => { - await page.setViewportSize({ width: 1440, height: 1200 }); - await openDashboard(page, "dark"); - - await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); - await expect(page.getByRole("heading", { name: "Fitz status" })).toBeVisible(); - await page.screenshot({ - fullPage: true, - path: testInfo.outputPath("dashboard-dark.png"), - animations: "disabled", - }); -}); - test("captures the dashboard refreshing state", async ({ page }, testInfo) => { await page.setViewportSize({ width: 1440, height: 1200 }); diff --git a/ui/tests/e2e/shell-domains.spec.ts b/ui/tests/e2e/shell-domains.spec.ts index 95bc9944..f6032404 100644 --- a/ui/tests/e2e/shell-domains.spec.ts +++ b/ui/tests/e2e/shell-domains.spec.ts @@ -10,14 +10,14 @@ import { mockScheduleResourceApis, } from "./shell/resource-mocks"; -async function expectDomainStatsOnSingleRow(page: Page) { - const cards = page.locator('.domain-stat-grid > [data-slot="card"]'); - await expect(cards).toHaveCount(3); - const statTops = await cards.evaluateAll((elements) => - elements.map((card) => Math.round(card.getBoundingClientRect().top)), +async function expectDomainSummary(page: Page, mobile: boolean) { + const stats = page.locator('.domain-inventory-summary [data-slot="stat"]'); + await expect(stats).toHaveCount(3); + const statTops = await stats.evaluateAll((elements) => + elements.map((stat) => Math.round(stat.getBoundingClientRect().top)), ); expect(statTops).toHaveLength(3); - expect(new Set(statTops).size).toBe(1); + expect(new Set(statTops).size).toBe(mobile ? 3 : 1); } const rollupHeaders: Record = { @@ -37,7 +37,9 @@ async function expectInventoryRollups(page: Page, domain: string, mobile: boolea } await page.getByRole("button", { name: new RegExp(`Sort by ${headers[0]}`) }).click(); await expect( - page.getByRole("button", { name: new RegExp(`Sort by ${headers[0]}, descending`) }), + page.getByRole("button", { + name: new RegExp(`Sort by ${headers[0]}, descending`), + }), ).toBeVisible(); const scrollHint = page.getByText("Scroll horizontally to view every metric."); if (mobile) { @@ -46,7 +48,7 @@ async function expectInventoryRollups(page: Page, domain: string, mobile: boolea await expect(scrollHint).toBeHidden(); } if (mobile) { - const table = page.locator(".domain-resource-virtual-table"); + const table = page.locator(".domain-resource-data-table"); expect(await table.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(true); } } @@ -59,10 +61,11 @@ test("captures a domain inventory page", async ({ page }, testInfo) => { await expect(page.locator("main#main-content")).toHaveCount(1); await expect(page.locator(".page-frame-sidebar")).toHaveCount(0); await expect(page.getByRole("heading", { name: /Queue inventory/ })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Resource inventory" })).toBeVisible(); - await expect(page.getByRole("link", { name: "queue://default/ops/primary" })).toHaveAttribute( + await expect(page.getByRole("heading", { name: "Realms" })).toBeVisible(); + await expect(page.getByRole("table", { name: "Realms" })).toBeVisible(); + await expect(page.getByRole("link", { name: "default" })).toHaveAttribute( "href", - "/admin/1/queue/default/ops/primary", + "/admin/1/queue/default", ); await page.screenshot({ fullPage: true, @@ -81,12 +84,16 @@ test("omits comparison controls from queue resource inspection", async ({ page } await mockQueueResourceApis(page, queueScope); await page.goto("/admin/1/queue/acme/payments/orders"); - await expect( - page.getByRole("heading", { level: 1, name: "Queue resource inspection" }), - ).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "orders" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Compare scopes" })).toHaveCount(0); await expect(page.locator("#compare-realm, #compare-family")).toHaveCount(0); await expect(page.getByRole("heading", { level: 2, name: "Dead letters" })).toBeVisible(); + await expect(page.getByRole("table", { name: "Dead-letter queue messages" })).toBeVisible(); + await expect(page.getByRole("table", { name: "Inflight queue messages" })).toBeVisible(); + const timeline = page.getByRole("list", { name: "Queue resource timeline" }); + await expect(timeline).toBeVisible(); + await expect(timeline.getByRole("listitem")).toHaveCount(1); + await expect(page.locator("#queue-timeline table")).toHaveCount(0); }); test("captures lease overview empty state", async ({ page }, testInfo) => { @@ -120,15 +127,24 @@ test("navigates lease scope drill-down links and shows ownership countdown updat await mockResourceDetailApis(page, "lease", leaseScope); await page.goto("/admin/1/lease"); + await page.locator('a[href="/admin/1/lease/default"]').click(); + await expect(page).toHaveURL("/admin/1/lease/default"); + await page.locator('a[href="/admin/1/lease/default/default"]').click(); + await expect(page).toHaveURL("/admin/1/lease/default/default"); await page.locator('a[href="/admin/1/lease/default/default/primary"]').click(); await expect(page).toHaveURL("/admin/1/lease/default/default/primary"); - await expect(page.getByRole("heading", { name: "primary" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "primary", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Back to area" })).toHaveCount(0); await expect(page.getByRole("navigation", { name: "Resource hierarchy" })).toContainText( "primary", ); - const remainingCell = page.locator("table tbody tr td").nth(5); + const ownershipDetails = page.getByRole("region", { + name: "Ownership details", + }); + await expect(ownershipDetails).toBeVisible(); + await expect(page.getByRole("table", { name: "Lease ownership rows" })).toHaveCount(0); + const remainingCell = ownershipDetails.locator("[data-field='remaining-ttl']"); const initialRemaining = (await remainingCell.textContent())?.trim(); await page.waitForTimeout(1200); const updatedRemaining = (await remainingCell.textContent())?.trim(); @@ -144,20 +160,28 @@ test("navigates notice scope drill-down links to operation detail", async ({ pag await mockDomainOverviewApis(page); await page.goto("/admin/1/notice"); + await page.locator('a[href="/admin/1/notice/default"]').click(); + await page.locator('a[href="/admin/1/notice/default/default"]').click(); await page.locator('a[href="/admin/1/notice/default/default/primary"]').click(); await expect(page).toHaveURL("/admin/1/notice/default/default/primary"); - await expect(page.getByRole("heading", { level: 1, name: "Notice operations" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "primary" })).toBeVisible(); + const operations = page.getByRole("list", { name: "Notice operations" }); + await expect(operations.getByRole("listitem")).toHaveCount(2); + await expect(page.locator("#notice-resource-operations table")).toHaveCount(0); await page.locator('a[href="/admin/1/notice/default/default/primary/GetStatus"]').click(); await expect(page).toHaveURL("/admin/1/notice/default/default/primary/GetStatus"); await expect(page.getByRole("heading", { level: 1, name: "GetStatus" })).toBeVisible(); await expect(page.getByRole("heading", { level: 2, name: "Delivery evidence" })).toBeVisible(); - await expect(page.getByRole("columnheader", { name: "Notifications observed" })).toBeVisible(); + const deliveries = page.getByRole("list", { name: "Delivery evidence" }); + await expect(deliveries.getByRole("listitem")).toHaveCount(1); + await expect(deliveries).toContainText("Notifications observed"); + await expect(page.locator("#notice-delivery-evidence table")).toHaveCount(0); await expect(page.getByRole("navigation", { name: "Resource hierarchy" })).toContainText( "primary", ); }); -test("renders component-returned notice rows as aligned direct table rows", async ({ page }) => { +test("renders notice delivery evidence as semantic list items", async ({ page }) => { await page.setViewportSize({ width: 1440, height: 1200 }); await mockDomainOverviewApis(page); await page.goto("/admin/1/notice/acme/payments/orders/RefreshProjection"); @@ -165,18 +189,12 @@ test("renders component-returned notice rows as aligned direct table rows", asyn await expect(page.getByRole("heading", { level: 1, name: "RefreshProjection" })).toBeVisible(); await expect(page.getByRole("heading", { level: 2, name: "Delivery evidence" })).toBeVisible(); - const table = page.locator(".notice-operation-table-wrap table"); - await expect(table.locator(":scope > tbody > tr")).toHaveCount(1); - await expect(table.locator(":scope > tbody > div")).toHaveCount(0); - - const headerStarts = await table - .locator("thead th") - .evaluateAll((cells) => cells.map((cell) => Math.round(cell.getBoundingClientRect().left))); - const bodyStarts = await table - .locator("tbody > tr:first-child > td") - .evaluateAll((cells) => cells.map((cell) => Math.round(cell.getBoundingClientRect().left))); - - expect(bodyStarts).toEqual(headerStarts); + const deliveries = page.getByRole("list", { name: "Delivery evidence" }); + await expect(deliveries.getByRole("listitem")).toHaveCount(1); + await expect(deliveries).toContainText("session-1"); + await expect(deliveries).toContainText("Notifications observed: 12"); + await expect(deliveries.getByLabel("Status: open")).toBeVisible(); + await expect(page.locator("#notice-delivery-evidence table")).toHaveCount(0); }); test("navigates schedule scope drill-down links to resource detail", async ({ page }) => { @@ -192,14 +210,26 @@ test("navigates schedule scope drill-down links to resource detail", async ({ pa await page.goto("/admin/1/schedule"); await expect(page.getByRole("heading", { name: /Schedule inventory/ })).toBeVisible(); + await page.locator('a[href="/admin/1/schedule/default"]').click(); + await page.locator('a[href="/admin/1/schedule/default/default"]').click(); await page.locator('a[href="/admin/1/schedule/default/default/primary"]').click(); await expect(page).toHaveURL("/admin/1/schedule/default/default/primary"); - await expect(page.getByRole("heading", { name: "Schedule resource inspection" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "primary" })).toBeVisible(); await expect(page.getByText("Non-authoritative; not downstream execution history")).toBeVisible(); await expect( page.getByRole("heading", { name: "Acknowledged handoff observations" }), ).toBeVisible(); await expect(page.getByRole("heading", { name: "Pending and missed handoffs" })).toBeVisible(); + const acknowledged = page.getByRole("list", { + name: "Acknowledged handoff observations", + }); + const pending = page.getByRole("list", { + name: "Pending and missed handoffs", + }); + await expect(acknowledged.getByRole("listitem")).toHaveCount(1); + await expect(pending.getByRole("listitem")).toHaveCount(1); + await expect(page.locator("#schedule-acknowledged-handoffs table")).toHaveCount(0); + await expect(page.locator("#schedule-pending-handoffs table")).toHaveCount(0); }); test("captures kv overview empty state", async ({ page }, testInfo) => { @@ -362,8 +392,8 @@ test.describe("captures domain overview templates", () => { await expect(page.getByRole("heading", { name: overviewPage.heading })).toBeVisible(); await expect(page.locator("main#main-content")).toHaveCount(1); - await expectDomainStatsOnSingleRow(page); - await expectInventoryRollups(page, overviewPage.domain, false); + await expect(page.getByRole("table", { name: "Realms" })).toBeVisible(); + await expectDomainSummary(page, false); expect(detailRequests).toEqual([]); await page.screenshot({ @@ -384,8 +414,8 @@ test.describe("captures domain overview templates", () => { await expect(page.getByRole("heading", { name: overviewPage.heading })).toBeVisible(); await expect(page.locator("main#main-content")).toHaveCount(1); - await expectDomainStatsOnSingleRow(page); - await expectInventoryRollups(page, overviewPage.domain, true); + await expect(page.getByRole("table", { name: "Realms" })).toBeVisible(); + await expectDomainSummary(page, true); expect(detailRequests).toEqual([]); await page.screenshot({ @@ -396,3 +426,30 @@ test.describe("captures domain overview templates", () => { }); } }); + +test.describe("progressive domain drilldown", () => { + for (const overviewPage of domainOverviewPages) { + test(`drills ${overviewPage.domain} through realm and area`, async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1200 }); + await mockDomainOverviewApis(page); + await page.goto(overviewPage.path); + + const realmTable = page.getByRole("table", { name: "Realms" }); + const realmLink = realmTable.getByRole("link").first(); + const realmHref = await realmLink.getAttribute("href"); + expect(realmHref).toBeTruthy(); + await realmLink.click(); + await expect(page).toHaveURL(realmHref ?? ""); + + const areaTable = page.getByRole("table", { name: "Areas" }); + const areaLink = areaTable.getByRole("link").first(); + const areaHref = await areaLink.getAttribute("href"); + expect(areaHref).toBeTruthy(); + await areaLink.click(); + await expect(page).toHaveURL(areaHref ?? ""); + + await expect(page.getByRole("table", { name: "Resource inventory" })).toBeVisible(); + await expectInventoryRollups(page, overviewPage.domain, false); + }); + } +}); diff --git a/ui/tests/e2e/shell-sessions-metrics.spec.ts b/ui/tests/e2e/shell-sessions-metrics.spec.ts index 13a6b04c..17c10736 100644 --- a/ui/tests/e2e/shell-sessions-metrics.spec.ts +++ b/ui/tests/e2e/shell-sessions-metrics.spec.ts @@ -7,24 +7,6 @@ import { } from "./shell/api-fixtures"; import { openDashboard } from "./shell/chrome"; -test("captures the mobile navbar panel", async ({ page }, testInfo) => { - await page.setViewportSize({ width: 390, height: 844 }); - await openDashboard(page); - const primaryNav = page.getByRole("navigation", { name: "Primary navigation" }); - - await primaryNav.getByRole("button", { name: /Menu|Navigation/ }).click(); - await expect(primaryNav.getByRole("link", { name: "Overview" })).toBeVisible(); - await expect(primaryNav.getByRole("link", { name: "Diagnostics" })).toBeVisible(); - await expect(primaryNav.getByRole("link", { name: "Metrics" })).toBeVisible(); - await expect(primaryNav.getByRole("link", { name: "Queue" })).toBeVisible(); - - await page.screenshot({ - fullPage: true, - path: testInfo.outputPath("mobile-nav-open.png"), - animations: "disabled", - }); -}); - test("operates the mobile navigation disclosure from the keyboard", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await openDashboard(page); @@ -48,7 +30,7 @@ test("captures sessions data state", async ({ page }, testInfo) => { await page.goto("/admin/1/sessions"); await expect(page.getByRole("heading", { name: "Active sessions", exact: true })).toBeVisible(); - await expect(page.locator("table tbody tr").first()).toBeVisible(); + await expect(page.locator("ul.session-list li").first()).toBeVisible(); await page.screenshot({ fullPage: true, @@ -80,7 +62,7 @@ test("captures sessions on mobile", async ({ page }, testInfo) => { await page.goto("/admin/1/sessions"); await expect(page.getByRole("heading", { name: "Active sessions", exact: true })).toBeVisible(); - await expect(page.locator("ul.session-mobile-list li").first()).toBeVisible(); + await expect(page.locator("ul.session-list li").first()).toBeVisible(); await page.screenshot({ fullPage: true, @@ -141,22 +123,3 @@ test("captures metrics on mobile", async ({ page }, testInfo) => { animations: "disabled", }); }); - -test("captures metrics in dark mode", async ({ page }, testInfo) => { - await page.setViewportSize({ width: 1440, height: 1200 }); - await mockMetricsApi(page); - await page.addInitScript(() => { - localStorage.setItem("fitz-admin-theme", "dark"); - }); - - await page.goto("/admin/1/metrics"); - - await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); - await expect(page.getByRole("heading", { name: "Metrics explorer" })).toBeVisible(); - - await page.screenshot({ - fullPage: true, - path: testInfo.outputPath("metrics-dark.png"), - animations: "disabled", - }); -}); diff --git a/ui/tests/e2e/shell/chrome.ts b/ui/tests/e2e/shell/chrome.ts index 8267d521..f4e2adf8 100644 --- a/ui/tests/e2e/shell/chrome.ts +++ b/ui/tests/e2e/shell/chrome.ts @@ -183,9 +183,7 @@ export async function expectNoHorizontalOverflow(page: Page) { export async function expectReachableScrollableTables(page: Page) { const unreachable = await page.evaluate(() => { const surfaces = Array.from( - document.querySelectorAll( - '.domain-table-wrap, [data-slot="virtual-table"], [data-slot="table-container"]', - ), + document.querySelectorAll('.domain-table-wrap, [data-slot="table-container"]'), ); return surfaces.filter((surface) => { @@ -311,13 +309,13 @@ export const sprint16Routes: RouteScenario[] = [ path: "/admin/1/lease/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Lease inventory", + title: "default", }, { path: "/admin/1/lease/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Lease inventory", + title: "ops", }, { path: "/admin/1/notice", @@ -329,19 +327,19 @@ export const sprint16Routes: RouteScenario[] = [ path: "/admin/1/notice/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Notice inventory", + title: "default", }, { path: "/admin/1/notice/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Notice inventory", + title: "ops", }, { path: "/admin/1/notice/default/ops/primary", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Notice operations", + title: "primary", }, { path: "/admin/1/notice/default/ops/primary/GetStatus", @@ -365,13 +363,13 @@ export const sprint16Routes: RouteScenario[] = [ path: "/admin/1/schedule/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Schedule inventory", + title: "default", }, { path: "/admin/1/schedule/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Schedule inventory", + title: "ops", }, { path: "/admin/1/queue", @@ -383,13 +381,13 @@ export const sprint16Routes: RouteScenario[] = [ path: "/admin/1/queue/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Queue inventory", + title: "default", }, { path: "/admin/1/queue/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Queue inventory", + title: "ops", }, { path: "/admin/1/stream", @@ -407,44 +405,44 @@ export const sprint16Routes: RouteScenario[] = [ path: "/admin/1/kv/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "KV tables", + title: "default", }, { path: "/admin/1/kv/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "KV tables", + title: "ops", }, { path: "/admin/1/rpc/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "RPC inventory", + title: "default", }, { path: "/admin/1/rpc/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "RPC inventory", + title: "ops", }, { path: "/admin/1/stream/default", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Stream inventory", + title: "default", }, { path: "/admin/1/stream/default/ops", shell: "app", setup: (page) => mockDomainOverviewApis(page), - title: "Stream inventory", + title: "ops", }, { path: "/admin/1/queue/default/ops/primary", shell: "app", setup: (page) => mockQueueResourceApis(page, parseRouteResourceScope("/admin/1/queue/default/ops/primary")), - title: "Queue resource inspection", + title: "primary", }, { path: "/admin/1/kv/default/ops/primary", @@ -499,7 +497,7 @@ export const sprint16Routes: RouteScenario[] = [ page, parseRouteResourceScope("/admin/1/schedule/default/ops/primary"), ), - title: "Schedule resource inspection", + title: "primary", }, { path: "/admin/1/stream/default/ops/primary", diff --git a/ui/tests/page-smoke-dashboard.test.tsx b/ui/tests/page-smoke-dashboard.test.tsx index 172c8195..05166a51 100644 --- a/ui/tests/page-smoke-dashboard.test.tsx +++ b/ui/tests/page-smoke-dashboard.test.tsx @@ -443,10 +443,9 @@ describe("admin page smoke tests", () => { expect(root.textContent).toContain("Refreshing"); expect(root.textContent).toContain("Queue inventory"); - expect(root.textContent).toContain("Resource inventory"); - expect(root.textContent).toContain("queue://default/ops/primary"); + expect(root.textContent).toContain("Realms"); expect(root.textContent).not.toContain("messages are visible"); expect(root.textContent).not.toContain("Activity alone does not establish pressure"); - expect(root.querySelector('a[href="/admin/1/queue/default/ops/primary"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/queue/default"]')).toBeTruthy(); }); }); diff --git a/ui/tests/page-smoke-domains.test.tsx b/ui/tests/page-smoke-domains.test.tsx index 75632d22..776882bd 100644 --- a/ui/tests/page-smoke-domains.test.tsx +++ b/ui/tests/page-smoke-domains.test.tsx @@ -62,18 +62,17 @@ describe("admin page smoke tests", () => { const text = root.textContent ?? ""; expect(text).toContain("Lease inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("lease://default/ops/primary"); + expect(text).toContain("Realms"); expect(text).toContain("1h"); expect(text).toContain("Pressure"); expect(text).not.toContain("acquire timeout"); expect(text).not.toContain("Historical totals do not identify a current incident"); expect(text).not.toContain("Broker-local owners"); - expect(root.querySelector('a[href="/admin/1/lease/default/ops/primary"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/lease/default"]')).toBeTruthy(); }); it("renders kv tables with inventory stats and explorer links", async () => { const { default: KvPage } = await import("@/pages/app/kv"); - const root = await mountRoute("/kv", "/kv", KvPage); + const root = await mountRoute("/kv/default/ops", "/kv/{realm}/{area}", KvPage); const text = root.textContent ?? ""; const labels = ["Route", "Records", "Storage", "Txns", "Read p95 ms", "Write p95 ms"]; @@ -84,7 +83,7 @@ describe("admin page smoke tests", () => { cursor = index; } - expect(text).toContain("KV tables"); + expect(text).toContain("KV area"); expect(text).toContain("kv://default/ops/primary"); expect(text).toContain("300"); expect(text).toContain("16.0 KiB"); @@ -117,10 +116,9 @@ describe("admin page smoke tests", () => { ); const { default: KvPage } = await import("@/pages/app/kv"); - const root = await mountRoute("/kv", "/kv", KvPage); - const headers = Array.from( - root.querySelectorAll("[data-slot='virtual-table-header-cell']"), - (header) => header.textContent?.trim(), + const root = await mountRoute("/kv/default/ops", "/kv/{realm}/{area}", KvPage); + const headers = Array.from(root.querySelectorAll("[data-slot='table-header-cell']"), (header) => + header.textContent?.trim(), ); expect(headers).toEqual(["Route"]); @@ -292,10 +290,12 @@ describe("admin page smoke tests", () => { "/admin/{family}/lease/{realm}/{area}/{resource}", LeaseResourcePage, ); - const initialRemaining = root - .querySelector("tbody tr") - ?.querySelectorAll("td")[5] + const ownershipCard = root.querySelector(".lease-ownership-card"); + const initialRemaining = ownershipCard + ?.querySelector("[data-field='remaining-ttl']") ?.textContent?.trim(); + expect(ownershipCard).toBeTruthy(); + expect(root.querySelector('table[aria-label="Lease ownership rows"]')).toBeNull(); expect(initialRemaining).toBeTruthy(); expect(root.textContent).not.toContain("not crash-safe continuity"); }); @@ -321,14 +321,13 @@ describe("admin page smoke tests", () => { const text = root.textContent ?? ""; expect(text).toContain("Notice inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("notice://default/ops/primary"); + expect(text).toContain("Realms"); expect(text).toContain("Live"); expect(text).not.toContain("2 delivery drop"); expect(text).not.toContain("1 wildcard reject"); expect(text).not.toContain("Historical totals do not identify a current fanout incident"); expect(text).not.toContain("Communication flow"); - expect(root.querySelector('a[href="/admin/1/notice/default/ops/primary"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/notice/default"]')).toBeTruthy(); }); it("renders Notice resource publish rates without an unavailable latency column", async () => { const { default: NoticePage } = await import("@/pages/app/notice"); @@ -339,10 +338,15 @@ describe("admin page smoke tests", () => { ); const text = root.textContent ?? ""; - expect(text).toContain("Notice operations"); + expect(root.querySelector(".domain-header-title-row > span:first-child")?.textContent).toBe( + "primary", + ); expect(text).toContain("Publishes / min"); expect(text).not.toContain("Latency"); expect(text).not.toContain("--/N/A"); + const operations = root.querySelector('ul[aria-label="Notice operations"]'); + expect(operations?.querySelectorAll('[data-slot="item"]')).toHaveLength(1); + expect(root.querySelector("#notice-resource-operations [data-slot='table']")).toBeNull(); }); it("renders notice operation metrics and delivery evidence", async () => { const { default: NoticeOperationPage } = await import("@/pages/app/notice-operation"); @@ -363,7 +367,9 @@ describe("admin page smoke tests", () => { expect(text).toContain("does not report a reset scope"); expect(text).toContain("session-1"); expect(text).toContain("session-2"); - expect(root.querySelector(".notice-operation-table-wrap [data-slot='table']")).toBeTruthy(); + const deliveries = root.querySelector('ul[aria-label="Delivery evidence"]'); + expect(deliveries?.querySelectorAll('[data-slot="item"]')).toHaveLength(2); + expect(root.querySelector("#notice-delivery-evidence [data-slot='table']")).toBeNull(); }); it("renders schedule health in the inventory header", async () => { mocks.queryStates.schedule = queryState.fresh( @@ -390,22 +396,21 @@ describe("admin page smoke tests", () => { const text = root.textContent ?? ""; expect(text).toContain("Schedule inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("schedule://default/ops/primary"); + expect(text).toContain("Realms"); expect(text).toContain("Pressure"); expect(text).not.toContain("Schedule does not imply durable downstream delivery."); expect(text).not.toContain( "Cumulative failures describe process history, not a current incident.", ); expect(text).not.toContain("Schedule realms"); - expect(root.querySelector('a[href="/admin/1/schedule/default/ops/primary"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/schedule/default"]')).toBeTruthy(); }); it("renders schedule hierarchy routes and resource drill-down pages", async () => { const { default: SchedulePage } = await import("@/pages/app/schedule"); const realmRoot = await mountRoute("/schedule/default", "/schedule/{realm}", SchedulePage); - expect(realmRoot.textContent).toContain("Schedule inventory"); - expect(realmRoot.textContent).toContain("Resource inventory"); - expect(realmRoot.textContent).toContain("schedule://default/ops/primary"); + expect(realmRoot.textContent).toContain("Schedule realm"); + expect(realmRoot.textContent).toContain("Areas"); + expect(realmRoot.querySelector('a[href="/admin/1/schedule/default/ops"]')).toBeTruthy(); cleanupApp(realmRoot); document.body.innerHTML = ""; @@ -414,7 +419,7 @@ describe("admin page smoke tests", () => { "/schedule/{realm}/{area}", SchedulePage, ); - expect(areaRoot.textContent).toContain("Schedule inventory"); + expect(areaRoot.textContent).toContain("Schedule area"); expect(areaRoot.textContent).toContain("Resource inventory"); expect(areaRoot.textContent).toContain("schedule://default/ops/primary"); cleanupApp(areaRoot); @@ -428,7 +433,9 @@ describe("admin page smoke tests", () => { ); const text = resourceRoot.textContent ?? ""; - expect(text).toContain("Schedule resource inspection"); + expect( + resourceRoot.querySelector(".domain-header-title-row > span:first-child")?.textContent, + ).toBe("primary"); expect(text).toContain("Handoff evidence"); expect(text).toContain("Scheduled run"); expect(text).not.toContain("Next run"); @@ -439,6 +446,16 @@ describe("admin page smoke tests", () => { expect(text).not.toContain("Is anyone listening?"); expect(text).not.toContain("No live listeners visible"); expect(text).not.toContain("Back to schedule area"); + const acknowledged = resourceRoot.querySelector( + 'ul[aria-label="Acknowledged handoff observations"]', + ); + const pending = resourceRoot.querySelector('ul[aria-label="Pending and missed handoffs"]'); + expect(acknowledged?.querySelectorAll('[data-slot="item"]')).toHaveLength(1); + expect(pending?.querySelectorAll('[data-slot="item"]')).toHaveLength(1); + expect( + resourceRoot.querySelector("#schedule-acknowledged-handoffs [data-slot='table']"), + ).toBeNull(); + expect(resourceRoot.querySelector("#schedule-pending-handoffs [data-slot='table']")).toBeNull(); }); it("describes future, overdue, missing, and invalid schedule timestamps truthfully", async () => { @@ -477,15 +494,14 @@ describe("admin page smoke tests", () => { const text = root.textContent ?? ""; expect(text).toContain("Stream inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("stream://default/ops/primary"); + expect(text).toContain("Realms"); expect(text).not.toContain("100+ behind"); expect(text).toContain("Attention"); expect(text).toContain("Committed events"); expect(text).not.toContain("4,200 committed event"); expect(text).not.toContain("live subscriptions"); expect(text).not.toContain("Stream metrics"); - expect(root.querySelector('a[href="/admin/1/stream/default/ops/primary"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/stream/default"]')).toBeTruthy(); }); it("does not label unavailable detail queries as live", async () => { const cases = [ @@ -551,13 +567,12 @@ describe("admin page smoke tests", () => { let root = await mountRoute("/stream/default", "/stream/{realm}", StreamPage); let text = root.textContent ?? ""; - expect(text).toContain("Stream inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("stream://default/ops/primary"); + expect(text).toContain("Stream realm"); + expect(text).toContain("Areas"); root = await mountRoute("/stream/default/ops", "/stream/{realm}/{area}", StreamPage); text = root.textContent ?? ""; - expect(text).toContain("Stream inventory"); + expect(text).toContain("Stream area"); expect(text).toContain("Resource inventory"); expect(text).toContain("stream://default/ops/primary"); @@ -569,9 +584,16 @@ describe("admin page smoke tests", () => { text = root.textContent ?? ""; expect(text).toContain("Stream resource"); expect(text).toContain("From offset"); - expect(text).toContain("Stream resource metrics"); - expect(text).toContain("stream://default/ops/events"); + expect(text).toContain("Committed metadata"); + expect(text).not.toContain("stream://default/ops/events"); expect(text).toContain('{"ok":true}'); + const recordsTable = root.querySelector('table[aria-label="Stream records"]'); + expect(recordsTable).toBeTruthy(); + expect( + Array.from(recordsTable?.querySelectorAll('[data-slot="table-header-cell"]') ?? []).map( + (header) => header.textContent?.trim(), + ), + ).toEqual(["Offset", "Created", "Body", "Action"]); }); it("renders rpc health in the inventory header", async () => { mocks.queryStates.rpc = queryState.fresh( @@ -594,14 +616,13 @@ describe("admin page smoke tests", () => { const text = root.textContent ?? ""; expect(text).toContain("RPC inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("rpc://default/ops/primary"); + expect(text).toContain("Realms"); expect(text).toContain("Pressure"); expect(text).not.toContain("not covered by a registered worker"); expect(text).not.toContain("Pending work is in-memory"); expect(text).not.toContain("pending requests"); expect(text).not.toContain("Communication flow"); - expect(root.querySelector('a[href="/admin/1/rpc/default/ops/primary"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/rpc/default"]')).toBeTruthy(); }); it("renders RPC hierarchy routes and operation pages", async () => { const { default: RpcPage } = await import("@/pages/app/rpc"); @@ -610,13 +631,12 @@ describe("admin page smoke tests", () => { let root = await mountRoute("/rpc/default", "/rpc/{realm}", RpcPage); let text = root.textContent ?? ""; - expect(text).toContain("RPC inventory"); - expect(text).toContain("Resource inventory"); - expect(text).toContain("rpc://default/ops/primary"); + expect(text).toContain("RPC realm"); + expect(text).toContain("Areas"); root = await mountRoute("/rpc/default/ops", "/rpc/{realm}/{area}", RpcPage); text = root.textContent ?? ""; - expect(text).toContain("RPC inventory"); + expect(text).toContain("RPC area"); expect(text).toContain("Resource inventory"); expect(text).toContain("rpc://default/ops/primary"); @@ -632,6 +652,9 @@ describe("admin page smoke tests", () => { expect(text).toContain("Requests handled"); expect(text).toContain("in-memory pending request evidence"); expect(text).toContain("GetStatus"); + const operations = root.querySelector('ul[aria-label="RPC operations"]'); + expect(operations?.querySelectorAll('[data-slot="item"]')).toHaveLength(1); + expect(root.querySelector("#rpc-resource-operations [data-slot='table']")).toBeNull(); root = await mountRoute( "/admin/1/rpc/default/ops/primary/GetStatus", @@ -647,6 +670,9 @@ describe("admin page smoke tests", () => { expect(text).toContain("Worker Registered"); expect(text).toContain("Observed handled total"); expect(text).toContain("does not report a reset window"); + const calls = root.querySelector('ul[aria-label="Live call evidence"]'); + expect(calls?.querySelectorAll('[data-slot="item"]')).toHaveLength(1); + expect(root.querySelector("#rpc-live-call-evidence [data-slot='table']")).toBeNull(); }); it("renders the status-first dashboard sections", async () => { const { default: Home } = await import("@/pages/app/home"); diff --git a/ui/tests/page-smoke-queue-auth.test.tsx b/ui/tests/page-smoke-queue-auth.test.tsx index 6bb84dd1..514d081e 100644 --- a/ui/tests/page-smoke-queue-auth.test.tsx +++ b/ui/tests/page-smoke-queue-auth.test.tsx @@ -7,7 +7,7 @@ import { queueInventory, queueResource } from "./page-smoke/fixtures"; const mocks = pageSmokeMocks(); describe("admin page smoke tests", () => { - it("renders queue resource links for overview, realm, and area routes", async () => { + it("renders progressive queue links for overview, realm, and area routes", async () => { const { default: QueuePage } = await import("@/pages/app/queue"); mocks.queryStates.queueInventory = queryState.fresh( { @@ -25,21 +25,17 @@ describe("admin page smoke tests", () => { let root = await mountRoute("/admin/1/queue", "/admin/{family}/queue", QueuePage); expect(root.textContent).toContain("Queue inventory"); - expect(root.querySelector('a[href="/admin/1/queue/default"]')).toBeNull(); - expect( - root.querySelector('a[href="/admin/1/queue/default/ops/primary"]')?.textContent, - ).toContain("queue://default/ops/primary"); - expect(root.textContent).toContain("queue://globex/support/tickets"); + expect(root.querySelector('a[href="/admin/1/queue/default"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/queue/globex"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/queue/default/ops/primary"]')).toBeNull(); cleanupApp(root); document.body.innerHTML = ""; root = await mountRoute("/admin/1/queue/default", "/admin/{family}/queue/{realm}", QueuePage); - expect(root.textContent).toContain("Queue inventory"); - expect(root.querySelector('a[href="/admin/1/queue/default/ops"]')).toBeNull(); - expect( - root.querySelector('a[href="/admin/1/queue/default/ops/primary"]')?.textContent, - ).toContain("queue://default/ops/primary"); + expect(root.textContent).toContain("Queue realm"); + expect(root.querySelector('a[href="/admin/1/queue/default/ops"]')).toBeTruthy(); + expect(root.querySelector('a[href="/admin/1/queue/default/ops/primary"]')).toBeNull(); expect(root.textContent).not.toContain("queue://globex/support/tickets"); cleanupApp(root); @@ -50,7 +46,7 @@ describe("admin page smoke tests", () => { "/admin/{family}/queue/{realm}/{area}", QueuePage, ); - expect(root.textContent).toContain("Queue inventory"); + expect(root.textContent).toContain("Queue area"); expect( root.querySelector('a[href="/admin/1/queue/default/ops/primary"]')?.textContent, ).toContain("queue://default/ops/primary"); @@ -94,6 +90,10 @@ describe("admin page smoke tests", () => { expect(root.textContent).toContain("Key preview"); expect(root.textContent).toContain("user:1"); expect(root.textContent).toContain("alice"); + const committedRows = root.querySelector('[aria-label="Committed KV rows"]'); + expect(committedRows?.querySelectorAll('button[aria-label="Copy value"]')).toHaveLength(1); + expect(committedRows?.querySelector('button[aria-label="Copy key"]')).toBeNull(); + expect(committedRows?.textContent).not.toContain("Copy value"); expect( root.querySelector('a[href="/admin/1/kv/default/ops/primary?startsWith=user%3A"]') ?.textContent, @@ -128,6 +128,83 @@ describe("admin page smoke tests", () => { ?.textContent, ).toContain("Previous page"); expect(root.querySelector('button[aria-label^="Copy body at offset"]')).toBeTruthy(); + expect(root.textContent).not.toContain("Copy body"); + const headers = Array.from( + root.querySelectorAll('table[aria-label="Stream records"] [data-slot="table-header-cell"]'), + ).map((header) => header.textContent?.trim()); + expect(headers).toEqual(["Offset", "Created", "Body", "Action"]); + }); + it("uses tables for queue message state and a list for timeline evidence", async () => { + mocks.queryStates.queueResource = queryState.fresh( + { + ...queueResource, + deadLetters: [ + { + area: "ops", + attempts: 2, + deadLetteredAt: "2026-05-21T13:05:00Z", + family: 1, + messageId: 42, + realm: "default", + reason: "handler failed", + resource: "primary", + }, + ], + inflight: [ + { + area: "ops", + attempts: 1, + expiresAt: "2026-05-21T13:06:00Z", + family: 1, + inflightToken: "token-1", + messageId: 41, + realm: "default", + resource: "primary", + sessionId: "session-1", + }, + ], + timeline: { + ...queueResource.timeline, + events: [ + { + ageSeconds: 2, + area: "ops", + attempts: 1, + correlationId: "correlation-1", + kind: "transition" as const, + messageId: 41, + observedAt: "2026-05-21T13:00:00Z", + operation: "Peek", + ownerSession: "session-1", + realm: "default", + resource: "primary", + summary: "Queue worker activity observed.", + workerSession: "worker-1", + }, + ], + }, + }, + queryOptions(), + ); + + const { default: QueueResourcePage } = await import("@/pages/app/queue-resource"); + const root = await mountRoute( + "/queue/default/ops/primary", + "/queue/{realm}/{area}/{resource}", + QueueResourcePage, + ); + + expect(root.querySelector('table[aria-label="Dead-letter queue messages"]')).toBeTruthy(); + expect(root.querySelector('table[aria-label="Inflight queue messages"]')).toBeTruthy(); + const queueHeaders = Array.from( + root.querySelectorAll( + 'table[aria-label="Dead-letter queue messages"] [data-slot="table-header-cell"], table[aria-label="Inflight queue messages"] [data-slot="table-header-cell"]', + ), + ).map((header) => header.textContent?.trim()); + expect(queueHeaders).not.toContain("Family"); + const timeline = root.querySelector('ul[aria-label="Queue resource timeline"]'); + expect(timeline?.querySelectorAll('[data-slot="item"]')).toHaveLength(1); + expect(root.querySelector("#queue-timeline [data-slot='table']")).toBeNull(); }); it("opens an accessible queue dead-letter confirmation dialog", async () => { const { default: QueueResourcePage } = await import("@/pages/app/queue-resource"); diff --git a/ui/tests/page-smoke.test.tsx b/ui/tests/page-smoke.test.tsx index 6ee07c6a..76383fcf 100644 --- a/ui/tests/page-smoke.test.tsx +++ b/ui/tests/page-smoke.test.tsx @@ -53,19 +53,19 @@ describe("admin page smoke tests", () => { routePath: "/queue", }, { - assertText: "Queue inventory", + assertText: "Queue realm", module: () => import("@/pages/app/queue"), path: "/queue/default", routePath: "/queue/{realm}", }, { - assertText: "Queue inventory", + assertText: "Queue area", module: () => import("@/pages/app/queue"), path: "/queue/default/ops", routePath: "/queue/{realm}/{area}", }, { - assertText: "Queue resource inspection", + assertText: "primary", module: () => import("@/pages/app/queue-resource"), path: "/admin/1/queue/default/ops/primary", routePath: "/admin/{family}/queue/{realm}/{area}/{resource}", @@ -77,13 +77,13 @@ describe("admin page smoke tests", () => { routePath: "/kv", }, { - assertText: "KV tables", + assertText: "KV realm", module: () => import("@/pages/app/kv"), path: "/kv/default", routePath: "/kv/{realm}", }, { - assertText: "KV tables", + assertText: "KV area", module: () => import("@/pages/app/kv"), path: "/kv/default/ops", routePath: "/kv/{realm}/{area}", @@ -101,13 +101,13 @@ describe("admin page smoke tests", () => { routePath: "/lease", }, { - assertText: "Lease inventory", + assertText: "Lease realm", module: () => import("@/pages/app/lease"), path: "/lease/default", routePath: "/lease/{realm}", }, { - assertText: "Lease inventory", + assertText: "Lease area", module: () => import("@/pages/app/lease"), path: "/lease/default/ops", routePath: "/lease/{realm}/{area}", @@ -125,13 +125,13 @@ describe("admin page smoke tests", () => { routePath: "/notice", }, { - assertText: "Notice inventory", + assertText: "Notice realm", module: () => import("@/pages/app/notice"), path: "/notice/default", routePath: "/notice/{realm}", }, { - assertText: "Notice inventory", + assertText: "Notice area", module: () => import("@/pages/app/notice"), path: "/notice/default/ops", routePath: "/notice/{realm}/{area}", @@ -204,16 +204,17 @@ describe("admin page smoke tests", () => { expect(root.querySelectorAll("main#main-content")).toHaveLength(1); expect(text).toContain(page.assertText); - expect(text).toContain("Resource inventory"); - expect(text).toContain("Route"); - expect(text).toContain(`${page.path.slice(1)}://default/ops/primary`); + expect(text).toContain("Realms"); + expect(text).toContain("Areas"); + expect(text).toContain("Resources"); for (const statLabel of page.statLabels) { expect(text).toContain(statLabel); } expect(text).toContain("Refresh"); expect(text).toMatch(/Live|Healthy|Quiet|Pressure|Attention/); - expect(root.querySelector('[data-slot="virtual-table"]')).toBeTruthy(); - expect(root.querySelector(`a[href="${page.resourceHref}"]`)).toBeTruthy(); + expect(root.querySelector('[data-slot="table"]')).toBeTruthy(); + expect(root.querySelector(`a[href="/admin/1${page.path}/default"]`)).toBeTruthy(); + expect(root.querySelector(`a[href="${page.resourceHref}"]`)).toBeNull(); cleanupApp(root); document.body.innerHTML = ""; @@ -222,7 +223,7 @@ describe("admin page smoke tests", () => { PAGE_SMOKE_TIMEOUT_MS, ); it( - "renders the flat inventory for domain overview, realm, and area routes", + "renders progressive realm, area, and resource inventories", async () => { for (const page of domainOverviews) { const { default: Component } = await page.module(); @@ -237,16 +238,22 @@ describe("admin page smoke tests", () => { const root = await mountRoute(routeVariant.path, routeVariant.routePath, Component); const text = root.textContent ?? ""; - expect(text).toContain(page.assertText); - expect(text).toContain("Resource inventory"); - expect(text).toContain("Route"); - expect(text).toContain(`${page.path.slice(1)}://default/ops/primary`); if (routeVariant.path === page.path) { + expect(text).toContain(page.assertText); + expect(text).toContain("Realms"); + expect(root.querySelector(`a[href="/admin/1${page.path}/default"]`)).toBeTruthy(); for (const statLabel of page.statLabels) { expect(text).toContain(statLabel); } + } else if (routeVariant.path.endsWith("/default")) { + expect(text).toContain("Areas"); + expect(root.querySelector(`a[href="/admin/1${page.path}/default/ops"]`)).toBeTruthy(); + } else { + expect(text).toContain("Resource inventory"); + expect(text).toContain("Route"); + expect(text).toContain(`${page.path.slice(1)}://default/ops/primary`); + expect(root.querySelector(`a[href="${page.resourceHref}"]`)).toBeTruthy(); } - expect(root.querySelector(`a[href="${page.resourceHref}"]`)).toBeTruthy(); cleanupApp(root); document.body.innerHTML = ""; @@ -277,9 +284,9 @@ describe("admin page smoke tests", () => { mocks.queryStates.inventory = queryState.fresh(inventory, queryOptions()); const noticeRealmRoot = await mountRoute("/notice/default", "/notice/{realm}", NoticePage); - expect(noticeRealmRoot.textContent).toContain("Notice inventory"); - expect(noticeRealmRoot.textContent).toContain("Resource inventory"); - expect(noticeRealmRoot.textContent).toContain("notice://default/ops/primary"); + expect(noticeRealmRoot.textContent).toContain("Notice realm"); + expect(noticeRealmRoot.textContent).toContain("Areas"); + expect(noticeRealmRoot.querySelector('a[href="/admin/1/notice/default/ops"]')).toBeTruthy(); cleanupApp(noticeRealmRoot); document.body.innerHTML = ""; @@ -288,7 +295,7 @@ describe("admin page smoke tests", () => { "/notice/{realm}/{area}", NoticePage, ); - expect(noticeAreaRoot.textContent).toContain("Notice inventory"); + expect(noticeAreaRoot.textContent).toContain("Notice area"); expect(noticeAreaRoot.textContent).toContain("Resource inventory"); expect(noticeAreaRoot.textContent).toContain("notice://default/ops/primary"); cleanupApp(noticeAreaRoot); diff --git a/ui/tests/page-smoke/fixtures.ts b/ui/tests/page-smoke/fixtures.ts index 5515c520..f5b86050 100644 --- a/ui/tests/page-smoke/fixtures.ts +++ b/ui/tests/page-smoke/fixtures.ts @@ -797,19 +797,19 @@ export function leaseResourceRowsFixture(expiresOffsetSeconds = 120) { export const scheduleHierarchyRoutes = [ { - assertText: "Schedule inventory", + assertText: "Schedule realm", path: "/schedule/default", routePath: "/schedule/{realm}", module: () => import("@/pages/app/schedule"), }, { - assertText: "Schedule inventory", + assertText: "Schedule area", path: "/schedule/default/ops", routePath: "/schedule/{realm}/{area}", module: () => import("@/pages/app/schedule"), }, { - assertText: "Schedule resource inspection", + assertText: "primary", path: "/admin/1/schedule/default/ops/primary", routePath: "/admin/{family}/schedule/{realm}/{area}/{resource}", module: () => import("@/pages/app/schedule-resource"), @@ -818,7 +818,7 @@ export const scheduleHierarchyRoutes = [ export const noticeHierarchyRoutes = [ { - assertText: "Notice operations", + assertText: "primary", domain: "notice", path: "/admin/1/notice/default/ops/primary", routePath: "/admin/{family}/notice/{realm}/{area}/{resource}", diff --git a/ui/tests/shared-ui-polish.test.tsx b/ui/tests/shared-ui-polish.test.tsx index 95061cc8..945735e9 100644 --- a/ui/tests/shared-ui-polish.test.tsx +++ b/ui/tests/shared-ui-polish.test.tsx @@ -279,7 +279,7 @@ describe("shared UI polish contracts", () => { expect(onSearchChange).toHaveBeenCalledWith(""); }); - it("sizes sparse resource inventories to their content minimum", async () => { + it("renders sparse resource inventories as standard tables", async () => { const root = await mount(() => ( { /> )); - expect(root.querySelector(".domain-resource-virtual-table")?.getAttribute("style")).toContain( - "140px", - ); + expect(root.querySelector(".domain-resource-data-table [data-slot='table']")).toBeTruthy(); }); it("scopes resource inventory rows to the route hierarchy", () => { @@ -693,8 +691,8 @@ describe("shared UI polish contracts", () => { expect(root.textContent).toContain("Route"); expect(root.textContent).toContain("Records"); expect(root.textContent).toContain("kv://default/ops/primary"); - expect(root.querySelector('[data-slot="virtual-table"]')).toBeTruthy(); - expect(root.querySelector(".domain-resource-virtual-table")).toBeTruthy(); + expect(root.querySelector('[data-slot="table"]')).toBeTruthy(); + expect(root.querySelector(".domain-resource-data-table")).toBeTruthy(); expect(root.querySelector(".domain-resource-metric")?.getAttribute("data-font")).toBe("mono"); expect(root.querySelector(".domain-resource-metric")?.getAttribute("data-numeric")).toBe( "tabular", @@ -736,7 +734,7 @@ describe("shared UI polish contracts", () => { expect(root.querySelector('button[aria-label="Sort by Ready, descending"]')).toBeTruthy(); }); - it("uses AskR table, virtual table, and card styling without app-local table chrome", async () => { + it("uses AskR table and card styling without app-local table chrome", async () => { const root = await mount(() => (
{ )); const tables = root.querySelectorAll('[data-slot="table"]'); - const virtualTables = root.querySelectorAll('[data-slot="virtual-table"]'); const cards = root.querySelectorAll('[data-slot="card"]'); - expect(tables).toHaveLength(1); - expect(virtualTables).toHaveLength(1); + expect(tables).toHaveLength(2); expect(cards).toHaveLength(2); - expect(root.querySelectorAll(".domain-table-wrap")).toHaveLength(1); + expect(root.querySelectorAll(".domain-table-wrap")).toHaveLength(2); expect(root.querySelector(".domain-table")).toBeNull(); expect(root.querySelector(".domain-metric-card")).toBeNull(); expect(root.querySelector(".domain-metric-value")?.getAttribute("data-font")).toBe("mono");