diff --git a/typed-doc/copy-to-clipboard-feature.tsx b/typed-doc/copy-to-clipboard-feature.tsx new file mode 100644 index 0000000..3e422ce --- /dev/null +++ b/typed-doc/copy-to-clipboard-feature.tsx @@ -0,0 +1,102 @@ +/** + * Issue #322: Add copy-to-clipboard on recommended fee + * + * Implements single-click copy of the raw stroop value from the + * RecommendationPanel, with "Copied!" feedback and graceful fallback. + */ + +// packages/ui/src/components/dashboard/RecommendationPanel.tsx +import { useState, useCallback } from "react"; + +interface RecommendationPanelProps { + recommendedFee: number; // raw stroop value (e.g. 3849) + lowFee: number; + mediumFee: number; + highFee: number; +} + +export function RecommendationPanel({ + recommendedFee, + lowFee, + mediumFee, + highFee, +}: RecommendationPanelProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(String(recommendedFee)); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // clipboard API unavailable — fallback + const ta = document.createElement("textarea"); + ta.value = String(recommendedFee); + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } + }, [recommendedFee]); + + return ( +
+

+ Recommended Fee +

+ +
+ + {recommendedFee.toLocaleString()} + + stroops + + +
+ +
+
Low: {lowFee.toLocaleString()}
+
Medium: {mediumFee.toLocaleString()}
+
High: {highFee.toLocaleString()}
+
+
+ ); +} + +// Icons (inline to avoid import complexity) +function CopyIcon({ className }: { className?: string }) { + return ( + + + + + ); +} + +function CheckIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/typed-doc/simulation-instrumentation.rs b/typed-doc/simulation-instrumentation.rs new file mode 100644 index 0000000..a3663df --- /dev/null +++ b/typed-doc/simulation-instrumentation.rs @@ -0,0 +1,127 @@ +/** + * Issue #340: Add span instrumentation to simulation module + * + * Wraps all public simulation functions with tracing spans using + * #[tracing::instrument] for observability of the fee simulation pipeline. + * + * Requirements: + * - Each span includes: function name, input config fields, output sample count + * - Spans visible in both JSON and human log formats + */ + +// packages/devkit/src/simulation/fee_model.rs +use tracing::instrument; + +/// Parameters controlling fee model behaviour. +#[derive(Debug)] +pub struct FeeModelConfig { + pub min_fee: u64, + pub max_fee: u64, + pub base_reserve: u64, + pub ledger_capacity: u64, +} + +/// Output from the fee computation. +pub struct FeeEstimate { + pub recommended: u64, + pub low: u64, + pub medium: u64, + pub high: u64, + pub sample_count: usize, +} + +impl FeeModel { + /// Compute fee estimates from the current ledger state. + #[instrument(skip(self), fields(config = ?self.config))] + pub fn estimate(&self, ledger: &LedgerState) -> FeeEstimate { + let samples = self.collect_samples(ledger); + let sorted = self.sort_samples(samples); + + let estimate = FeeEstimate { + recommended: sorted.percentile(50), + low: sorted.percentile(10), + medium: sorted.percentile(25), + high: sorted.percentile(75), + sample_count: sorted.len(), + }; + + tracing::info!( + sample_count = estimate.sample_count, + recommended = estimate.recommended, + "fee estimate computed" + ); + + estimate + } +} + +// packages/devkit/src/simulation/network_load.rs +use tracing::instrument; + +impl NetworkLoad { + /// Estimate the current network congestion level as a fraction 0.0 … 1.0. + #[instrument(skip(self))] + pub fn estimate_current_load(&self, recent_txs: &[Transaction]) -> f64 { + if recent_txs.is_empty() { + tracing::warn!("no recent transactions to estimate load"); + return 0.0; + } + + let load = self.compute_load(recent_txs); + let clamped = load.clamp(0.0, 1.0); + + tracing::debug!( + raw_load = load, + clamped_load = clamped, + tx_count = recent_txs.len(), + "network load estimated" + ); + + clamped + } +} + +// packages/devkit/src/simulation/congestion_predictor.rs +use tracing::instrument; + +pub enum CongestionLevel { + Low, + Medium, + High, + Extreme, +} + +impl CongestionPredictor { + /// Predict short-term congestion based on recent and historical data. + #[instrument(skip(self, recent_loads), fields(window = recent_loads.len()))] + pub fn predict_congestion_short_term( + &self, + recent_loads: &[f64], + historical_avg: f64, + ) -> CongestionLevel { + let current_avg = recent_loads.iter().copied().sum::() + / recent_loads.len().max(1) as f64; + + tracing::debug!( + current_avg = current_avg, + historical_avg = historical_avg, + "congestion prediction computed" + ); + + if current_avg > historical_avg * 1.5 { + CongestionLevel::High + } else if current_avg > historical_avg * 1.2 { + CongestionLevel::Medium + } else { + CongestionLevel::Low + } + } + + /// Predict long-term congestion trend. + #[instrument(skip(self))] + pub fn predict_congestion_trend(&self, loads: &[f64]) -> f64 { + let trend = self.compute_trend(loads); + tracing::info!(trend = trend, "long-term congestion trend"); + trend + } +} diff --git a/typed-doc/simulation-test-plan.ts b/typed-doc/simulation-test-plan.ts new file mode 100644 index 0000000..a1a3b43 --- /dev/null +++ b/typed-doc/simulation-test-plan.ts @@ -0,0 +1,60 @@ +/** + * Issue #338: Test plan — fee recommendation & monitoring modules + * + * Tests cover the simulation pipeline, fee recommendation output, + * structured logging, and clipboard interaction. + */ + +// ── 1. Fee Simulation Tests ─────────────────────────────────────────────────── +// +// Test fee_model.rs +// ✓ compute_base_fee returns correct value for given ledger data +// ✓ compute_base_fee handles empty / missing ledger entries +// ✓ compute_base_fee clamps result to [MIN_FEE, MAX_FEE] +// +// Test network_load.rs +// ✓ estimate_current_load returns 0.0 … 1.0 range +// ✓ estimate_current_load handles no recent transactions gracefully +// ✓ load multiplier correctly inflates base fee +// +// Test congestion_predictor.rs +// ✓ predict_congestion returns enum variant for each load band +// ✓ predict_congestion_short_term matches historical pattern +// ✓ predict_congestion integrates with tracing span + +// ── 2. Fee Recommendation Output Tests ──────────────────────────────────────── +// +// Test RecommendationPanel rendering +// ✓ displays recommendedFee, lowFee, mediumFee, highFee +// ✓ formats large numbers with toLocaleString separators +// +// Test clipboard interaction +// ✓ click copies raw integer value (not formatted string) +// ✓ shows "Copied!" label for 1.5 s after click +// ✓ reverts to "Copy" after timeout +// ✓ uses navigator.clipboard.writeText when available +// ✓ falls back to document.execCommand("copy") when clipboard API unavailable +// ✓ does not throw when clipboard write is rejected + +// ── 3. Structured Logging Tests ────────────────────────────────────────────── +// +// Test logging.rs +// ✓ init_logger respects DEVKIT_LOG env var levels (trace/debug/info/warn/error) +// ✓ JSON output format when stdout is not a TTY +// ✓ human-readable format when stdout is a TTY +// ✓ subscriber captures tracing events and spans +// ✓ subscriber does not panic on malformed level strings + +// ── 4. Integration Tests ───────────────────────────────────────────────────── +// +// Test end-to-end fee recommendation +// ✓ simulation → fee calculation → panel render → copy +// ✓ structured log output contains expected fields +// ✓ tracing spans are present in JSON log output + +// ── Test File Locations ─────────────────────────────────────────────────────── +// +// packages/core/src/simulation/ fee_model.rs, network_load.rs, congestion_predictor.rs +// packages/core/src/monitoring/ logging.rs +// packages/ui/src/components/ RecommendationPanel.tsx +// packages/ui/src/__tests__/ RecommendationPanel.test.tsx diff --git a/typed-doc/structured-logging-module.rs b/typed-doc/structured-logging-module.rs new file mode 100644 index 0000000..0b2a7fe --- /dev/null +++ b/typed-doc/structured-logging-module.rs @@ -0,0 +1,83 @@ +/** + * Issue #339: Add structured logging module + * + * Implements a tracing-based structured logger for the devkit. + * + * Requirements: + * - Log level configurable via DEVKIT_LOG env var + * - JSON output for machine readability + * - Human-readable output for local dev (auto-detect TTY) + */ + +// packages/devkit/src/monitoring/logging.rs +use std::env; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; +use tracing_subscriber::fmt::format::FmtSpan; + +/// Initialise the global tracing subscriber. +/// +/// Behaviour: +/// - Reads `DEVKIT_LOG` env var (default: "info") +/// - Auto-detects TTY: human-readable in terminals, JSON elsewhere +/// - Captures span open/close events for accurate timing +pub fn init_logger() { + let log_level = env::var("DEVKIT_LOG").unwrap_or_else(|_| "info".into()); + let is_tty = atty::is(atty::Stream::Stdout); + + let env_filter = EnvFilter::try_new(&log_level) + .unwrap_or_else(|_| EnvFilter::new("info")); + + let fmt_layer = if is_tty { + // Human-friendly output with ANSI colours + fmt::layer() + .pretty() + .with_line_number(true) + .with_target(true) + .with_span_events(FmtSpan::CLOSE) + .boxed() + } else { + // JSON output for machine consumption + fmt::layer() + .json() + .with_current_span(true) + .with_span_list(true) + .with_line_number(true) + .with_file(true) + .boxed() + }; + + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .init(); +} + +#[cfg(test)] +mod tests { + use super::*; + use tracing::info; + + #[test] + fn test_default_level_is_info() { + // When DEVKIT_LOG is unset, info!() should produce output + // but debug!() should not + init_logger(); + info!("this should appear"); + } + + #[test] + fn test_env_var_override() { + env::set_var("DEVKIT_LOG", "debug"); + init_logger(); + // Both info!() and debug!() should produce output now + } +} + +// packages/devkit/src/monitoring/mod.rs +pub mod logging; + +// packages/devkit/Cargo.toml additions: +// [dependencies] +// tracing = "0.1" +// tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } +// atty = "0.2"