diff --git a/Cargo.lock b/Cargo.lock index 4b8e761..1c55c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -75,6 +76,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "bitflags" version = "2.13.1" @@ -116,6 +128,7 @@ name = "bogos-binted-web" version = "0.1.0" dependencies = [ "axum", + "bogos-binted", "include_dir", "serde", "tokio", diff --git a/crates/demo/src/main.rs b/crates/demo/src/main.rs index d1d3adc..4030147 100644 --- a/crates/demo/src/main.rs +++ b/crates/demo/src/main.rs @@ -1,5 +1,5 @@ use bogos_binted::{LogDestination, LogRetriever, ServiceExt as _, in_memory_database}; -use std::{pin::Pin, sync::Arc, task::Poll, time::Duration}; +use std::{pin::Pin, sync::Arc, task::Poll}; use tower::{Service, ServiceExt as _}; use tracing::{info, info_span}; use tracing_futures::Instrument; @@ -8,8 +8,13 @@ use tracing_futures::Instrument; struct UppercaseService; async fn crunch_numbers() { - info!("some serious maths goin on here"); - tokio::time::sleep(Duration::from_millis(100)).await; + info_span!("json.parse").in_scope(|| info!("parsing something or other")); + info_span!("activate the device").in_scope(|| info!("clicking the on button")); + info_span!("do some maths").in_scope(|| { + info_span!("finding a prime") + .in_scope(|| info_span!("proving P=NP").in_scope(|| info!("boop"))) + }); + info!("done!"); } impl Service for UppercaseService { @@ -62,7 +67,7 @@ async fn main() { let retrieve = LogRetriever::new(db); dbg!(retrieve.available_sinks()); - for x in retrieve.get_all("uppercase_service") { + for x in retrieve.get_all("uppercase_service").unwrap() { dbg!(x.unwrap()); } @@ -70,6 +75,7 @@ async fn main() { "127.0.0.1:3000", bogos_binted_web::Config { service_name: "bogos binted demo".to_string(), + log_retriever: retrieve, }, ) .await diff --git a/crates/lib/src/logs.rs b/crates/lib/src/logs.rs index 94ce233..9c9c03d 100644 --- a/crates/lib/src/logs.rs +++ b/crates/lib/src/logs.rs @@ -20,8 +20,8 @@ pub type Attributes = Vec<(String, AttributeValue)>; /// A log event, or message. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Event { - span_idx: usize, - attributes: Attributes, + pub span_idx: usize, + pub attributes: Attributes, } /// What an attribute is stored as. This may be an alternative (ie Debug) representation of the actual type. @@ -47,6 +47,32 @@ impl ServiceCallLogs { pub fn num_events(&self) -> usize { self.events.len() } + + pub fn called_with(&self) -> &str { + &self.called_with + } + + pub fn attributes_of_span(&self, id: usize) -> &Attributes { + &self.span_attributes[id] + } + + pub fn events_of_span(&self, id: usize) -> impl Iterator { + self.events.iter().filter(move |x| x.span_idx == id) + } + + pub fn children_of(&self, id: usize) -> impl Iterator { + self.relationships.iter().filter_map(move |x| match x { + SpanRelationship::SpanChild(child, parent) if *parent == id => Some(*child), + _ => None, + }) + } + + pub fn span_follows(&self, id: usize) -> impl Iterator { + self.relationships.iter().filter_map(move |x| match x { + SpanRelationship::SpanFollows(a, b) if *b == id => Some(*b), + _ => None, + }) + } } /// Turns a stream of log entries into a structured representation of them. diff --git a/crates/lib/src/retrieval.rs b/crates/lib/src/retrieval.rs index f459156..110f2c0 100644 --- a/crates/lib/src/retrieval.rs +++ b/crates/lib/src/retrieval.rs @@ -4,11 +4,13 @@ use std::{ }; use thiserror::Error; -use redb::{Database, Range, ReadableDatabase, StorageError, TableDefinition, TableHandle}; +use redb::{Range, ReadableDatabase, StorageError, TableDefinition, TableHandle}; use ulid::Ulid; use crate::logs::ServiceCallLogs; +pub use redb::Database; + pub struct LogRetriever { db: D, table_names: Vec, @@ -31,7 +33,7 @@ impl> LogRetriever { &self.table_names } - pub fn get(&self, name: &'static str, id: Id) -> Option { + pub fn get(&self, name: &str, id: Id) -> Option { let definition: TableDefinition<'_, u128, Vec> = TableDefinition::new(name); let bytes = self @@ -48,25 +50,36 @@ impl> LogRetriever { Some(logs) } - pub fn get_all(&self, name: &'static str) -> EntryIterator<'_> { + pub fn get_all(&self, name: &str) -> Result, LogRetrievalError> { let definition = TableDefinition::new(name); - EntryIterator( + Ok(EntryIterator( self.db - .begin_read() - .unwrap() - .open_table(definition) - .unwrap() - .range(0..u128::MAX) - .unwrap(), - ) + .begin_read()? + .open_table(definition)? + .range(0..u128::MAX)?, + )) + } + + pub fn get_in_id_range( + &self, + name: &str, + after: Option, + before: Option, + ) -> Result, LogRetrievalError> { + let definition = TableDefinition::new(name); + Ok(EntryIterator( + self.db.begin_read()?.open_table(definition)?.range( + after.map(Into::into).unwrap_or(0)..before.map(Into::into).unwrap_or(u128::MAX), + )?, + )) } pub fn get_in_time_range( &self, - name: &'static str, + name: &str, start: SystemTime, end: SystemTime, - ) -> EntryIterator<'_> { + ) -> Result, LogRetrievalError> { let definition = TableDefinition::new(name); let start_ulid = { @@ -89,15 +102,12 @@ impl> LogRetriever { Ulid::from_parts(timebits, u128::MAX) }; - EntryIterator( + Ok(EntryIterator( self.db - .begin_read() - .unwrap() - .open_table(definition) - .unwrap() - .range(start_ulid.0..=end_ulid.0) - .unwrap(), - ) + .begin_read()? + .open_table(definition)? + .range(start_ulid.0..=end_ulid.0)?, + )) } } @@ -121,6 +131,18 @@ impl Iterator for EntryIterator<'_> { } } +#[derive(Debug, Error)] +pub enum LogRetrievalError { + #[error("error starting transaction: {0}")] + TransactionError(#[from] redb::TransactionError), + + #[error("error reading from storage: {0}")] + StorageError(#[from] redb::StorageError), + + #[error("error reading table: {0}")] + TableError(#[from] redb::TableError), +} + #[derive(Debug, Error)] pub enum LoadEntryErr { #[error("error accessing database: {0}")] diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 4a5b8f6..d6f0142 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -8,6 +8,8 @@ tokio = { workspace = true } tracing = { workspace = true } tower = { workspace = true } serde = { workspace = true } -axum = "0.8.9" +axum = { version = "0.8.9", features = ["macros"] } tower-http = { version = "0.7.0", features = ["fs"] } include_dir = "0.7.4" + +bogos-binted = { path = "../lib" } diff --git a/crates/web/src/api.rs b/crates/web/src/api.rs new file mode 100644 index 0000000..f8ef9e9 --- /dev/null +++ b/crates/web/src/api.rs @@ -0,0 +1,182 @@ +use std::{collections::HashMap, ops::Deref, sync::Arc}; + +use axum::{ + Json, Router, + extract::{Path, Query, State}, + http::StatusCode, + response::Result, + routing::get, +}; +use bogos_binted::{ + Database, Id, LoadEntryErr, + logs::{AttributeValue, ServiceCallLogs}, +}; + +use serde::{Deserialize, Serialize}; + +use crate::Config; + +pub fn router + Send + Sync + 'static>( + config: Arc>, +) -> Router { + Router::new() + .route("/info", get(info_route)) + .route("/sink/{sink}/previews", get(page_preview::)) + .route("/sink/{sink}/log/{id}", get(log_data::)) + .with_state(config) +} + +#[derive(Debug, Clone, Serialize)] +struct InfoResponse { + name: String, + bogos_binted_version: &'static str, + available_sinks: Vec, +} + +async fn info_route + Send + Sync + 'static>( + config: State>>, +) -> Json { + Json(InfoResponse { + name: config.service_name.clone(), + bogos_binted_version: env!("CARGO_PKG_VERSION"), + available_sinks: config.log_retriever.available_sinks().to_vec(), + }) +} + +#[derive(Debug, Clone, Serialize)] +struct LogPreview { + id: String, + input: String, +} + +#[derive(Debug, Clone, Serialize)] +struct LogPreviews { + results: Vec, +} + +#[derive(Deserialize)] +struct Pagination { + after: Option, + + #[serde(default = "default_limit")] + limit: usize, +} + +fn default_limit() -> usize { + 20 +} + +impl From<(Id, ServiceCallLogs)> for LogPreview { + fn from((id, logs): (Id, ServiceCallLogs)) -> Self { + Self { + id: format!("{}", id), + input: logs.called_with().to_string(), + } + } +} + +async fn page_preview + Send + Sync + 'static>( + sink: Path, + page: Query, + config: State>>, +) -> Result> { + let results = config + .log_retriever + .get_in_id_range( + &sink, + None, + match &page.after { + Some(x) => Some( + Id::from_string(&x) + .map_err(|e| (StatusCode::BAD_REQUEST, format!("error parsing id: {e}")))?, + ), + None => None, + }, + ) + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("error retrieving logs: {e}"), + ) + })? + .take(page.limit) + .map(|r| r.map(Into::into)) + .collect::, LoadEntryErr>>() + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("error retrieving logs: {e}"), + ) + })?; + + Ok(Json(LogPreviews { results })) +} + +#[derive(Debug, Serialize, Clone)] +struct LogData { + input: String, + root_span: SpanData, +} + +impl From for LogData { + fn from(value: ServiceCallLogs) -> Self { + LogData { + input: value.called_with().to_string(), + root_span: parse_span(&value, 0), + } + } +} + +impl From for EventData { + fn from(value: bogos_binted::logs::Event) -> Self { + Self { + attributes: value.attributes.into_iter().collect(), + } + } +} + +fn parse_span(logs: &ServiceCallLogs, id: usize) -> SpanData { + let child_spans = logs + .children_of(id) + .map(|id| parse_span(logs, id)) + .collect(); + + SpanData { + id, + child_events: logs.events_of_span(id).cloned().map(Into::into).collect(), + child_spans, + attributes: logs.attributes_of_span(id).iter().cloned().collect(), + follows: logs.span_follows(id).collect(), + } +} + +#[derive(Debug, Serialize, Clone)] +struct SpanData { + id: usize, + child_events: Vec, + child_spans: Vec, + attributes: HashMap, + follows: Vec, +} + +#[derive(Debug, Serialize, Clone)] +struct EventData { + attributes: HashMap, +} + +async fn log_data + Send + Sync + 'static>( + paths: Path<(String, String)>, + config: State>>, +) -> Result> { + let (sink, id) = paths.0; + + let results = config + .log_retriever + .get( + &sink, + Id::from_string(&id).map_err(|_| (StatusCode::NOT_FOUND, "error parsing id"))?, + ) + .ok_or_else(|| (StatusCode::NOT_FOUND, format!("couldnt find log entry")))?; + + Ok(Json(results.into())) +} diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index 6542d8b..a93e98e 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -1,5 +1,6 @@ +use bogos_binted::{Database, LogRetriever}; use serde::Serialize; -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, ops::Deref, sync::Arc}; #[cfg(debug_assertions)] use std::{path::PathBuf, str::FromStr}; @@ -17,12 +18,17 @@ use axum::{Json, Router, extract::State, routing::get}; use tokio::net::ToSocketAddrs; use tracing::info; -#[derive(Debug)] -pub struct Config { +mod api; + +pub struct Config { pub service_name: String, + pub log_retriever: LogRetriever, } -pub async fn serve(addr: impl ToSocketAddrs + Debug, config: Config) -> Result<(), std::io::Error> { +pub async fn serve + Send + Sync + 'static>( + addr: impl ToSocketAddrs + Debug, + config: Config, +) -> Result<(), std::io::Error> { info!("will listen on {:?}", addr); axum::serve( tokio::net::TcpListener::bind(addr).await.unwrap(), @@ -33,8 +39,10 @@ pub async fn serve(addr: impl ToSocketAddrs + Debug, config: Config) -> Result<( /// Build an axum [`Router`] that runs the web interface. /// This is useful when integrating with a larger axum app. -pub fn router(config: Config) -> Router { +pub fn router + Send + Sync + 'static>(config: Config) -> Router { let mut router = Router::new(); + let config = Arc::new(config); + #[cfg(debug_assertions)] let path = { info!("in debug mode, serving from filesystem"); @@ -78,20 +86,13 @@ pub fn router(config: Config) -> Router { serve_file!("/css.css", "css.css", "text/css"); serve_file!("/js.js", "js.js", "text/javascript"); - router - .route("/api/info", get(info_route)) - .with_state(Arc::new(config)) + router.nest("/api", api::router(config)) } -#[derive(Debug, Clone, Serialize)] -struct InfoResponse { - name: String, - bogos_binted_version: &'static str, -} - -async fn info_route(config: State>) -> Json { - Json(InfoResponse { - name: config.service_name.clone(), - bogos_binted_version: env!("CARGO_PKG_VERSION"), - }) +impl Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Config") + .field("service_name", &self.service_name) + .finish_non_exhaustive() + } } diff --git a/crates/web/templates/css.css b/crates/web/templates/css.css index 04cd77d..00bfdbe 100644 --- a/crates/web/templates/css.css +++ b/crates/web/templates/css.css @@ -1,5 +1,4 @@ - -/* GIC.css Version 1.4.0 +/* Parts of this from GIC.css Version 1.4.0 * (c) Copyright Ariadne (me@aria.rip) 2026. * Made available under the MIT license */ :root { @@ -78,40 +77,12 @@ --black-tint-95: #f3f3f3; --white: #ffffff; - --colour-brand: var(--blue); + --colour-brand: var(--purple); --colour-brand-text: var(--white); --colour-text: var(--black); --colour-body-background: var(--white); - /* Used in for example 'muted' text and help text. */ - --colour-text-secondary: var(--black-tint-25); - - --colour-focus: var(--yellow); - --colour-focus-text: var(--black); - - --colour-error: var(--red); - --colour-success: var(--green); - - /* Used in for example borders, separators, rules and keylines. */ - --colour-border: var(--black-tint-80); - /* Used for form inputs and controls */ - --colour-input-border: var(--black); - /* Used for hover states on form controls */ - --colour-hover: var(--black-tint-80); - /* Standard links (on white) */ - --colour-link: var(--blue-shade-10); - --colour-link-visited: var(--purple); - --colour-link-hover: var(--blue-shade-50); - --colour-link-active: var(--black); - - /* 'Surfaces' are our name for components that have different colour */ - /* palettes to typical page content. This is the generic surface. */ - --colour-surface-background: var(--blue-tint-95); - --colour-surface-text: var(--black); - --colour-surface-board: var(--blue-tint-50); - --colour-button-text: var(--white); - @media (prefers-color-scheme: dark) { color-scheme: dark; @@ -187,169 +158,14 @@ --white: #cdd6f4; --white-shade-25: #9aa1b7; - /* Alternative Dark theme, from modus operandi (Vivendi) */ - /* A bit higher contrast for those who like that */ - /* --blue: #2fafff; */ - /* --blue-tint-25: #2383bf; */ - /* --blue-tint-50: #185880; */ - /* --blue-tint-80: #092333; */ - /* --blue-tint-95: #02090d; */ - /* --blue-shade-10: #44b7ff; */ - /* --blue-shade-25: #63c3ff; */ - /* --blue-shade-50: #97d7ff; */ - /* --blue-shade-95: #f5fbff; */ - /* --green: #44bc44; */ - /* --green-tint-25: #338d33; */ - /* --green-tint-50: #225e22; */ - /* --green-tint-80: #0e260e; */ - /* --green-tint-95: #030903; */ - /* --green-shade-25: #73cd73; */ - /* --green-shade-50: #a2dea2; */ - /* --teal: #00d3d0; */ - /* --teal-tint-25: #009e9c; */ - /* --teal-tint-50: #006a68; */ - /* --teal-tint-80: #002a2a; */ - /* --teal-tint-95: #000b0a; */ - /* --teal-shade-25: #40dedc; */ - /* --teal-shade-50: #80e9e8; */ - /* --purple: #9099d9; */ - /* --purple-tint-25: #6c73a3; */ - /* --purple-tint-50: #484d6d; */ - /* --purple-tint-80: #1d1f2b; */ - /* --purple-tint-95: #07080b; */ - /* --purple-shade-25: #acb3e3; */ - /* --purple-shade-50: #c8ccec; */ - /* --pink: #feacd0; */ - /* --pink-tint-25: #bf819c; */ - /* --pink-tint-50: #7f5668; */ - /* --pink-tint-80: #33222a; */ - /* --pink-tint-95: #0d090a; */ - /* --pink-shade-25: #fec1dc; */ - /* --pink-shade-50: #ffd6e8; */ - /* --red: #ff5f59; */ - /* --red-tint-25: #bf4743; */ - /* --red-tint-50: #80302d; */ - /* --red-tint-80: #331312; */ - /* --red-tint-95: #0d0504; */ - /* --red-shade-25: #ff8783; */ - /* --red-shade-50: #ffafac; */ - /* --orange: #db7b5f; */ - /* --orange-tint-25: #a45c47; */ - /* --orange-tint-50: #6e3e30; */ - /* --orange-tint-80: #2c1913; */ - /* --orange-tint-95: #0b0605; */ - /* --orange-shade-25: #e49c87; */ - /* --orange-shade-50: #edbdaf; */ - /* --yellow: #d0bc00; */ - /* --yellow-tint-25: #9c8d00; */ - /* --yellow-tint-50: #685e00; */ - /* --yellow-tint-80: #2a2600; */ - /* --yellow-tint-95: #0a0900; */ - /* --yellow-shade-25: #dccd40; */ - /* --yellow-shade-50: #e8de80; */ - /* --brown: #f2cdcd; */ - /* --brown-tint-25: #b69a9a; */ - /* --brown-tint-50: #796767; */ - /* --brown-tint-95: #0c0a0a; */ - /* --black: #000000; */ - /* --black-tint-25: #404040; */ - /* --black-tint-50: #808080; */ - /* --black-tint-80: #cccccc; */ - /* --black-tint-95: #f2f2f2; */ - /* --white: #ffffff; */ - /* --white-shade-250: #bfbfbf; */ - --colour-text: var(--white); - --colour-body-background: var(--black); - --colour-text-secondary: var(--white-shade-25); - --colour-border: var(--white); - --colour-input-border: var(--white); - --colour-focus-text: var(--white); - --colour-link-active: var(--white); - --colour-surface-background: var(--blue-tint-80); - --colour-surface-text: #fff; - --colour-surface-board: var(--blue-tint-50); - --colour-button-text: var(--black); --colour-brand-text: var(--black); - } - - - --width-input-border: 2px; - --width-form-group-error: 5px; - --width-focus: 3px; - - --font-size-80: 53px; - --line-height-80: 55px; - --font-size-48: 32px; - --line-height-48: 35px; - --font-size-36: 27px; - --line-height-36: 30px; - --font-size-27: 21px; - --line-height-27: 25px; - --font-size-24: 21px; - --line-height-24: 25px; - --font-size-19: 19px; - --line-height-19: 25px; - --font-size-16: 16px; - --line-height-16: 20px; - - --spacing-0: 0; - --spacing-1: 5px; - --spacing-2: 10px; - --spacing-3: 15px; - --spacing-4: 15px; - --spacing-5: 15px; - --spacing-6: 20px; - --spacing-7: 25px; - --spacing-8: 30px; - --spacing-9: 40px; - - @media screen and (min-width: 641px) { /* tablet */ - --font-size-80: 80px; - --line-height-80: 80px; - --font-size-48: 48px; - --line-height-48: 50px; - --font-size-36: 36px; - --line-height-36: 40px; - --font-size-27: 27px; - --line-height-27: 30px; - --font-size-24: 24px; - --line-height-24: 30px; - --spacing-4: 20px; - --spacing-5: 25px; - --spacing-6: 30px; - --spacing-7: 40px; - --spacing-8: 50px; - --spacing-9: 60px; - } - - @media print { - --colour-text: #000000; - --font-size-80: 53pt; - --line-height-80: 1.1; - --font-size-48: 32pt; - --line-height-48: 1.15; - --font-size-36: 24pt; - --line-height-36: 1.05; - --font-size-27: 18pt; - --line-height-27: 1.15; - --font-size-24: 18pt; - --line-height-24: 1.15; - --font-size-19: 14pt; - --line-height-19: 1.15; - --font-size-16: 14pt; - --line-height-16: 1.2; + --colour-body-background: var(--black); } } -/* eeeee e e eeeee eeeee eeeee eeeee eeeee eeeee e e e e */ -/* 8 8 8 8 8 8 88 8 8 8 8 8 8 8 8 8 8 8 8 */ -/* 8e 8eeee8 8eee8 8 8 8e 8eee8e 8eee8 8eee8 8eee8 8eeee8 */ -/* 88 88 88 8 8 88 "8 88 8 88 8 88 88 8 88 */ -/* 88 88 88 8eee8 88ee8 88 8 88 8 88 88 8 88 */ -html { background: var(--colour-body-background); } -html:has(footer) { background: var(--colour-surface-background); } +html { background: var(--colour-body-background); font-size: 14pt; } body { font-family: var(--font-variable); @@ -367,675 +183,145 @@ body { text-size-adjust: 100%; } -h1,h2,h3,h4 { font-weight: var(--font-weight-bold); margin-top: 0; } -span.caption { color: var(--colour-text-secondary); display: block; } - -p { - margin-top: 0; -} - -/* sizing stuff. */ -h1 { font-size: var(--font-size-48); line-height: var(--line-height-48); margin-bottom: var(--spacing-8); } -h2 { font-size: var(--font-size-36); line-height: var(--line-height-36); margin-bottom: var(--spacing-6); } -h3 { font-size: var(--font-size-24); line-height: var(--line-height-24); margin-bottom: var(--spacing-4); } -h4 { font-size: var(--font-size-19); line-height: var(--line-height-19); margin-bottom: var(--spacing-4); } -p { font-size: var(--font-size-19); line-height: var(--line-height-19); margin-bottom: var(--spacing-4); } -p.large { font-size: var(--font-size-24); line-height: var(--line-height-24); margin-bottom: var(--spacing-6); } -p.small { font-size: var(--font-size-16); line-height: var(--line-height-16); margin-bottom: var(--spacing-4); } - -span.caption:has(+ h1) { font-size: var(--font-size-27); line-height: var(--line-height-27); margin-bottom: var(--spacing-1); } -span.caption:has(+ h2) { font-size: var(--font-size-24); line-height: var(--line-height-24); margin-bottom: var(--spacing-1); } -span.caption:has(+ h3) { font-size: var(--font-size-19); line-height: var(--line-height-19); } - -/* Add top padding to headings that appear directly after paragraphs. */ -p.large + h2 { padding-top: var(--spacing-1); } - -p + h2, -ul + h2, -ol + h2 { padding-top: var(--spacing-4); } - -p + h3, -ul + h3, -ol + h3, -p + h4, -ul + h4, -ol + h4 { padding-top: var(--spacing-1); } - -a { - text-decoration: underline; - text-decoration-thickness: max(1px, .0625rem); - text-decoration-offset: 0.1578em; - color: var(--colour-link); - - &:not(.no-visited):visited { - color: var(--colour-link-visited); - } - - &:hover { - color: var(--colour-link-hover); - text-decoration-thickness: max(3px, .1875rem, .12em); - text-decoration-skip-ink: none; - text-decoration-skip: none; - } - - &:active { - color: var(--colour-link-active); - } - - /* When focussed, the text colour needs to be darker to ensure that colour */ - /* contrast is still acceptable */ - &:focus { - color: var(--colour-focus-text); - } - - /* Show URL when printing */ - @media print { - &[href^="/"], - &[href^="http://"], - &[href^="https://"] { - &::after { - content: " (" attr(href) ")"; - font-size: 90%; - word-wrap: break-word; - } - } - } - - &.error { - &, - &:visited { - color: var(--colour-error); - } - - &:hover { - color: var(--red-shade-50); - } - - &:active { - color: var(--colour-error); - } - - &:focus { - color: var(--colour-focus-text); - } - } - - &.success { - &, - &:visited { - color: var(--colour-success); - } - - &:hover { - color: var(--green-shade-50); - } - - &:active { - color: var(--colour-success); - } - - &:focus { - color: var(--colour-focus-text); - } - } +h1, h2, h3, h4, h5 { font-family: monospace; } - - &.no-underline:not(:hover):not(:active) { - text-decoration: none; - } -} - - -ul, ol { - font-size: var(--font-size-19); - line-spacing: var(--line-spacing-19); - - margin-top: 0; - margin-bottom: var(--spacing-4); - - color: var(--colour-text); - - padding-left: var(--spacing-4); - /* list-style-type: disc; */ - &.no-bullet { - padding-left: 0; - list-style-type: none; - > li { - margin-bottom: var(--spacing-1); - } - } - - /* Add a top margin for nested lists */ - ul, ol { - margin-top: var(--spacing-2); - } - - - /* Lists without numbers or bullets should always have extra space between */ - /* list items. Lists with numbers or bullets only have this extra space on */ - /* tablet and above */ - > li { - margin-bottom: 0; - @media screen and (min-width: 641px) { /* tablet */ - margin-bottom: var(--spacing-1); - } - } - - &.extra-space > li { - margin-bottom: var(--spacing-2); - @media screen and (min-width: 641px) { /* tablet */ - margin-bottom: var(--spacing-3); - } - } -} - - -/* section spacer */ -hr { margin: var(--spacing-4) 0; border: 0; } -hr.xl { margin: var(--spacing-8) 0; } -hr.l { margin: var(--spacing-6) 0; } -hr.visible { border-bottom: 1px solid var(--colour-border) } - -/* utilities */ -.break-word { word-break: break-word; } - -/* oo dP */ -/* 88 */ -/* .d8888b. 88d888b. dP .d888b88 .d8888b. */ -/* 88' `88 88' `88 88 88' `88 Y8ooooo. */ -/* 88. .88 88 88 88. .88 88 */ -/* `8888P88 dP dP `88888P8 `88888P' */ -/* .88 */ -/* d8888P */ main { - padding: var(--spacing-6) 0; -} + max-width: 110ch; + margin: 0 auto; -grid-container { - --size-gutter: 30px; - --size-gutter-half: calc(var(--size-gutter) / 2); - --size-page-width: 960px; - - max-width: var(--size-page-width); - - margin-left: var(--size-gutter-half); - margin-right: var(--size-gutter-half); - - /* On tablet, add full width gutters */ - @media screen and (min-width: 641px) { /* tablet */ - margin-right: var(--size-gutter); - margin-left: var(--size-gutter); + h1#title { + text-align: center; + font-size: 3rem; } - /* As soon as the viewport is greater than the width of the page plus the */ - /* gutters, just centre the content instead of adding gutters. */ - @media (min-width: 1020px) { - margin-right: auto; - margin-left: auto; - } - - display: grid; - grid-template-columns: repeat(12, 1fr); - grid-gap: var(--size-gutter); -} - - -/* By default, column spans only apply past the tablet breakpoint */ -.whole-row { grid-column-start: span 12; } -.one-third { grid-column-start: span 12; } -.two-thirds { grid-column-start: span 12; } -.one-half { grid-column-start: span 12; } -.one-quarter { grid-column-start: span 12; } -.two-quarters { grid-column-start: span 12; } -.three-quarters { grid-column-start: span 12; } - -/* Force applying the whole time */ -.mobile-whole-row { grid-column-start: span 12; } -.mobile-one-third { grid-column-start: span 4; } -.mobile-two-thirds { grid-column-start: span 8; } -.mobile-one-half { grid-column-start: span 6; } -.mobile-one-quarter { grid-column-start: span 3; } -.mobile-two-quarters { grid-column-start: span 6; } -.mobile-three-quarters { grid-column-start: span 9; } - -@media screen and (min-width: 641px) { /* tablet */ - .whole-row { grid-column-start: span 12; } - .one-third { grid-column-start: span 4; } - .two-thirds { grid-column-start: span 8; } - .one-half { grid-column-start: span 6; } - .one-quarter { grid-column-start: span 3; } - .two-quarters { grid-column-start: span 6; } - .three-quarters { grid-column-start: span 9; } - - .tablet-whole-row { grid-column-start: span 12; } - .tablet-one-third { grid-column-start: span 4; } - .tablet-two-thirds { grid-column-start: span 8; } - .tablet-one-half { grid-column-start: span 6; } - .tablet-one-quarter { grid-column-start: span 3; } - .tablet-two-quarters { grid-column-start: span 6; } - .tablet-three-quarters { grid-column-start: span 9; } -} - -@media screen and (min-width: 769px) { /* desktop */ - .desktop-whole-row { grid-column-start: span 12; } - .desktop-one-third { grid-column-start: span 4; } - .desktop-two-thirds { grid-column-start: span 8; } - .desktop-one-half { grid-column-start: span 6; } - .desktop-one-quarter { grid-column-start: span 3; } - .desktop-two-quarters { grid-column-start: span 6; } - .desktop-three-quarters { grid-column-start: span 9; } -} - -/* _ _ ____ ____ ___ ____ ____ ____ ____ ____ ___ ____ ____ */ -/* |__| |___ |__| | \ |___ |__/ |___ | | | | | |___ |__/ */ -/* | | |___ | | |__/ |___ | \ | |__| |__| | |___ | \ */ -/* */ -header.masthead { - font-size: var(--font-size-16); - font-weight: var(--font-weight-bold); - line-height: 1; - - border-bottom: 1px solid transparent; - color: var(--colour-brand-text); - background: var(--colour-brand); - - &:has(> a) { padding: 16px; } - - > a { - display: inline; - margin-right: var(--spacing-2); - font-size: var(--font-size-27); - line-height: var(--line-height-27); - text-decoration: none; - color: inherit; - - &:visited { color: inherit; } + instance-info { + padding: 1lh; + display: block; } - nav { + #available-sink-list { display: flex; - flex-direction: row; + flex-direction: column; + justify-items: center; align-items: center; + list-style-type: none; - &:has(input:checked) { - padding-top: var(--spacing-3); - flex-direction: column; - } - - ul { - list-style-type: none; - align-items: center; - margin: 0; padding: 0; - height: 100%; - - /* On mobile layouts, hide items by default (except for the first one, which is a checkbox in disguise). - When checkbox is checked, flex in a column */ - display: flex; - flex-grow: 1; - flex-direction: row; - justify-content: flex-end; - li:not(:first-child) { display: none; } - &:has(input:checked){ - width: 100%; - display: flex; - flex-direction: column; - margin-top: var(--spacing-3); - li { - display: block; - width: 100%; - text-align: center; - border-top: 1px solid var(--colour-text); - } - } - - /* On tablet and above, show in a row */ - @media screen and (min-width: 641px) { /* tablet */ - li:has(input) { display: none !important; } - li { display: block !important; } - flex-direction: row; - justify-content: flex-start; - } - - li { - margin: 0; - box-sizing: border-box; - - /* Checkbox used for show/hide on mobile */ - input { - appearance: none; - display: block; width: 100%; height: 100%; - outline: none; - border: none; - margin: 0 auto; - &::before { - font-size: var(--font-size-16); - display: block; - width: 100%; - text-align: center; - content: "Open Navbar"; - font-weight: var(--font-weight-bold); - } - &:checked::before { content: "Close navbar"; } - } - - a,input { - padding: var(--spacing-4); - display: block; - box-sizing: border-box; - width: 100%; height: 100%; - - color: var(--colour-brand-text) !important; - text-decoration: none; - - &:hover, &:focus { - background: var(--colour-hover); - color: var(--colour-text) !important; - } - } - } + button { + appearance: none; + border: 0; + background: var(--colour-brand); + color: var(--colour-brand-text); + padding: 1rem; } } } footer { - font-size: var(--font-size-19); - line-height: var(--line-height-19); - - padding: var(--spacing-5) 0; - - border-top: 10px solid var(--colour-brand); - color: var(--colour-footer-text); - background: var(--colour-surface-background); -} - -/* ______ ___ ____ __ ____ __ */ -/* | || | // \\ || )) || || (( \ */ -/* || ||=|| ||=) || ||== \\ */ -/* || || || ||_)) ||__| ||___ \_)) */ -/* */ -table { - font-size: var(--font-size-19); - line-height: var(--line-height-19); - - margin-bottom: var(--spacing-6); - width: 100%; - display: block; - overflow: auto; - - border-spacing: 0; - border-collapse: collapse; - - thead { font-weight: var(--font-weight-bold); } - - td,th { - padding: var(--spacing-2) var(--spacing-4) var(--spacing-2) 0; - border-bottom: 1px solid var(--colour-border); - text-align: left; - vertical-align: top; - - &.numeric { text-align: right; } - &:last-child { padding-right: 0; } - } - - - caption { - display: table-caption; - font-weight: var(--font-weight-bold); - text-align: left; - - - &.xl, &.l, &.m { margin-bottom: var(--spacing-3); } - - &.xl { font-size: var(--font-size-48); line-height: var(--line-height-48); } - &.l { font-size: var(--font-size-36); line-height: var(--line-height-36); } - &.m { font-size: var(--font-size-24); line-height: var(--line-height-24); } + border-top: 3px solid var(--colour-brand); + > p { + max-width: 110ch; + margin: 1lh auto; } } -/* ____ ____ ____ _ _ ____ */ -/* |___ | | |__/ |\/| [__ */ -/* | |__| | \ | | ___] */ -/* */ -input { - font-size: var(--font-size-19); - line-height: var(--line-height-19); +loading, +loading:after { box-sizing: border-box; - width: 100%; - margin-top: 0; - padding: var(--spacing-1); - - - border: var(--width-input-border) var(--colour-input-border) solid; - border-radius: 0; - appearance: none; - - &:focus { - outline: var(--width-focus) solid var(--colour-focus); - outline-offset: 0; - /* Double the border by adding its width again. Use `box-shadow` for this */ - /* instead of changing `border-width` - this is for consistency with */ - /* components such as textarea where we avoid changing `border-width` as */ - /* it will change the element size. Also, `outline` cannot be utilised */ - /* here as it is already used for the yellow focus state. */ - box-shadow: inset 0 0 0 var(--width-input-border) var(--colour-input-border); - } - - &:disabled { - opacity: 0.5; - color: inherit; - background-color: transparent; - cursor: not-allowed; - } - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - margin: 0; - -webkit-appearance: none; - } - - &[type="number"] { - -moz-appearance: textfield; - } - - &.extra-letter-spacing { - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; - } - - - @media screen and (max-width: 640px) { /* until mobile */ - display: block; - max-width: 100%; - } -} - -input-wrapper { - display: flex; - input { flex: 0 1 auto; } } - -input-prefix, input-suffix { - font-family: var(--font-size-19); - line-height: var(--line-height-19); - - box-sizing: border-box; - display: flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - min-width: 1rem; - padding: var(--spacing-1); - border: var(--width-input-border) var(--colour-input-border) solid; - - background-color: var(--black-tint-95); - text-align: center; - white-space: nowrap; - cursor: default; - -} -input-prefix { border-right: 0; } -input-suffix { border-left: 0; } - -.width-30 { max-width: 29.5em; } -.width-20 { max-width: 20.5em; } -.width-10 { max-width: 11.5em; } -.width-5 { max-width: 5.5em; } -.width-4 { max-width: 4.5em; } -.width-3 { max-width: 3.75em; } -.width-2 { max-width: 2.75em; } - - -form-group { - margin-bottom: var(--spacing-6); - display: block; - - &:last-of-type { - margin-bottom: 0; - } - - &:has(.error), &:has(input:invalid) { - padding-left: var(--spacing-3); - border-left: var(--width-form-group-error) solid var(--colour-error); - } -} - -input:invalid, -textarea:invalid, -form-group:has(.error) input, -form-group:has(.error) textarea { - border-color: var(--colour-error); - &:focus { border-color: var(--colour-input-border); } -} - -.error:not(a) { - font-size: var(--font-size-19); - line-height: var(--line-height-19); - - font-weight: var(--font-weight-bold); - +loading { display: block; - margin-top: 0; - margin-bottom: var(--spacing-3); - clear: both; - color: var(--colour-error); + color: var(--colour-brand); + margin: 2lh auto; + width: 80px; + height: 80px; } - -label { +loading:after { + content: " "; display: block; - margin-bottom: var(--spacing-1); - color: var(--color-text); + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6.4px solid currentColor; + border-color: currentColor transparent currentColor transparent; + animation: lds-dual-ring 1.2s linear infinite; } - -h1 > label, -h2 > label, -h3 > label { - margin-bottom: var(--spacing-3); -} - -h1 > label { font-size: var(--font-size-48); } - -h1:has(> label), -h2:has(> label), -h3:has(> label) { - margin: 0; -} - -.hint { - font-size: var(--font-size-19); - line-height: var(--line-height-19); - - display: block; - margin-bottom: var(--spacing-3); - - color: var(--colour-secondary-text); -} - -textarea { - font-size: var(--font-size-19); - line-height: 1.25; - - display: block; - width: 100%; - min-height: 50px; - margin-bottom: var(--spacing-6); - padding: var(--spacing-1); - - resize: vertical; - border: var(--width-input-border) solid var(--colour-input-border); - border-radius: 0; - -webkit-appearance: none; - - &:focus { - outline: var(--width-focus) solid var(--colour-focus); - outline-offset: 0; - box-shadow: inset 0 0 0 var(--width-input-border) var(--colour-input-border); +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); } - - &:disabled { - opacity: 0.5; - color: inherit; - background-color: transparent; - cursor: not-allowed; + 100% { + transform: rotate(360deg); } } -button { - --button-bg: var(--green-shade-25); - --button-bg-darker: var(--green-shade-50); - --text: var(--colour-button-text); - - text-decoration: none; - font-weight: var(--font-weight-normal); - font-size: var(--font-size-80) - line-height: 1; - text-align: center; - - display: inline-block; - position: relative; - - margin: 0 0 22px; - margin-bottom: 22px; - padding: 8px 10px 7px; - border: 2px solid transparent; +ul#previews { + display: flex; + list-style-type: none; + padding: 1lh; + align-content: flex-start; + flex-direction: row; - color: var(--text); - box-shadow: 0 2px 0 var(--button-bg-darker); - background-color: var(--button-bg); + li { + display: block; + width: 100%; + + log-list-item, + expanded-log { + display: block; + border: 1px solid var(--colour-brand); + + .header { + span { + display: inline-block; + font-family: monospace; + font-size: 1rem; + } + span#id { + background: var(--colour-brand); + color: var(--colour-brand-text); + padding: 0.5rem; + margin-right: 0.5rem; + } + span#input { + font-family: monospace; + } - &:hover { - background-color: var(--button-bg-darker); - } + #collapse { + float: right; + background: var(--colour-text); + color: var(--colour-body-background); + border: 0; + padding: 0.5rem; + font-size: 1rem; + } + } + } - &:active { - top: 2px; + #root-span { + padding: 1rem; + } } +} - &:focus { - border-color: var(--colour-focus); - box-shadow: 0 var(--width-input-border) 0 var(--colour-focus-text); - outline: var(--width-focus) solid transparent; +log-span { + border-left: 1rem solid var(--span-colour); + display: block; + padding-top: 0.5lh; + attributes { + display: block; + padding: 0.5lh 1rem; + pre { + margin: 0; + } + border-bottom: 1px solid var(--colour-text); } - &.secondary { - --button-bg: var(--black-tint-95); - --button-bg-darker: var(--black-tint-80); - --text: var(--colour-text); - } - &.warning { - --button-bg: var(--red-shade-25); - --button-bg-darker: var(--red-shade-50); + #events,#children { + list-style-type: none; + padding: 0; } - &[disabled] { - opacity: .5; - cursor: notallowed; - &:hover { - background-color: var(--button-bg); - } - &:active { - top: 0; - } - } } diff --git a/crates/web/templates/index.html b/crates/web/templates/index.html index 347e6e0..7b0073d 100644 --- a/crates/web/templates/index.html +++ b/crates/web/templates/index.html @@ -10,37 +10,75 @@
- -
- -

- loading, please wait... -

- + +

+ loading, please wait... +

-