diff --git a/Cargo.lock b/Cargo.lock index f30d151ea37a..b0c0684a78c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9224,6 +9224,7 @@ dependencies = [ "axum", "axum-extra", "clap", + "dirs 5.0.1", "futures", "http 1.3.1", "mime_guess", diff --git a/apps/cli/src/domains/mod.rs b/apps/cli/src/domains/mod.rs index 43abe754e16e..7e2cfe143c55 100644 --- a/apps/cli/src/domains/mod.rs +++ b/apps/cli/src/domains/mod.rs @@ -10,6 +10,7 @@ pub mod library; pub mod location; pub mod logs; pub mod network; +pub mod redundancy; pub mod search; pub mod spaces; pub mod sync; diff --git a/apps/cli/src/domains/redundancy/args.rs b/apps/cli/src/domains/redundancy/args.rs new file mode 100644 index 000000000000..e54ddb94fd70 --- /dev/null +++ b/apps/cli/src/domains/redundancy/args.rs @@ -0,0 +1,70 @@ +use clap::Args; +use uuid::Uuid; + +use sd_core::ops::redundancy::summary::RedundancySummaryInput; + +#[derive(Args, Debug)] +pub struct SummaryArgs { + /// Restrict summary to specific volume UUIDs (can be specified multiple times). + /// Omit to summarize the entire library. + #[arg(long = "volume", value_name = "VOLUME_UUID")] + pub volumes: Option>, +} + +impl From for RedundancySummaryInput { + fn from(args: SummaryArgs) -> Self { + Self { + volume_uuids: args.volumes, + } + } +} + +#[derive(Args, Debug)] +pub struct AtRiskArgs { + /// Only return files present on this volume (UUID) + #[arg(long, value_name = "VOLUME_UUID")] + pub volume: Option, + + /// Show redundant files (content on 2+ volumes) instead of at-risk (content on 1 volume) + #[arg(long)] + pub redundant: bool, + + /// Max number of files to show + #[arg(long, default_value = "50")] + pub limit: u32, + + /// Pagination offset + #[arg(long, default_value = "0")] + pub offset: u32, +} + +#[derive(Args, Debug)] +pub struct CompareArgs { + /// First volume UUID + pub volume_a: Uuid, + + /// Second volume UUID + pub volume_b: Uuid, + + /// What to compare + #[arg(long, value_enum, default_value = "unique-a")] + pub mode: CompareMode, + + /// Max number of files to show + #[arg(long, default_value = "50")] + pub limit: u32, + + /// Pagination offset + #[arg(long, default_value = "0")] + pub offset: u32, +} + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum CompareMode { + /// Files present on volume A but not on volume B + UniqueA, + /// Files present on volume B but not on volume A + UniqueB, + /// Files present on both volumes + Shared, +} diff --git a/apps/cli/src/domains/redundancy/mod.rs b/apps/cli/src/domains/redundancy/mod.rs new file mode 100644 index 000000000000..9cb737b7c257 --- /dev/null +++ b/apps/cli/src/domains/redundancy/mod.rs @@ -0,0 +1,314 @@ +//! Redundancy CLI domain +//! +//! Commands for inspecting data redundancy across volumes: +//! - `summary` — per-volume at-risk vs redundant breakdown + replication score +//! - `at-risk` — list files whose content lives on only one volume +//! - `compare` — diff two volumes (unique-to-A, unique-to-B, shared) + +mod args; + +use anyhow::Result; +use clap::Subcommand; +use comfy_table::{presets::UTF8_BORDERS_ONLY, Attribute, Cell, Table}; + +use crate::context::Context; +use crate::util::prelude::*; + +use sd_core::ops::redundancy::summary::{RedundancySummaryInput, RedundancySummaryOutput}; +use sd_core::ops::search::{ + input::{ + FileSearchInput, PaginationOptions, SearchFilters, SearchMode, SearchScope, SortDirection, + SortField, SortOptions, + }, + output::FileSearchOutput, +}; + +use self::args::*; + +#[derive(Subcommand, Debug)] +pub enum RedundancyCmd { + /// Show redundancy summary (replication score + per-volume breakdown) + Summary(SummaryArgs), + /// List at-risk files (content that only lives on one volume) + AtRisk(AtRiskArgs), + /// Compare two volumes (unique-to-A, unique-to-B, or shared) + Compare(CompareArgs), +} + +pub async fn run(ctx: &Context, cmd: RedundancyCmd) -> Result<()> { + match cmd { + RedundancyCmd::Summary(args) => run_summary(ctx, args).await, + RedundancyCmd::AtRisk(args) => run_at_risk(ctx, args).await, + RedundancyCmd::Compare(args) => run_compare(ctx, args).await, + } +} + +async fn run_summary(ctx: &Context, args: SummaryArgs) -> Result<()> { + let input: RedundancySummaryInput = args.into(); + let out: RedundancySummaryOutput = execute_query!(ctx, input); + + print_output!(ctx, &out, |o: &RedundancySummaryOutput| { + render_summary(o); + }); + Ok(()) +} + +async fn run_at_risk(ctx: &Context, args: AtRiskArgs) -> Result<()> { + let input = FileSearchInput { + query: String::new(), + scope: SearchScope::Library, + mode: SearchMode::Fast, + filters: SearchFilters { + at_risk: Some(!args.redundant), + on_volumes: args.volume.map(|v| vec![v]), + ..Default::default() + }, + sort: SortOptions { + field: SortField::Size, + direction: SortDirection::Desc, + }, + pagination: PaginationOptions { + limit: args.limit, + offset: args.offset, + }, + }; + + let out: FileSearchOutput = execute_query!(ctx, input); + let label = if args.redundant { "redundant" } else { "at-risk" }; + + print_output!(ctx, &out, |o: &FileSearchOutput| { + render_file_list(o, label); + }); + Ok(()) +} + +async fn run_compare(ctx: &Context, args: CompareArgs) -> Result<()> { + let (on, not_on, min_count, label) = match args.mode { + CompareMode::UniqueA => ( + Some(vec![args.volume_a]), + Some(vec![args.volume_b]), + None, + "unique to A", + ), + CompareMode::UniqueB => ( + Some(vec![args.volume_b]), + Some(vec![args.volume_a]), + None, + "unique to B", + ), + CompareMode::Shared => ( + Some(vec![args.volume_a, args.volume_b]), + None, + Some(2u32), + "shared", + ), + }; + + let input = FileSearchInput { + query: String::new(), + scope: SearchScope::Library, + mode: SearchMode::Fast, + filters: SearchFilters { + on_volumes: on, + not_on_volumes: not_on, + min_volume_count: min_count, + ..Default::default() + }, + sort: SortOptions { + field: SortField::Size, + direction: SortDirection::Desc, + }, + pagination: PaginationOptions { + limit: args.limit, + offset: args.offset, + }, + }; + + let out: FileSearchOutput = execute_query!(ctx, input); + + print_output!(ctx, &out, |o: &FileSearchOutput| { + render_file_list(o, label); + }); + Ok(()) +} + +// ─── rendering helpers ──────────────────────────────────────────────────────── + +fn render_summary(o: &RedundancySummaryOutput) { + let totals = &o.library_totals; + + // Library-wide header + let mut overview = Table::new(); + overview.load_preset(UTF8_BORDERS_ONLY); + overview.set_header(vec![ + Cell::new("Redundancy Summary").add_attribute(Attribute::Bold), + Cell::new(""), + ]); + overview.add_row(vec![ + Cell::new("Unique content"), + Cell::new(format_bytes_i64(totals.total_unique_content_bytes)), + ]); + overview.add_row(vec![ + Cell::new("At risk"), + Cell::new(format!( + "{} ({})", + format_bytes_i64(totals.total_at_risk_bytes), + percent_of(totals.total_at_risk_bytes, totals.total_unique_content_bytes), + )), + ]); + overview.add_row(vec![ + Cell::new("Redundant"), + Cell::new(format!( + "{} ({})", + format_bytes_i64(totals.total_redundant_bytes), + percent_of(totals.total_redundant_bytes, totals.total_unique_content_bytes), + )), + ]); + overview.add_row(vec![ + Cell::new("Replication score"), + Cell::new(format!( + "{:.2} ({:.1}%)", + totals.replication_score, + totals.replication_score * 100.0 + )), + ]); + println!("{}", overview); + println!(); + + // Per-volume breakdown + if o.volumes.is_empty() { + println!("No volumes with indexed content found."); + return; + } + + let mut table = Table::new(); + table.load_preset(UTF8_BORDERS_ONLY); + table.set_header(vec![ + Cell::new("Volume").add_attribute(Attribute::Bold), + Cell::new("Total").add_attribute(Attribute::Bold), + Cell::new("At-Risk").add_attribute(Attribute::Bold), + Cell::new("Redundant").add_attribute(Attribute::Bold), + Cell::new("Files").add_attribute(Attribute::Bold), + ]); + + for v in &o.volumes { + let name = v + .display_name + .as_deref() + .map(|n| n.to_string()) + .unwrap_or_else(|| v.volume_uuid.to_string()); + + let at_risk = format!( + "{} ({})", + format_bytes_i64(v.at_risk_bytes), + percent_of(v.at_risk_bytes, v.total_bytes), + ); + let redundant = format!( + "{} ({})", + format_bytes_i64(v.redundant_bytes), + percent_of(v.redundant_bytes, v.total_bytes), + ); + let files = format!( + "{} ({} at-risk / {} redundant)", + v.total_file_count, v.at_risk_file_count, v.redundant_file_count + ); + + table.add_row(vec![ + Cell::new(name), + Cell::new(format_bytes_i64(v.total_bytes)), + Cell::new(at_risk), + Cell::new(redundant), + Cell::new(files), + ]); + + // UUID on a subline for precision + table.add_row(vec![ + Cell::new(format!(" {}", v.volume_uuid)), + Cell::new(""), + Cell::new(""), + Cell::new(""), + Cell::new(""), + ]); + } + + println!("{}", table); +} + +fn render_file_list(o: &FileSearchOutput, label: &str) { + if o.files.is_empty() { + println!("No {} files found.", label); + return; + } + + println!( + "Showing {} of {} {} file(s) ({}ms)", + o.files.len(), + o.total_found, + label, + o.execution_time_ms + ); + println!(); + + let mut table = Table::new(); + table.load_preset(UTF8_BORDERS_ONLY); + table.set_header(vec![ + Cell::new("#").add_attribute(Attribute::Bold), + Cell::new("Name").add_attribute(Attribute::Bold), + Cell::new("Size").add_attribute(Attribute::Bold), + Cell::new("Ext").add_attribute(Attribute::Bold), + Cell::new("Modified").add_attribute(Attribute::Bold), + Cell::new("Path").add_attribute(Attribute::Bold), + ]); + + for (i, f) in o.files.iter().enumerate() { + table.add_row(vec![ + Cell::new((i + 1).to_string()), + Cell::new(truncate(&f.name, 48)), + Cell::new(format_bytes_u64(f.size)), + Cell::new(f.extension.clone().unwrap_or_default()), + Cell::new(f.modified_at.format("%Y-%m-%d %H:%M").to_string()), + Cell::new(truncate(&f.sd_path.display().to_string(), 60)), + ]); + } + + println!("{}", table); +} + +fn percent_of(part: i64, total: i64) -> String { + if total <= 0 { + return "0%".into(); + } + format!("{:.1}%", (part as f64 / total as f64) * 100.0) +} + +fn format_bytes_i64(bytes: i64) -> String { + if bytes < 0 { + return format!("-{}", format_bytes_u64(bytes.unsigned_abs())); + } + format_bytes_u64(bytes as u64) +} + +fn format_bytes_u64(bytes: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"]; + let mut size = bytes as f64; + let mut unit = 0; + while size >= 1024.0 && unit < UNITS.len() - 1 { + size /= 1024.0; + unit += 1; + } + if unit == 0 { + format!("{} {}", bytes, UNITS[unit]) + } else { + format!("{:.1} {}", size, UNITS[unit]) + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + let mut out: String = s.chars().take(max.saturating_sub(1)).collect(); + out.push('…'); + out + } +} diff --git a/apps/cli/src/domains/search/args.rs b/apps/cli/src/domains/search/args.rs index 3634bd5b281a..e190ce9b6259 100644 --- a/apps/cli/src/domains/search/args.rs +++ b/apps/cli/src/domains/search/args.rs @@ -220,6 +220,11 @@ impl From for FileSearchInput { }), include_hidden: Some(args.include_hidden), include_archived: Some(args.include_archived), + at_risk: None, + on_volumes: None, + not_on_volumes: None, + min_volume_count: None, + max_volume_count: None, }; let sort = SortOptions { diff --git a/apps/cli/src/domains/volume/mod.rs b/apps/cli/src/domains/volume/mod.rs index 62db9b373e97..c1f6774f73ab 100644 --- a/apps/cli/src/domains/volume/mod.rs +++ b/apps/cli/src/domains/volume/mod.rs @@ -87,8 +87,15 @@ pub async fn run(ctx: &Context, cmd: VolumeCmd) -> Result<()> { println!(" Fingerprint: {}", volume.fingerprint); println!(" Type: {:?}", volume.volume_type); println!(" Mount: {}", volume.mount_point.display()); - println!(" Mounted: {}", volume.is_mounted); - println!(" Tracked: {}", volume.is_tracked); + println!( + " Capacity: {} total, {} available", + format_bytes(volume.total_capacity), + format_bytes(volume.available_space), + ); + println!( + " Visible: {}, Tracked: {}, Mounted: {}", + volume.is_user_visible, volume.is_tracked, volume.is_mounted, + ); println!(); } } @@ -99,3 +106,21 @@ pub async fn run(ctx: &Context, cmd: VolumeCmd) -> Result<()> { } Ok(()) } + +fn format_bytes(bytes: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"]; + if bytes == 0 { + return "0 B".to_string(); + } + let mut value = bytes as f64; + let mut unit = 0; + while value >= 1024.0 && unit < UNITS.len() - 1 { + value /= 1024.0; + unit += 1; + } + if unit == 0 { + format!("{} {}", bytes, UNITS[unit]) + } else { + format!("{:.2} {}", value, UNITS[unit]) + } +} diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs index f21a7745e76f..741f8e7b2303 100644 --- a/apps/cli/src/main.rs +++ b/apps/cli/src/main.rs @@ -58,6 +58,7 @@ use crate::domains::{ location::{self, LocationCmd}, logs::{self, LogsCmd}, network::{self, NetworkCmd}, + redundancy::{self, RedundancyCmd}, search::{self, SearchCmd}, spaces::{self, SpacesCmd}, sync::{self, SyncCmd}, @@ -207,6 +208,9 @@ enum Commands { /// View and follow logs #[command(subcommand)] Logs(LogsCmd), + /// Redundancy / cross-volume replication awareness + #[command(subcommand)] + Redundancy(RedundancyCmd), /// Search operations #[command(subcommand)] Search(SearchCmd), @@ -707,6 +711,7 @@ async fn run_client_command( Commands::Job(cmd) => job::run(&ctx, cmd).await?, Commands::Sync(cmd) => sync::run(&ctx, cmd).await?, Commands::Logs(cmd) => logs::run(&ctx, cmd).await?, + Commands::Redundancy(cmd) => redundancy::run(&ctx, cmd).await?, Commands::Search(cmd) => search::run(&ctx, cmd).await?, Commands::Spaces(cmd) => spaces::exec(cmd, &ctx).await?, Commands::Tag(cmd) => tag::run(&ctx, cmd).await?, diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 18bec306bf7c..da468a8de17b 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -47,6 +47,9 @@ thiserror = "1" # CLI clap = { version = "4", features = ["derive"] } +# Default data directory resolution in dev mode +dirs = "5.0" + # Dev dependencies tempfile = "3" diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 9d1196ea396f..ca8b19916d1c 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -43,11 +43,13 @@ COPY apps/server ./apps/server # Embedded web UI assets — must be pre-built before `docker build` runs. COPY apps/web/dist ./apps/web/dist -# Build server with media processing features +# Build server with media processing features. +# SD_SKIP_WEB_BUILD tells build.rs to use the pre-built apps/web/dist instead +# of trying to run `bun run build` (bun isn't installed in this stage). RUN --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/root/.cargo/git \ --mount=type=cache,target=/build/target \ - cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg && \ + SD_SKIP_WEB_BUILD=1 cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg && \ cp target/release/sd-server /usr/local/bin/sd-server #-- diff --git a/apps/server/build.rs b/apps/server/build.rs new file mode 100644 index 000000000000..7954b31ca3fd --- /dev/null +++ b/apps/server/build.rs @@ -0,0 +1,101 @@ +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=SD_SKIP_WEB_BUILD"); + + if env::var_os("SD_SKIP_WEB_BUILD").is_some() { + println!("cargo:warning=SD_SKIP_WEB_BUILD set — using existing apps/web/dist"); + return; + } + + // If bun isn't available (e.g., Docker Rust build stage), the caller is + // expected to have prebuilt apps/web/dist. Skip silently. + if Command::new("bun").arg("--version").output().is_err() { + println!("cargo:warning=bun not found on PATH — using existing apps/web/dist"); + return; + } + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo_root = manifest_dir + .parent() + .and_then(Path::parent) + .expect("apps/server is two levels below the repo root") + .to_path_buf(); + let web_dir = repo_root.join("apps/web"); + + // Refuse to proceed if workspace dependencies aren't installed. + if !repo_root.join("node_modules").exists() { + panic!( + "node_modules missing at {} — run `bun install` (or `just setup`) before building sd-server, \ + or set SD_SKIP_WEB_BUILD=1 to skip the embedded UI build.", + repo_root.display() + ); + } + + // Invalidate the build script when any UI source or relevant config changes. + // Cargo will cache this build script's output otherwise, so Rust-only changes + // won't pay the cost of rebuilding the web bundle. + watch_dir(&web_dir.join("src")); + watch_dir(&repo_root.join("packages/interface/src")); + watch_dir(&repo_root.join("packages/ts-client/src")); + for path in [ + web_dir.join("index.html"), + web_dir.join("vite.config.ts"), + web_dir.join("package.json"), + web_dir.join("tsconfig.json"), + repo_root.join("packages/interface/package.json"), + repo_root.join("packages/ts-client/package.json"), + ] { + if path.exists() { + rerun(&path); + } + } + + let status = Command::new("bun") + .args(["run", "build"]) + .current_dir(&web_dir) + .status() + .expect("failed to spawn `bun run build`"); + + if !status.success() { + panic!( + "`bun run build` in {} failed with status {}", + web_dir.display(), + status + ); + } +} + +fn watch_dir(dir: &Path) { + if !dir.exists() { + return; + } + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries.flatten() { + let path = entry.path(); + let file_type = match entry.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + if file_type.is_dir() { + if let Some(name) = path.file_name().and_then(|s| s.to_str()) { + if matches!(name, "node_modules" | "dist" | ".turbo" | "build" | ".next") { + continue; + } + } + watch_dir(&path); + } else if file_type.is_file() { + rerun(&path); + } + } +} + +fn rerun(path: &Path) { + println!("cargo:rerun-if-changed={}", path.display()); +} diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 3eaf0b2d6bd3..e7c02e4a40d6 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -301,9 +301,19 @@ async fn main() -> Result<(), Box> { } #[cfg(debug_assertions)] { + // Default to `~/.spacedrive` in dev — matches the Tauri desktop app, + // so `just dev-server` and `just dev-desktop` share libraries and + // data persists between runs. Falls back to a tempdir only if the + // home directory can't be resolved. std::env::var("DATA_DIR") .map(PathBuf::from) + .or_else(|_| { + dirs::home_dir() + .map(|h| h.join(".spacedrive")) + .ok_or(()) + }) .unwrap_or_else(|_| { + warn!("Could not resolve home directory; falling back to tempdir"); let temp = tempfile::tempdir().expect("Failed to create temp dir"); temp.path().to_path_buf() }) @@ -496,7 +506,13 @@ async fn is_daemon_running(socket_addr: &str) -> bool { ) } -/// Graceful shutdown handler +/// Graceful shutdown handler. +/// +/// On first signal: logs, aborts the daemon task (if we started it), and arms +/// a background force-exit that fires on either a second Ctrl+C or a 5-second +/// timeout. This returns immediately so axum can begin its graceful shutdown, +/// but long-held connections (e.g. the `/events` SSE stream held open by a +/// browser) can't block the process indefinitely. async fn shutdown_signal(daemon_handle: Option>>>) { let ctrl_c = async { signal::ctrl_c() @@ -517,10 +533,10 @@ async fn shutdown_signal(daemon_handle: Option { - info!("Received Ctrl+C, shutting down gracefully..."); + info!("Received Ctrl+C, shutting down gracefully (5s timeout)..."); } () = terminate => { - info!("Received SIGTERM, shutting down gracefully..."); + info!("Received SIGTERM, shutting down gracefully (5s timeout)..."); } } @@ -528,4 +544,18 @@ async fn shutdown_signal(daemon_handle: Option { + warn!("Second signal received, forcing exit"); + } + _ = tokio::time::sleep(Duration::from_secs(5)) => { + warn!("Graceful shutdown timed out after 5s, forcing exit"); + } + } + std::process::exit(0); + }); } diff --git a/build-server.sh b/build-server.sh new file mode 100755 index 000000000000..ac644a10585e --- /dev/null +++ b/build-server.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Build sd-server + sd-cli natively on TrueNAS Scale +# Uses zig cc as C compiler (since gcc/clang not installed) +# Dev tools at /mnt/pool/dev-tools/ + +set -e +SR=/mnt/pool/dev-tools/sysroot +export BINDGEN_EXTRA_CLANG_ARGS="-I$SR/usr/lib/gcc/x86_64-linux-gnu/12/include -I$SR/usr/include -I$SR/usr/include/x86_64-linux-gnu" +export PATH="/mnt/pool/dev-tools:/mnt/pool/dev-tools/bin:/mnt/pool/dev-tools/sysroot/usr/bin:$PATH" +export CC=/mnt/pool/dev-tools/cc +export CXX="/mnt/pool/dev-tools/c++" +export AR=/mnt/pool/dev-tools/ar +export C_INCLUDE_PATH="$SR/usr/include:$SR/usr/include/x86_64-linux-gnu:$SR/usr/lib/gcc/x86_64-linux-gnu/12/include" +export CPLUS_INCLUDE_PATH="$C_INCLUDE_PATH" +export OPENSSL_INCLUDE_DIR="$SR/usr/include" +export OPENSSL_LIB_DIR="$SR/usr/lib/x86_64-linux-gnu" + +cd /mnt/pool/spacedrive +cargo build --release --bin sd-server --bin sd-cli \ + --features sd-core/heif,sd-core/ffmpeg \ + -j10 "$@" + +echo "Binaries at:" +ls -lh target/release/sd-server target/release/sd-cli 2>/dev/null diff --git a/core/src/domain/space.rs b/core/src/domain/space.rs index 5ed2e643dd29..e0f561cb1dbe 100644 --- a/core/src/domain/space.rs +++ b/core/src/domain/space.rs @@ -445,6 +445,9 @@ pub enum ItemType { /// Specific archive data source Source { source_id: String }, + + /// Redundancy awareness dashboard + Redundancy, } /// Complete sidebar layout for a space #[derive(Debug, Clone, Serialize, Deserialize, Type)] diff --git a/core/src/infra/db/migration/m20260414_000001_add_redundancy_indexes.rs b/core/src/infra/db/migration/m20260414_000001_add_redundancy_indexes.rs new file mode 100644 index 000000000000..884acd1d8fc3 --- /dev/null +++ b/core/src/infra/db/migration/m20260414_000001_add_redundancy_indexes.rs @@ -0,0 +1,37 @@ +//! Add composite index on entries(content_id, volume_id) for redundancy queries +//! +//! The redundancy feature needs to GROUP BY content_id and COUNT(DISTINCT volume_id) +//! across the entries table. Without this composite index, those queries would +//! require a full table scan on large libraries. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Composite index for redundancy queries: "which content exists on which volumes?" + // This covers GROUP BY content_id + COUNT(DISTINCT volume_id) patterns + manager + .get_connection() + .execute_unprepared( + "CREATE INDEX IF NOT EXISTS idx_entries_content_volume \ + ON entries(content_id, volume_id) \ + WHERE content_id IS NOT NULL AND volume_id IS NOT NULL", + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared("DROP INDEX IF EXISTS idx_entries_content_volume") + .await?; + + Ok(()) + } +} diff --git a/core/src/infra/db/migration/mod.rs b/core/src/infra/db/migration/mod.rs index 1a301e8d93f8..35f88974e94f 100644 --- a/core/src/infra/db/migration/mod.rs +++ b/core/src/infra/db/migration/mod.rs @@ -38,6 +38,7 @@ mod m20260104_000001_replace_device_id_with_volume_id; mod m20260105_000001_add_volume_id_to_locations; mod m20260114_000001_fix_search_index_include_directories; mod m20260123_000001_remove_legacy_sync_columns; +mod m20260414_000001_add_redundancy_indexes; pub struct Migrator; @@ -81,6 +82,7 @@ impl MigratorTrait for Migrator { Box::new(m20260105_000001_add_volume_id_to_locations::Migration), Box::new(m20260114_000001_fix_search_index_include_directories::Migration), Box::new(m20260123_000001_remove_legacy_sync_columns::Migration), + Box::new(m20260414_000001_add_redundancy_indexes::Migration), ] } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 33a92292bd83..5d5cf8c21487 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -520,10 +520,27 @@ impl Core { // Register protocols and set up event bridge if let Some(networking_service) = self.services.networking() { - // Register default protocol handlers only if networking was just initialized - // (if networking was already initialized during Core::new(), protocols are already registered) - if !already_initialized { - logger.info("Registering protocol handlers...").await; + // Core::new() attempts to register default protocols after starting + // networking, but swallows errors (only logs them). That means + // `already_initialized` does NOT imply protocols are registered — + // e.g. if the event loop's command sender wasn't ready when Core::new() + // tried to build the pairing handler, registration silently failed. + // Check the registry directly and re-register if missing. + let pairing_registered = networking_service + .protocol_registry() + .read() + .await + .get_handler("pairing") + .is_some(); + + if !already_initialized || !pairing_registered { + if already_initialized && !pairing_registered { + logger + .warn("Networking was initialized but protocol handlers are missing; re-registering") + .await; + } else { + logger.info("Registering protocol handlers...").await; + } self.register_default_protocols(&networking_service).await?; } else { logger diff --git a/core/src/library/manager.rs b/core/src/library/manager.rs index 61b016f16ffe..66effbea5c38 100644 --- a/core/src/library/manager.rs +++ b/core/src/library/manager.rs @@ -1239,12 +1239,15 @@ impl LibraryManager { info!("Created default space for library {}", library.id()); - // Create space-level items (Overview, Recents, Favorites, File Kinds) - these appear outside groups + // Create space-level items (Overview, Recents, Favorites, File Kinds, + // Sources, Redundancy) - these appear outside groups let space_items = vec![ (ItemType::Overview, "Overview", 0), (ItemType::Recents, "Recents", 1), (ItemType::Favorites, "Favorites", 2), (ItemType::FileKinds, "File Kinds", 3), + (ItemType::Sources, "Sources", 4), + (ItemType::Redundancy, "Redundancy", 5), ]; use crate::infra::db::entities::space_item::{Column as ItemColumn, Entity as ItemEntity}; diff --git a/core/src/library/mod.rs b/core/src/library/mod.rs index 60a894169391..9edef98d93ce 100644 --- a/core/src/library/mod.rs +++ b/core/src/library/mod.rs @@ -1256,6 +1256,20 @@ impl Library { "Primary" | "UserData" | "External" | "Secondary" ) }) + // Drop volumes the user can't see. Honor the persisted flag *and* + // re-check the current platform visibility rules so stale DB rows + // (created before the filter logic existed) don't inflate the + // library's reported capacity. Without this, every ZFS dataset on + // a TrueNAS pool gets counted even though they share storage. + .filter(|vol| { + vol.is_user_visible.unwrap_or(true) + && !vol + .mount_point + .as_deref() + .map(std::path::Path::new) + .map(crate::volume::utils::should_hide_by_mount_path) + .unwrap_or(false) + }) .collect(); // Deduplicate by fingerprint first (same physical volume tracked multiple times) @@ -1669,6 +1683,20 @@ impl Library { "Primary" | "UserData" | "External" | "Secondary" ) }) + // Drop volumes the user can't see. Honor the persisted flag *and* + // re-check the current platform visibility rules so stale DB rows + // (created before the filter logic existed) don't inflate the + // library's reported capacity. Without this, every ZFS dataset on + // a TrueNAS pool gets counted even though they share storage. + .filter(|vol| { + vol.is_user_visible.unwrap_or(true) + && !vol + .mount_point + .as_deref() + .map(std::path::Path::new) + .map(crate::volume::utils::should_hide_by_mount_path) + .unwrap_or(false) + }) .collect(); // Deduplicate by fingerprint first (same physical volume tracked multiple times) diff --git a/core/src/ops/libraries/info/query.rs b/core/src/ops/libraries/info/query.rs index d96ba387dd0f..c9c85e873deb 100644 --- a/core/src/ops/libraries/info/query.rs +++ b/core/src/ops/libraries/info/query.rs @@ -63,32 +63,29 @@ impl LibraryQuery for LibraryInfoQuery { && cached_stats.tag_count == 0; let statistics = if is_stale { - // First load or completely empty - calculate synchronously - tracing::debug!( + // First load or completely empty — return zeros immediately and + // calculate in the background. The synchronous path used to + // block here, but on large libraries (e.g. NAS with millions of + // files being indexed) the closure-table walk in + // calculate_file_statistics can take minutes, locking up the RPC + // endpoint and making the UI unresponsive. The background task + // emits a ResourceChanged event when done so the UI refreshes. + tracing::info!( library_id = %library_id, library_name = %config.name, - "Cached statistics are empty, calculating synchronously for first load" + "Cached statistics are empty, returning zeros and calculating in background" ); - let stats = library - .calculate_statistics_for_query() - .await - .map_err(|e| { - QueryError::Internal(format!("Failed to calculate statistics: {}", e)) - })?; - - // Also trigger background save and event emission - // (non-blocking, happens after we return the stats to the user) if let Err(e) = library.recalculate_statistics().await { tracing::warn!( library_id = %library_id, library_name = %config.name, error = %e, - "Failed to trigger background statistics save after sync calculation" + "Failed to trigger background statistics calculation" ); } - stats + cached_stats } else { // Return cached statistics immediately (non-blocking) tracing::debug!( diff --git a/core/src/ops/mod.rs b/core/src/ops/mod.rs index cdb15fc55fef..425e0dc23a9b 100644 --- a/core/src/ops/mod.rs +++ b/core/src/ops/mod.rs @@ -24,6 +24,7 @@ pub mod media; pub mod metadata; pub mod models; pub mod network; +pub mod redundancy; pub mod search; pub mod sidecar; pub mod sources; diff --git a/core/src/ops/redundancy/mod.rs b/core/src/ops/redundancy/mod.rs new file mode 100644 index 000000000000..6a12857def87 --- /dev/null +++ b/core/src/ops/redundancy/mod.rs @@ -0,0 +1,7 @@ +//! Redundancy awareness operations +//! +//! Provides queries for understanding data redundancy across volumes: +//! - Summary statistics (per-volume at-risk vs redundant bytes) +//! - Integration with search filters for file-level redundancy queries + +pub mod summary; diff --git a/core/src/ops/redundancy/summary/input.rs b/core/src/ops/redundancy/summary/input.rs new file mode 100644 index 000000000000..71022aa3671b --- /dev/null +++ b/core/src/ops/redundancy/summary/input.rs @@ -0,0 +1,13 @@ +//! Input types for redundancy summary query + +use serde::{Deserialize, Serialize}; +use specta::Type; +use uuid::Uuid; + +/// Input for the redundancy summary query +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct RedundancySummaryInput { + /// Optional: restrict summary to specific volumes. None = all volumes. + #[serde(default)] + pub volume_uuids: Option>, +} diff --git a/core/src/ops/redundancy/summary/mod.rs b/core/src/ops/redundancy/summary/mod.rs new file mode 100644 index 000000000000..daf4a68dbffd --- /dev/null +++ b/core/src/ops/redundancy/summary/mod.rs @@ -0,0 +1,9 @@ +//! Redundancy summary query + +pub mod input; +pub mod output; +pub mod query; + +pub use input::*; +pub use output::*; +pub use query::*; diff --git a/core/src/ops/redundancy/summary/output.rs b/core/src/ops/redundancy/summary/output.rs new file mode 100644 index 000000000000..55d831b0ba11 --- /dev/null +++ b/core/src/ops/redundancy/summary/output.rs @@ -0,0 +1,48 @@ +//! Output types for redundancy summary query + +use serde::{Deserialize, Serialize}; +use specta::Type; +use uuid::Uuid; + +/// Complete redundancy summary for the library +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct RedundancySummaryOutput { + /// Per-volume redundancy breakdown + pub volumes: Vec, + /// Library-wide totals + pub library_totals: LibraryRedundancyTotals, +} + +/// Redundancy breakdown for a single volume +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct VolumeRedundancySummary { + /// Volume UUID + pub volume_uuid: Uuid, + /// Display name of the volume + pub display_name: Option, + /// Total bytes of file content on this volume (deduplicated within volume) + pub total_bytes: i64, + /// Bytes of content unique to this volume (at risk if volume is lost) + pub at_risk_bytes: i64, + /// Number of files whose content only exists on this volume + pub at_risk_file_count: u32, + /// Bytes of content that also exists on at least one other volume + pub redundant_bytes: i64, + /// Number of files whose content exists on other volumes too + pub redundant_file_count: u32, + /// Total number of files on this volume + pub total_file_count: u32, +} + +/// Library-wide redundancy totals +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct LibraryRedundancyTotals { + /// Total unique content bytes across the entire library (deduplicated) + pub total_unique_content_bytes: i64, + /// Content bytes that exist on only one volume + pub total_at_risk_bytes: i64, + /// Content bytes that exist on two or more volumes + pub total_redundant_bytes: i64, + /// Ratio of redundant to total content (0.0 = nothing replicated, 1.0 = everything replicated) + pub replication_score: f64, +} diff --git a/core/src/ops/redundancy/summary/query.rs b/core/src/ops/redundancy/summary/query.rs new file mode 100644 index 000000000000..16ac13d112c8 --- /dev/null +++ b/core/src/ops/redundancy/summary/query.rs @@ -0,0 +1,295 @@ +//! Redundancy summary query implementation + +use super::{ + input::RedundancySummaryInput, + output::{LibraryRedundancyTotals, RedundancySummaryOutput, VolumeRedundancySummary}, +}; +use crate::{ + context::CoreContext, + infra::{ + db::entities, + query::{LibraryQuery, QueryError, QueryResult}, + }, +}; +use sea_orm::{ + ColumnTrait, ConnectionTrait, DbBackend, EntityTrait, FromQueryResult, QueryFilter, Statement, +}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::{collections::HashMap, sync::Arc}; +use uuid::Uuid; + +/// Redundancy summary query +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct RedundancySummaryQuery { + pub input: RedundancySummaryInput, +} + +/// Row result for per-volume redundancy stats +#[derive(FromQueryResult)] +struct VolumeRedundancyRow { + volume_id: i32, + file_count: i64, + content_bytes: i64, +} + +/// Row result for per-volume total file stats +#[derive(FromQueryResult)] +struct VolumeTotalRow { + volume_id: i32, + total_file_count: i64, + total_bytes: i64, +} + +/// Row result for library-wide unique content totals +#[derive(FromQueryResult)] +struct LibraryTotalRow { + total_bytes: i64, +} + +impl LibraryQuery for RedundancySummaryQuery { + type Input = RedundancySummaryInput; + type Output = RedundancySummaryOutput; + + fn from_input(input: Self::Input) -> QueryResult { + Ok(Self { input }) + } + + async fn execute( + self, + context: Arc, + session: crate::infra::api::SessionContext, + ) -> QueryResult { + let library_id = session + .current_library_id + .ok_or_else(|| QueryError::Internal("No library in session".to_string()))?; + + let library = context + .libraries() + .await + .get_library(library_id) + .await + .ok_or_else(|| QueryError::Internal("Library not found".to_string()))?; + + let db = library.db().conn(); + + // Build volume UUID filter if scoped + let volume_id_filter = if let Some(ref uuids) = self.input.volume_uuids { + if uuids.is_empty() { + None + } else { + // Resolve UUIDs to internal IDs + let volumes = entities::volume::Entity::find() + .filter(entities::volume::Column::Uuid.is_in(uuids.clone())) + .all(db) + .await?; + Some(volumes.iter().map(|v| v.id).collect::>()) + } + } else { + None + }; + + // Fetch all visible volumes for display names and UUID mapping + let all_volumes = entities::volume::Entity::find() + .filter(entities::volume::Column::IsUserVisible.eq(true)) + .all(db) + .await?; + + let volume_id_to_uuid: HashMap = + all_volumes.iter().map(|v| (v.id, v.uuid)).collect(); + let volume_id_to_name: HashMap> = + all_volumes.iter().map(|v| (v.id, v.display_name.clone())).collect(); + + // Helper to build volume ID WHERE clause + let volume_where = match &volume_id_filter { + Some(ids) => { + let id_list = ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(","); + format!("AND e.volume_id IN ({})", id_list) + } + None => String::new(), + }; + + // Query 1: Per-volume at-risk content (content existing on exactly one volume) + let at_risk_sql = format!( + r#" + SELECT e.volume_id as volume_id, + COUNT(*) as file_count, + COALESCE(SUM(ci.total_size), 0) as content_bytes + FROM entries e + INNER JOIN content_identities ci ON e.content_id = ci.id + WHERE e.content_id IS NOT NULL + AND e.volume_id IS NOT NULL + AND e.kind = 0 + {} + AND e.content_id IN ( + SELECT e2.content_id FROM entries e2 + WHERE e2.content_id IS NOT NULL AND e2.volume_id IS NOT NULL + GROUP BY e2.content_id + HAVING COUNT(DISTINCT e2.volume_id) = 1 + ) + GROUP BY e.volume_id + "#, + volume_where + ); + + let at_risk_rows = VolumeRedundancyRow::find_by_statement( + Statement::from_string(DbBackend::Sqlite, at_risk_sql), + ) + .all(db) + .await?; + + // Query 2: Per-volume redundant content (content existing on 2+ volumes) + let redundant_sql = format!( + r#" + SELECT e.volume_id as volume_id, + COUNT(*) as file_count, + COALESCE(SUM(ci.total_size), 0) as content_bytes + FROM entries e + INNER JOIN content_identities ci ON e.content_id = ci.id + WHERE e.content_id IS NOT NULL + AND e.volume_id IS NOT NULL + AND e.kind = 0 + {} + AND e.content_id IN ( + SELECT e2.content_id FROM entries e2 + WHERE e2.content_id IS NOT NULL AND e2.volume_id IS NOT NULL + GROUP BY e2.content_id + HAVING COUNT(DISTINCT e2.volume_id) > 1 + ) + GROUP BY e.volume_id + "#, + volume_where + ); + + let redundant_rows = VolumeRedundancyRow::find_by_statement( + Statement::from_string(DbBackend::Sqlite, redundant_sql), + ) + .all(db) + .await?; + + // Query 3: Per-volume total file counts + let totals_sql = format!( + r#" + SELECT e.volume_id as volume_id, + COUNT(*) as total_file_count, + COALESCE(SUM(e.size), 0) as total_bytes + FROM entries e + WHERE e.volume_id IS NOT NULL + AND e.kind = 0 + {} + GROUP BY e.volume_id + "#, + volume_where + ); + + let total_rows = VolumeTotalRow::find_by_statement(Statement::from_string( + DbBackend::Sqlite, + totals_sql, + )) + .all(db) + .await?; + + // Query 4: Library-wide unique content total (deduplicated) + let library_total_sql = r#" + SELECT COALESCE(SUM(ci.total_size), 0) as total_bytes + FROM content_identities ci + WHERE ci.id IN ( + SELECT DISTINCT e.content_id FROM entries e + WHERE e.content_id IS NOT NULL AND e.volume_id IS NOT NULL + ) + "#; + + let library_total = LibraryTotalRow::find_by_statement(Statement::from_string( + DbBackend::Sqlite, + library_total_sql.to_string(), + )) + .one(db) + .await?; + + // Build lookup maps + let mut at_risk_map: HashMap = HashMap::new(); + for row in &at_risk_rows { + at_risk_map.insert(row.volume_id, row); + } + + let mut redundant_map: HashMap = HashMap::new(); + for row in &redundant_rows { + redundant_map.insert(row.volume_id, row); + } + + let mut totals_map: HashMap = HashMap::new(); + for row in &total_rows { + totals_map.insert(row.volume_id, row); + } + + // Build per-volume summaries + let mut volumes = Vec::new(); + let mut lib_at_risk_bytes: i64 = 0; + let mut lib_redundant_bytes: i64 = 0; + + // Determine which volume IDs to include + let volume_ids: Vec = match &volume_id_filter { + Some(ids) => ids.clone(), + None => { + let mut ids: Vec = totals_map.keys().copied().collect(); + ids.sort(); + ids + } + }; + + for vol_id in &volume_ids { + let volume_uuid = match volume_id_to_uuid.get(vol_id) { + Some(uuid) => *uuid, + None => continue, + }; + + let at_risk = at_risk_map.get(vol_id); + let redundant = redundant_map.get(vol_id); + let total = totals_map.get(vol_id); + + let at_risk_bytes = at_risk.map_or(0, |r| r.content_bytes); + let at_risk_file_count = at_risk.map_or(0, |r| r.file_count as u32); + let redundant_bytes = redundant.map_or(0, |r| r.content_bytes); + let redundant_file_count = redundant.map_or(0, |r| r.file_count as u32); + let total_bytes = total.map_or(0, |r| r.total_bytes); + let total_file_count = total.map_or(0, |r| r.total_file_count as u32); + + lib_at_risk_bytes += at_risk_bytes; + lib_redundant_bytes += redundant_bytes; + + volumes.push(VolumeRedundancySummary { + volume_uuid, + display_name: volume_id_to_name.get(vol_id).cloned().flatten(), + total_bytes, + at_risk_bytes, + at_risk_file_count, + redundant_bytes, + redundant_file_count, + total_file_count, + }); + } + + let total_unique_content_bytes = library_total.map_or(0, |r| r.total_bytes); + let replication_score = if lib_at_risk_bytes + lib_redundant_bytes > 0 { + lib_redundant_bytes as f64 / (lib_at_risk_bytes + lib_redundant_bytes) as f64 + } else { + 0.0 + }; + + Ok(RedundancySummaryOutput { + volumes, + library_totals: LibraryRedundancyTotals { + total_unique_content_bytes, + total_at_risk_bytes: lib_at_risk_bytes, + total_redundant_bytes: lib_redundant_bytes, + replication_score, + }, + }) + } +} + +crate::register_library_query!(RedundancySummaryQuery, "redundancy.summary"); diff --git a/core/src/ops/search/filters.rs b/core/src/ops/search/filters.rs index c947252f782b..a0b47ce55e78 100644 --- a/core/src/ops/search/filters.rs +++ b/core/src/ops/search/filters.rs @@ -3,7 +3,8 @@ use super::input::*; use crate::domain::ContentKind; use crate::filetype::FileTypeRegistry; -use sea_orm::{ColumnTrait, Condition}; +use sea_orm::{sea_query::Expr, ColumnTrait, Condition}; +use uuid::Uuid; /// Filter builder for search queries pub struct FilterBuilder { @@ -113,6 +114,101 @@ impl FilterBuilder { self } + /// Filter by redundancy status: at_risk=true means content on exactly 1 volume + pub fn at_risk(mut self, at_risk: &Option) -> Self { + if let Some(is_at_risk) = at_risk { + let having = if *is_at_risk { "= 1" } else { "> 1" }; + self.condition = self.condition.add(Expr::cust(format!( + "entries.content_id IN (\ + SELECT e2.content_id FROM entries e2 \ + WHERE e2.content_id IS NOT NULL AND e2.volume_id IS NOT NULL \ + GROUP BY e2.content_id \ + HAVING COUNT(DISTINCT e2.volume_id) {}\ + )", + having + ))); + } + self + } + + /// Filter to files whose content is present on the specified volumes + pub fn on_volumes(mut self, on_volumes: &Option>) -> Self { + if let Some(uuids) = on_volumes { + if !uuids.is_empty() { + let uuid_list = uuids + .iter() + .map(uuid_to_sqlite_blob_literal) + .collect::>() + .join(","); + self.condition = self.condition.add(Expr::cust(format!( + "entries.content_id IN (\ + SELECT e2.content_id FROM entries e2 \ + INNER JOIN volumes v ON e2.volume_id = v.id \ + WHERE e2.content_id IS NOT NULL \ + AND v.uuid IN ({})\ + )", + uuid_list + ))); + } + } + self + } + + /// Filter to files whose content is NOT present on the specified volumes + pub fn not_on_volumes(mut self, not_on_volumes: &Option>) -> Self { + if let Some(uuids) = not_on_volumes { + if !uuids.is_empty() { + let uuid_list = uuids + .iter() + .map(uuid_to_sqlite_blob_literal) + .collect::>() + .join(","); + self.condition = self.condition.add(Expr::cust(format!( + "entries.content_id NOT IN (\ + SELECT e2.content_id FROM entries e2 \ + INNER JOIN volumes v ON e2.volume_id = v.id \ + WHERE e2.content_id IS NOT NULL \ + AND v.uuid IN ({})\ + )", + uuid_list + ))); + } + } + self + } + + /// Filter by minimum number of volumes content exists on + pub fn min_volume_count(mut self, min_count: &Option) -> Self { + if let Some(min) = min_count { + self.condition = self.condition.add(Expr::cust(format!( + "entries.content_id IN (\ + SELECT e2.content_id FROM entries e2 \ + WHERE e2.content_id IS NOT NULL AND e2.volume_id IS NOT NULL \ + GROUP BY e2.content_id \ + HAVING COUNT(DISTINCT e2.volume_id) >= {}\ + )", + min + ))); + } + self + } + + /// Filter by maximum number of volumes content exists on + pub fn max_volume_count(mut self, max_count: &Option) -> Self { + if let Some(max) = max_count { + self.condition = self.condition.add(Expr::cust(format!( + "entries.content_id IN (\ + SELECT e2.content_id FROM entries e2 \ + WHERE e2.content_id IS NOT NULL AND e2.volume_id IS NOT NULL \ + GROUP BY e2.content_id \ + HAVING COUNT(DISTINCT e2.volume_id) <= {}\ + )", + max + ))); + } + self + } + /// Apply hidden files filter pub fn include_hidden(mut self, include_hidden: &Option) -> Self { if let Some(include) = include_hidden { @@ -129,6 +225,22 @@ impl FilterBuilder { // Removed hardcoded extension mapping - now using FileTypeRegistry +/// Format a UUID as a SQLite BLOB literal (`X'...'`). +/// +/// `volumes.uuid` is stored as a 16-byte BLOB (SeaORM default for `Uuid` +/// on SQLite), so comparing against a quoted UUID string silently returns +/// zero matches. A blob literal compares byte-for-byte. +fn uuid_to_sqlite_blob_literal(uuid: &Uuid) -> String { + let mut out = String::with_capacity(36); + out.push_str("X'"); + for byte in uuid.as_bytes() { + use std::fmt::Write; + let _ = write!(out, "{:02X}", byte); + } + out.push('\''); + out +} + impl Default for FilterBuilder { fn default() -> Self { Self::new() diff --git a/core/src/ops/search/input.rs b/core/src/ops/search/input.rs index 8c2c45e3c6fb..28d2f28dcd5c 100644 --- a/core/src/ops/search/input.rs +++ b/core/src/ops/search/input.rs @@ -62,6 +62,19 @@ pub struct SearchFilters { pub content_types: Option>, pub include_hidden: Option, pub include_archived: Option, + + // Redundancy filters + /// Only return files that are at risk (true) or redundant (false). + /// At risk = content exists on exactly one volume. + pub at_risk: Option, + /// Only return files whose content is present on these volumes + pub on_volumes: Option>, + /// Only return files whose content is NOT present on these volumes + pub not_on_volumes: Option>, + /// Minimum number of distinct volumes the content must exist on + pub min_volume_count: Option, + /// Maximum number of distinct volumes the content can exist on + pub max_volume_count: Option, } /// Filter for tags, supporting complex boolean logic @@ -190,7 +203,14 @@ impl FileSearchInput { let is_recents_query = self.query.trim().is_empty() && matches!(self.sort.field, SortField::IndexedAt); - if self.query.trim().is_empty() && !is_recents_query { + // Allow empty queries when redundancy filters are active (browsing at-risk files) + let has_redundancy_filters = self.filters.at_risk.is_some() + || self.filters.on_volumes.is_some() + || self.filters.not_on_volumes.is_some() + || self.filters.min_volume_count.is_some() + || self.filters.max_volume_count.is_some(); + + if self.query.trim().is_empty() && !is_recents_query && !has_redundancy_filters { return Err("Query cannot be empty".to_string()); } diff --git a/core/src/ops/search/mod.rs b/core/src/ops/search/mod.rs index 043c2b95c664..b48273e64a60 100644 --- a/core/src/ops/search/mod.rs +++ b/core/src/ops/search/mod.rs @@ -41,6 +41,10 @@ pub enum FilterKind { ContentTypes, Tags, // Persistent only Locations, // Persistent only - Hidden, // Not implemented yet - Archived, // Not implemented yet + Hidden, // Not implemented yet + Archived, // Not implemented yet + AtRisk, // Redundancy: content on exactly one volume + OnVolumes, // Redundancy: content present on specific volumes + NotOnVolumes, // Redundancy: content absent from specific volumes + VolumeCount, // Redundancy: min/max volume count } diff --git a/core/src/ops/search/query.rs b/core/src/ops/search/query.rs index f1fb04bb8472..eb21832fb048 100644 --- a/core/src/ops/search/query.rs +++ b/core/src/ops/search/query.rs @@ -924,7 +924,12 @@ impl FileSearchQuery { let filter_builder = FilterBuilder::new() .file_types(&self.input.filters.file_types) .date_range(&self.input.filters.date_range) - .size_range(&self.input.filters.size_range); + .size_range(&self.input.filters.size_range) + .at_risk(&self.input.filters.at_risk) + .on_volumes(&self.input.filters.on_volumes) + .not_on_volumes(&self.input.filters.not_on_volumes) + .min_volume_count(&self.input.filters.min_volume_count) + .max_volume_count(&self.input.filters.max_volume_count); query = query.filter(filter_builder.build()); @@ -951,10 +956,122 @@ impl FileSearchQuery { tracing::info!("Fast search without FTS returned {} entries", entries.len()); - // Convert entries to FileSearchResult using helper + // Batch-hydrate content_identity + content_kind + sidecars so the + // frontend gets real file metadata (mime, thumbs, dedup UUID) instead + // of "Unknown" placeholders. This mirrors the FTS path's hydration. + let content_ids: Vec = entries + .iter() + .filter_map(|e| e.content_id) + .collect::>() + .into_iter() + .collect(); + + let content_identities = if !content_ids.is_empty() { + content_identity::Entity::find() + .filter(content_identity::Column::Id.is_in(content_ids.clone())) + .all(db) + .await? + } else { + Vec::new() + }; + + // Build id -> ContentIdentity (with mapped ContentKind) lookup + let kind_ids: Vec = content_identities + .iter() + .map(|ci| ci.kind_id) + .collect::>() + .into_iter() + .collect(); + + let kinds_by_id: std::collections::HashMap = if !kind_ids.is_empty() { + crate::infra::db::entities::content_kind::Entity::find() + .filter(crate::infra::db::entities::content_kind::Column::Id.is_in(kind_ids)) + .all(db) + .await? + .into_iter() + .map(|k| (k.id, k.name)) + .collect() + } else { + std::collections::HashMap::new() + }; + + // Batch sidecars by content UUID + let content_uuids: Vec = content_identities + .iter() + .filter_map(|ci| ci.uuid) + .collect(); + + let all_sidecars = if !content_uuids.is_empty() { + sidecar::Entity::find() + .filter(sidecar::Column::ContentUuid.is_in(content_uuids.clone())) + .all(db) + .await? + } else { + Vec::new() + }; + + let mut sidecars_by_content: std::collections::HashMap< + Uuid, + Vec, + > = std::collections::HashMap::new(); + for s in all_sidecars { + sidecars_by_content + .entry(s.content_uuid) + .or_insert_with(Vec::new) + .push(crate::domain::file::Sidecar { + id: s.id, + content_uuid: s.content_uuid, + kind: s.kind, + variant: s.variant, + format: s.format, + status: s.status, + size: s.size, + created_at: s.created_at, + updated_at: s.updated_at, + }); + } + + // Index content identities by entry content_id for per-entry lookup + let identities_by_content_id: std::collections::HashMap< + i32, + &content_identity::Model, + > = content_identities.iter().map(|ci| (ci.id, ci)).collect(); + + // Convert entries to FileSearchResult, hydrating each with content_identity let mut results = Vec::new(); for entry_model in entries { - if let Some(result) = self.entry_to_search_result(entry_model, db, 1.0).await? { + let content_id = entry_model.content_id; + if let Some(mut result) = + self.entry_to_search_result(entry_model, db, 1.0).await? + { + if let Some(cid) = content_id { + if let Some(ci) = identities_by_content_id.get(&cid) { + let kind = kinds_by_id + .get(&ci.kind_id) + .map(|name| crate::domain::ContentKind::from(name.as_str())) + .unwrap_or(crate::domain::ContentKind::Unknown); + + if let Some(ci_uuid) = ci.uuid { + result.file.content_identity = + Some(crate::domain::content_identity::ContentIdentity { + uuid: ci_uuid, + kind, + content_hash: ci.content_hash.clone(), + integrity_hash: ci.integrity_hash.clone(), + mime_type_id: ci.mime_type_id, + text_content: ci.text_content.clone(), + total_size: ci.total_size, + entry_count: ci.entry_count, + first_seen_at: ci.first_seen_at, + last_verified_at: ci.last_verified_at, + }); + if let Some(sidecars) = sidecars_by_content.get(&ci_uuid) { + result.file.sidecars = sidecars.clone(); + } + } + result.file.content_kind = kind; + } + } results.push(result); } } diff --git a/core/src/ops/volumes/list/query.rs b/core/src/ops/volumes/list/query.rs index dd5a1094ef5e..f062ca4efc9a 100644 --- a/core/src/ops/volumes/list/query.rs +++ b/core/src/ops/volumes/list/query.rs @@ -226,7 +226,9 @@ impl LibraryQuery for VolumeListQuery { // Re-apply current platform visibility rules so stale DB // entries from earlier versions (which tracked everything) // inherit newly-added filters without a data migration. - if should_hide_by_mount_path(&offline_vol.mount_point) { + if crate::volume::utils::should_hide_by_mount_path( + &offline_vol.mount_point, + ) { offline_vol.is_user_visible = false; offline_vol.auto_track_eligible = false; } @@ -264,18 +266,4 @@ impl LibraryQuery for VolumeListQuery { } } -/// Re-evaluate whether a mount path should be hidden from the user based on -/// current platform visibility rules. Used to retroactively hide tracked -/// volumes whose DB entries were created before the filter rules existed. -#[cfg(target_os = "linux")] -fn should_hide_by_mount_path(mount_point: &std::path::Path) -> bool { - crate::volume::utils::is_system_mount_point(mount_point) - || crate::volume::utils::is_nested_app_mount(mount_point) -} - -#[cfg(not(target_os = "linux"))] -fn should_hide_by_mount_path(_mount_point: &std::path::Path) -> bool { - false -} - crate::register_library_query!(VolumeListQuery, "volumes.list"); diff --git a/core/src/service/network/core/mod.rs b/core/src/service/network/core/mod.rs index 5edae2bc27bc..959d9428ac29 100644 --- a/core/src/service/network/core/mod.rs +++ b/core/src/service/network/core/mod.rs @@ -213,40 +213,78 @@ impl NetworkingService { // - mDNS for local network discovery // - PkarrPublisher to publish our address to dns.iroh.link (enables remote discovery) // - DnsDiscovery to resolve other nodes from dns.iroh.link - let endpoint = Endpoint::builder() - .secret_key(secret_key) - .alpns(vec![ - PAIRING_ALPN.to_vec(), - FILE_TRANSFER_ALPN.to_vec(), - MESSAGING_ALPN.to_vec(), - SYNC_ALPN.to_vec(), - JOB_ACTIVITY_ALPN.to_vec(), - ]) - .relay_mode(iroh::RelayMode::Default) - .discovery(MdnsDiscovery::builder()) - .discovery(PkarrPublisher::n0_dns()) - .discovery(DnsDiscovery::n0_dns()) - .bind_addr_v4(std::net::SocketAddrV4::new( - std::net::Ipv4Addr::UNSPECIFIED, - 0, - )) - .bind_addr_v6(std::net::SocketAddrV6::new( - std::net::Ipv6Addr::UNSPECIFIED, - 0, - 0, - 0, - )) - .bind() - .await - .map_err(|e| NetworkingError::Transport(format!("Failed to create endpoint: {}", e)))?; + // + // mDNS is best-effort: on hosts where another service (e.g. avahi-daemon + // on most Linux boxes / TrueNAS) already owns UDP :5353, Iroh's own mDNS + // service can't bind and endpoint creation fails wholesale. Fall back to + // pkarr + DNS-only discovery in that case — remote pairing via node ID + // continues to work, we just lose local-network auto-discovery. + let build_endpoint = |with_mdns: bool| { + let mut builder = Endpoint::builder() + .secret_key(secret_key.clone()) + .alpns(vec![ + PAIRING_ALPN.to_vec(), + FILE_TRANSFER_ALPN.to_vec(), + MESSAGING_ALPN.to_vec(), + SYNC_ALPN.to_vec(), + JOB_ACTIVITY_ALPN.to_vec(), + ]) + .relay_mode(iroh::RelayMode::Default) + .discovery(PkarrPublisher::n0_dns()) + .discovery(DnsDiscovery::n0_dns()) + .bind_addr_v4(std::net::SocketAddrV4::new( + std::net::Ipv4Addr::UNSPECIFIED, + 0, + )) + .bind_addr_v6(std::net::SocketAddrV6::new( + std::net::Ipv6Addr::UNSPECIFIED, + 0, + 0, + 0, + )); + if with_mdns { + builder = builder.discovery(MdnsDiscovery::builder()); + } + builder.bind() + }; + + let endpoint = match build_endpoint(true).await { + Ok(ep) => { + self.logger + .info("Endpoint bound successfully with mDNS + pkarr discovery enabled") + .await; + ep + } + Err(e) => { + let err_str = e.to_string().to_lowercase(); + if err_str.contains("mdns") { + self.logger + .warn(&format!( + "mDNS discovery unavailable ({}); retrying with pkarr + DNS only. \ + Local-network auto-discovery is disabled on this host, but remote \ + pairing via node ID will still work.", + e + )) + .await; + let ep = build_endpoint(false).await.map_err(|e| { + NetworkingError::Transport(format!("Failed to create endpoint: {}", e)) + })?; + self.logger + .info("Endpoint bound successfully without mDNS (pkarr + DNS only)") + .await; + ep + } else { + return Err(NetworkingError::Transport(format!( + "Failed to create endpoint: {}", + e + ))); + } + } + }; // Store endpoint reference for other methods self.endpoint = Some(endpoint.clone()); - self.logger - .info("Endpoint bound successfully with mDNS + pkarr discovery enabled") - .await; - // Create and start event loop let event_loop = NetworkingEventLoop::new( endpoint, @@ -1455,32 +1493,43 @@ impl NetworkingService { } } } else { - // Normal mode with node_id: race mDNS and relay - tokio::select! { - result = self.try_mdns_discovery(session_id, force_relay) => { - match result { - Ok(()) => { - self.logger.info("Connected via mDNS (local network)").await; - Ok(()) - } - Err(e) => { - self.logger.warn(&format!("mDNS discovery failed: {}", e)).await; - Err(e) - } + // Normal mode with node_id: race mDNS and relay, but only fail if + // BOTH fail. `tokio::select!` resolves on the first completed branch + // including errors, which means a host that can't bind mDNS (e.g. + // where avahi owns :5353) would have its instant mDNS failure abort + // pairing before relay discovery gets a chance. `select_ok` picks + // the first Ok and only returns an error when every branch errors. + use futures::future::{select_ok, FutureExt}; + + let logger_mdns = self.logger.clone(); + let logger_relay = self.logger.clone(); + let mdns_fut = self + .try_mdns_discovery(session_id, force_relay) + .inspect(move |r| { + let logger = logger_mdns.clone(); + if let Err(e) = r { + let msg = format!("mDNS discovery failed: {}", e); + tokio::spawn(async move { logger.warn(&msg).await }); } - } - result = self.try_relay_discovery(&pairing_code_clone) => { - match result { - Ok(()) => { - self.logger.info("Connected via relay (remote network)").await; - Ok(()) - } - Err(e) => { - self.logger.warn(&format!("Relay discovery failed: {}", e)).await; - Err(e) - } + }) + .boxed(); + let relay_fut = self + .try_relay_discovery(&pairing_code_clone) + .inspect(move |r| { + let logger = logger_relay.clone(); + if let Err(e) = r { + let msg = format!("Relay discovery failed: {}", e); + tokio::spawn(async move { logger.warn(&msg).await }); } + }) + .boxed(); + + match select_ok([mdns_fut, relay_fut]).await { + Ok(((), _)) => { + self.logger.info("Connected (dual-path discovery)").await; + Ok(()) } + Err(e) => Err(e), } }; diff --git a/core/src/volume/fs/zfs.rs b/core/src/volume/fs/zfs.rs index 0c71f3eff157..5e87b9bdb3f0 100644 --- a/core/src/volume/fs/zfs.rs +++ b/core/src/volume/fs/zfs.rs @@ -412,11 +412,42 @@ pub async fn fetch_zfs_list_output() -> VolumeResult { /// are marked as system-level and hidden. /// - Datasets under known app/container parent paths (e.g. `ix-applications`) /// are hidden. +/// +/// Capacity fix: for pool-root datasets, `df` under-reports Size because it +/// only counts the root dataset's *own* used bytes plus avail. Descendant +/// datasets hold the real data but `df` can't see them from the root. ZFS's +/// `zfs list` exposes the root's `used` property which *does* include +/// descendants, so we override `total_capacity` with `used + available` to +/// reflect the pool's true usable capacity (e.g. 62 TB instead of 15 TB on +/// a 60 TB raidz2 pool that's mostly full of data in child datasets). pub fn enhance_volume_with_cached_output(volume: &mut Volume, zfs_list_output: &str) { if let Some(mount_point) = volume.mount_point.to_str() { if let Ok(dataset_info) = find_dataset_for_path(zfs_list_output, Path::new(mount_point)) { debug!("Enhanced ZFS volume with dataset info: {:?}", dataset_info); + // If this volume IS the pool root (dataset name equals pool name), + // overwrite the df-derived capacity with the pool-wide total from + // `zfs list`. Only the pool root carries descendant-inclusive + // `used`, so this correction only applies there — leaf datasets + // would over-report if we did it for them (each would claim the + // whole pool's capacity). + if dataset_info.name == dataset_info.pool_name { + let pool_total = dataset_info + .used_bytes + .saturating_add(dataset_info.available_bytes); + debug!( + "ZFS pool root '{}' at {}: overriding total_capacity {} → {} (used={}, avail={})", + dataset_info.pool_name, + mount_point, + volume.total_capacity, + pool_total, + dataset_info.used_bytes, + dataset_info.available_bytes, + ); + volume.total_capacity = pool_total; + volume.available_space = dataset_info.available_bytes; + } + if is_system_zfs_pool(&dataset_info.pool_name) { debug!( "VISIBILITY: Hiding ZFS volume on system pool '{}': {}", diff --git a/core/src/volume/utils.rs b/core/src/volume/utils.rs index 6474a9ed54e8..c047c7cc14df 100644 --- a/core/src/volume/utils.rs +++ b/core/src/volume/utils.rs @@ -166,6 +166,21 @@ pub fn is_nested_app_mount(mount_point: &Path) -> bool { || path_str.contains("/.zfs/snapshot/") } +/// Re-evaluate whether a mount path should be hidden from the user based on +/// current platform visibility rules. Shared between the volume list query +/// and the library statistics calculation so both consistently hide system +/// mounts and nested app/container volumes even when the tracked DB row +/// predates these filters (and so has a stale `is_user_visible = true`). +#[cfg(target_os = "linux")] +pub fn should_hide_by_mount_path(mount_point: &Path) -> bool { + is_system_mount_point(mount_point) || is_nested_app_mount(mount_point) +} + +#[cfg(not(target_os = "linux"))] +pub fn should_hide_by_mount_path(_mount_point: &Path) -> bool { + false +} + /// Parse filesystem type from string to FileSystem enum pub fn parse_filesystem_type(fs_type: &str) -> FileSystem { match fs_type.to_lowercase().as_str() { @@ -345,4 +360,42 @@ mod tests { FileSystem::Other(_) )); } + + #[cfg(target_os = "linux")] + #[test] + fn test_should_hide_by_mount_path_linux() { + // System mounts — always hidden + assert!(should_hide_by_mount_path(Path::new("/"))); + assert!(should_hide_by_mount_path(Path::new("/usr"))); + assert!(should_hide_by_mount_path(Path::new("/var"))); + assert!(should_hide_by_mount_path(Path::new("/etc"))); + assert!(should_hide_by_mount_path(Path::new("/home"))); + assert!(should_hide_by_mount_path(Path::new("/boot/grub"))); + assert!(should_hide_by_mount_path(Path::new("/var/log/journal"))); + assert!(should_hide_by_mount_path(Path::new( + "/var/db/system/netdata" + ))); + assert!(should_hide_by_mount_path(Path::new("/sys/firmware/efi"))); + + // TrueNAS Scale app-managed datasets + assert!(should_hide_by_mount_path(Path::new("/mnt/.ix-apps"))); + assert!(should_hide_by_mount_path(Path::new( + "/mnt/.ix-apps/docker" + ))); + assert!(should_hide_by_mount_path(Path::new( + "/mnt/pool/ix-applications" + ))); + assert!(should_hide_by_mount_path(Path::new( + "/mnt/pool/ix-applications/releases/plex/volumes/ix_volumes/ix-plex_data" + ))); + + // User data — not hidden + assert!(!should_hide_by_mount_path(Path::new("/mnt/pool"))); + assert!(!should_hide_by_mount_path(Path::new( + "/mnt/pool/footage" + ))); + assert!(!should_hide_by_mount_path(Path::new( + "/mnt/pool/calvin-nas" + ))); + } } diff --git a/docs/.gitignore b/docs/.gitignore index 7a5b99dff609..a6ce2a420f13 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,9 @@ -# No longer needed since we're copying files directly +node_modules +.next +.source +out +dist +build +next-env.d.ts +.env*.local +.DS_Store diff --git a/docs/CODE_COMMENTS.mdx b/docs/CODE_COMMENTS.md similarity index 100% rename from docs/CODE_COMMENTS.mdx rename to docs/CODE_COMMENTS.md diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 8663fbeab48a..000000000000 --- a/docs/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Spacedrive Documentation - -This repository contains the Mintlify-powered documentation for Spacedrive. - -## Structure - -- `docs/` - Symlink to main spacedrive repo documentation -- `mint.json` - Mintlify configuration diff --git a/docs/WRITING_GUIDE.mdx b/docs/WRITING_GUIDE.md similarity index 100% rename from docs/WRITING_GUIDE.mdx rename to docs/WRITING_GUIDE.md diff --git a/docs/app/[[...slug]]/layout.tsx b/docs/app/[[...slug]]/layout.tsx new file mode 100644 index 000000000000..c1a2e3e58248 --- /dev/null +++ b/docs/app/[[...slug]]/layout.tsx @@ -0,0 +1,11 @@ +import { DocsLayout } from 'fumadocs-ui/layouts/docs'; +import { baseOptions } from '@/lib/layout.shared'; +import { source } from '@/lib/source'; + +export default function Layout({ children }: LayoutProps<'/[[...slug]]'>) { + return ( + + {children} + + ); +} diff --git a/docs/app/[[...slug]]/page.tsx b/docs/app/[[...slug]]/page.tsx new file mode 100644 index 000000000000..eb94c6d1edd9 --- /dev/null +++ b/docs/app/[[...slug]]/page.tsx @@ -0,0 +1,78 @@ +import { createRelativeLink } from 'fumadocs-ui/mdx'; +import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { + MarkdownCopyButton, + ViewOptionsPopover, +} from '@/components/ai/page-actions'; +import { gitConfig } from '@/lib/layout.shared'; +import { getPageImage, source } from '@/lib/source'; +import { getMDXComponents } from '@/mdx-components'; + +export default async function Page(props: PageProps<'/[[...slug]]'>) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + const MDX = page.data.body; + const markdownUrl = `${page.url}.mdx`; + const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/${gitConfig.docsPath}/${page.path}`; + + return ( + + {page.data.title} + + {page.data.description} + +
+ + +
+
+ + + + + ); +} + +export async function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata( + props: PageProps<'/[[...slug]]'>, +): Promise { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + openGraph: { + images: getPageImage(page).url, + }, + }; +} diff --git a/docs/app/api/search/route.ts b/docs/app/api/search/route.ts new file mode 100644 index 000000000000..79628b18416e --- /dev/null +++ b/docs/app/api/search/route.ts @@ -0,0 +1,6 @@ +import { createFromSource } from 'fumadocs-core/search/server'; +import { source } from '@/lib/source'; + +export const { GET } = createFromSource(source, { + language: 'english', +}); diff --git a/docs/app/global.css b/docs/app/global.css new file mode 100644 index 000000000000..e365b8182aef --- /dev/null +++ b/docs/app/global.css @@ -0,0 +1,41 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; + +@source '../node_modules/fumadocs-ui/dist/**/*.js'; + +:root { + --color-fd-primary: hsl(209, 100%, 61%); + --color-fd-primary-foreground: hsl(0, 0%, 100%); + --color-fd-ring: hsl(209, 100%, 61%); +} + +.dark { + --color-fd-primary: hsl(209, 100%, 65%); + --color-fd-primary-foreground: hsl(222, 47%, 11%); + --color-fd-ring: hsl(209, 100%, 65%); +} + +::selection { + background-color: hsla(209, 100%, 61%, 0.3); +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--color-fd-background); +} +::-webkit-scrollbar-thumb { + background: var(--color-fd-border); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--color-fd-ring); +} + +#nd-sidebar a, +#nd-sidebar button { + transition: none !important; +} diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx new file mode 100644 index 000000000000..73c1f04107c8 --- /dev/null +++ b/docs/app/layout.tsx @@ -0,0 +1,28 @@ +import { RootProvider } from 'fumadocs-ui/provider/next'; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './global.css'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: { + default: 'Spacedrive Documentation', + template: '%s — Spacedrive', + }, + description: + 'Infrastructure for multi-device computing. The file explorer from the future.', + metadataBase: new URL( + process.env.NEXT_PUBLIC_SITE_URL ?? 'https://docs.spacedrive.com', + ), +}; + +export default function Layout({ children }: LayoutProps<'/'>) { + return ( + + + {children} + + + ); +} diff --git a/docs/app/llms-full.txt/route.ts b/docs/app/llms-full.txt/route.ts new file mode 100644 index 000000000000..b6abca210147 --- /dev/null +++ b/docs/app/llms-full.txt/route.ts @@ -0,0 +1,10 @@ +import { getLLMText, source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + const scan = source.getPages().map(getLLMText); + const scanned = await Promise.all(scan); + + return new Response(scanned.join('\n\n')); +} diff --git a/docs/app/llms.mdx/docs/[[...slug]]/route.ts b/docs/app/llms.mdx/docs/[[...slug]]/route.ts new file mode 100644 index 000000000000..7a60c8d326e8 --- /dev/null +++ b/docs/app/llms.mdx/docs/[[...slug]]/route.ts @@ -0,0 +1,23 @@ +import { notFound } from 'next/navigation'; +import { getLLMText, source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET( + _req: Request, + { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>, +) { + const { slug } = await params; + const page = source.getPage(slug); + if (!page) notFound(); + + return new Response(await getLLMText(page), { + headers: { + 'Content-Type': 'text/markdown', + }, + }); +} + +export function generateStaticParams() { + return source.generateParams(); +} diff --git a/docs/app/llms.txt/route.ts b/docs/app/llms.txt/route.ts new file mode 100644 index 000000000000..738d57008973 --- /dev/null +++ b/docs/app/llms.txt/route.ts @@ -0,0 +1,14 @@ +import { source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + const lines: string[] = ['# Spacedrive Documentation', '']; + for (const page of source.getPages()) { + const description = page.data.description + ? `: ${page.data.description}` + : ''; + lines.push(`- [${page.data.title}](${page.url})${description}`); + } + return new Response(lines.join('\n')); +} diff --git a/docs/app/og/docs/[...slug]/route.tsx b/docs/app/og/docs/[...slug]/route.tsx new file mode 100644 index 000000000000..3411a3bb728a --- /dev/null +++ b/docs/app/og/docs/[...slug]/route.tsx @@ -0,0 +1,34 @@ +import { generate as DefaultImage } from 'fumadocs-ui/og'; +import { notFound } from 'next/navigation'; +import { ImageResponse } from 'next/og'; +import { getPageImage, source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET( + _req: Request, + { params }: RouteContext<'/og/docs/[...slug]'>, +) { + const { slug } = await params; + const page = source.getPage(slug.slice(0, -1)); + if (!page) notFound(); + + return new ImageResponse( + , + { + width: 1200, + height: 630, + }, + ); +} + +export function generateStaticParams() { + return source.getPages().map((page) => ({ + lang: page.locale, + slug: getPageImage(page).segments, + })); +} diff --git a/docs/biome.json b/docs/biome.json new file mode 100644 index 000000000000..e527c3a29b83 --- /dev/null +++ b/docs/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!node_modules", + "!.next", + "!dist", + "!build", + "!.source" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + }, + "domains": { + "next": "recommended", + "react": "recommended" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/docs/bun.lock b/docs/bun.lock new file mode 100644 index 000000000000..ce4fb468c824 --- /dev/null +++ b/docs/bun.lock @@ -0,0 +1,783 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@sd/docs", + "dependencies": { + "@radix-ui/react-popover": "^1.1.15", + "class-variance-authority": "^0.7.1", + "fumadocs-core": "^16.6.1", + "fumadocs-mdx": "^14.2.7", + "fumadocs-ui": "^16.6.1", + "lucide-react": "^0.563.0", + "next": "^16.1.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "tailwind-merge": "^3.4.0", + }, + "devDependencies": { + "@biomejs/biome": "^2.3.15", + "@tailwindcss/postcss": "^4.1.18", + "@types/mdx": "^2.0.13", + "@types/node": "^25.2.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@fumadocs/tailwind": ["@fumadocs/tailwind@0.0.5", "", { "peerDependencies": { "@tailwindcss/oxide": "^4.0.0", "tailwindcss": "^4.0.0" }, "optionalPeers": ["@tailwindcss/oxide", "tailwindcss"] }, "sha512-ENKPWUDRmriccsrUDE4bDBq3FNr/ms3BP2rWlsAEMV1yP23pcCaan+ceGfeBUsAQjw7sj9Q3R4Kl3g/TCStPzQ=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="], + + "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], + + "@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], + + "@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], + + "@shikijs/transformers": ["@shikijs/transformers@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/types": "4.0.2" } }, "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg=="], + + "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], + + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-value-to-estree": ["estree-util-value-to-estree@3.5.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], + + "fumadocs-core": ["fumadocs-core@16.7.16", "", { "dependencies": { "@orama/orama": "^3.1.18", "@shikijs/transformers": "^4.0.2", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "remark": "^15.0.1", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@mdx-js/mdx": "*", "@mixedbread/sdk": "^0.46.0", "@orama/core": "1.x.x", "@oramacloud/client": "2.x.x", "@tanstack/react-router": "1.x.x", "@types/estree-jsx": "*", "@types/hast": "*", "@types/mdast": "*", "@types/react": "*", "algoliasearch": "5.x.x", "flexsearch": "*", "lucide-react": "*", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router": "7.x.x", "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", "zod": "4.x.x" }, "optionalPeers": ["@mdx-js/mdx", "@mixedbread/sdk", "@orama/core", "@oramacloud/client", "@tanstack/react-router", "@types/estree-jsx", "@types/hast", "@types/mdast", "@types/react", "algoliasearch", "flexsearch", "lucide-react", "next", "react", "react-dom", "react-router", "waku", "zod"] }, "sha512-OXZMJPS/HVAfCw8/VEgsOEQNWUXq7+U2v1A/DbKmI0Vbb08uPCUpbIqVSj4kmGr46L+Pa/PZgG596Z7iPhlbCg=="], + + "fumadocs-mdx": ["fumadocs-mdx@14.3.0", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.0", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.4", "tinyexec": "^1.1.1", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-OsllpIpdk6Mu595MpX1hFFXrBq7cFpFBEkKNAFgO7aKZ/ux4e4pavTesDd5xKhuOfC0J9CZSUJ8RMlad9j5yTA=="], + + "fumadocs-ui": ["fumadocs-ui@16.7.16", "", { "dependencies": { "@fumadocs/tailwind": "0.0.5", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "lucide-react": "^1.8.0", "motion": "^12.38.0", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.0.2", "tailwind-merge": "^3.5.0", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@takumi-rs/image-response": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.7.16", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0" }, "optionalPeers": ["@takumi-rs/image-response", "@types/mdx", "@types/react", "next"] }, "sha512-NFWH8GuVV0O6OQnGb0TCS6jsdgyB7/5phQm8YjtIkyjkxAkERwA76N5RYCpS27p41NdtWdQw4eFIEve1Aq9Siw=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="], + + "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + + "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="], + + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + + "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + + "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], + + "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "fumadocs-ui/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + } +} diff --git a/docs/cli/CLI_REDESIGN.md b/docs/cli/CLI_REDESIGN.md deleted file mode 100644 index cf6034208b29..000000000000 --- a/docs/cli/CLI_REDESIGN.md +++ /dev/null @@ -1,389 +0,0 @@ -# Spacedrive CLI Redesign Plan - -## Overview - -Redesign the CLI structure to be more intuitive, consistent, and user-friendly while maintaining power-user capabilities. All top-level commands (except start/stop) will support interactive wizards when called without arguments, making the CLI approachable for new users while keeping direct command paths for scripting. - -## Core Design Principles - -1. **Interactive by Default**: Commands without args enter interactive mode -2. **Hybrid SdPath Support**: Accept both traditional paths and SdPath URIs (`local://`, `s3://`, `content://`) -3. **Consistent Patterns**: Every resource type has predictable subcommands (list, create, remove, etc.) -4. **Smart Context Awareness**: Commands adapt based on context (e.g., browse uses index when available) -5. **Scriptable**: All interactive flows have direct command equivalents with `--format json` support - -## Command Structure - -### Daemon Lifecycle (No Wizards) - -```bash -sd start [--foreground] # Start daemon -sd stop [--reset] # Stop daemon (optional data reset) -``` - -### Configuration - -```bash -sd config # Interactive: show current config, prompt to edit -sd config get # Get specific config value -sd config set # Set config value -``` - -### Library Management - -```bash -sd library # Show current library status (name, locations, stats, devices) -sd library create # Interactive: name, path, settings -sd library switch # Interactive: select from list -sd library list # List all libraries -sd library delete # Interactive: select + confirm -``` - -### Location Management (Managed Directories) - -```bash -sd location # Interactive: list → add/remove/rescan -sd location add # Interactive wizard (already implemented) -sd location remove # Interactive: select from list -sd location rescan [id] # Interactive: select location if no ID -sd location list # List all locations -``` - -### Universal Browsing (Location-Aware) - -```bash -sd browse [path|uri] # Smart browsing with interactive TUI - # - Uses location index if path is managed - # - Falls back to ephemeral index if outside locations - # - No path = interactive root picker -``` - -**Behavior:** - -- Inside managed location: instant (uses existing index) -- Outside locations: ephemeral index (temporary, not persisted) -- Supports SdPath URIs for remote browsing - -### File Operations (Hybrid SdPath) - -```bash -sd ls [path|uri] # List files (simple output) -sd cp # Copy (supports URIs + --device/--cloud flags) -sd mv # Move -sd rm # Delete (with confirmation) -sd info # Show file metadata -``` - -**SdPath Examples:** - -```bash -# Traditional paths -sd cp /Users/me/file.txt /backup/ - -# SdPath URIs -sd cp local://macbook/Users/me/file.txt s3://my-bucket/backup/ -sd info content://550e8400-e29b-41d4-a716-446655440000 -``` - -### Global Search - -```bash -sd search # Interactive: query builder with filters -sd search # Direct search -sd search --tag # Filter by tag -sd search --type # Filter by file type -sd search --content # Content search -sd search --size # Size filter -sd search --date # Date filter -``` - -### Organization - Tags - -```bash -sd tag # Interactive: select file → add tags -sd tag create # Interactive: name, color, namespace -sd tag apply # Direct apply tags -sd tag remove # Remove tags -sd tag list # List all tags -sd tag search # Search tag names (different from sd search) -``` - -### Organization - Collections - -```bash -sd collection # Interactive: list → create/add/remove -sd collection create # Interactive: name, description -sd collection add # Interactive: select files to add -sd collection remove # Interactive: select files to remove -sd collection list # List all collections -``` - -### Network - Pairing - -```bash -sd pair # Interactive: initiate or join -sd pair initiate # Generate pairing code -sd pair join [code] # Interactive: enter code if not provided -``` - -### Network - Devices - -Note: These are paired devices, not devices registered in a library, for clarity we should show which libraries these devices are participating in by quering the devices table for all libraries! - -```bash -sd devices # Interactive: list → revoke/manage -sd devices list # List paired devices -sd devices remove # Remove/revoke device -``` - -### Network - File Sharing - -This doesn't exist yet so we can implement as a stub - -```bash -sd share # Interactive: select device → select file -sd share # Direct share via Spacedrop -``` - -### Cloud Storage - -```bash -sd cloud # Interactive wizard (already implemented) -sd cloud add # Interactive: service type → credentials -sd cloud remove # Interactive: select volume -sd cloud list # List cloud volumes -``` - -### Volumes - -```bash -sd volume # Interactive: list → manage -sd volume list # List all volumes (local + cloud) -``` - -### Sync Conduits (WIP Feature) - -```bash -sd sync # Interactive: conduit management -sd sync status # Show sync state -sd sync create # Interactive: create sync conduit -``` - -### Jobs & Monitoring - -```bash -sd job # Interactive: list → monitor/pause/cancel -sd job list # List all jobs -sd job monitor [id] # Monitor jobs with TUI (all or specific) -sd job pause # Pause job -sd job resume # Resume job -sd job cancel # Cancel job -``` - -### Logs - -```bash -sd logs # Interactive: show or follow -sd logs show [--tail N] # Show recent logs -sd logs follow # Follow logs in real-time -``` - -## Removed/Merged Commands - -### Removed - -- `sd index` → Functionality absorbed into `sd location` and `sd browse` -- `sd status` → Replaced by `sd library` (shows current state) -- `sd network` → Split into `sd pair`, `sd devices`, `sd share` -- `sd restart` → Can be achieved with `sd stop && sd start` -- `sd update` → Can be system-level or `sd daemon update` if needed - -### Merged/Reorganized - -- `sd location browse` → `sd browse` (root level, location-aware) -- `sd index quick-scan` → `sd browse` (ephemeral mode automatic) -- `sd index start` → `sd location add` (with mode flags) -- `sd index verify` → `sd location rescan --verify` -- `sd network pair` → `sd pair` -- `sd network devices` → `sd devices` -- `sd network spacedrop` → `sd share` - -## Implementation Plan - -### Phase 1: Command Restructure - -**Goal**: Reorganize command structure and file layout - -**Tasks:** - -1. Create new domain modules: - - `apps/cli/src/domains/browse/` (new) - - `apps/cli/src/domains/pair/` (split from network) - - `apps/cli/src/domains/share/` (split from network) - - `apps/cli/src/domains/collection/` (new) - -2. Remove obsolete modules: - - `apps/cli/src/domains/index/` (merge into location + browse) - -3. Update `apps/cli/src/main.rs`: - - Restructure `Commands` enum to match new hierarchy - - Remove merged commands - - Update command routing - -4. Update existing domain modules to match new patterns - -### Phase 2: Smart Browse Implementation - -**Goal**: Create location-aware browsing command - -**Tasks:** - -1. Implement browse command with dual-mode indexing: - - Check if path is within managed location - - Use location index if available (fast) - - Fall back to ephemeral index if outside (slower) - -2. Add interactive TUI for navigation: - - Tree view or grid view - - Keyboard navigation - - Preview panel - - Reference existing location wizard UX - -3. Support SdPath URIs for remote browsing: - - `sd browse local://device/path` - - `sd browse s3://bucket/prefix` - -### Phase 3: Enhanced Search - -**Goal**: Restore and improve global search - -**Tasks:** - -1. Restore `domains/search/` with enhanced functionality -2. Implement filter flags: - - `--tag`, `--type`, `--content`, `--size`, `--date` -3. Create interactive query builder -4. Multiple output formats: table, json, paths-only - -### Phase 4: Interactive Wizards - -**Goal**: Add wizards to all commands that should have them - -**Commands requiring wizards:** - -- `sd config` - show/edit flow -- `sd browse` - TUI navigator -- `sd search` - query builder -- `sd tag` - tagging workflow -- `sd collection` - collection management -- `sd pair` - pairing flow -- `sd devices` - device management -- `sd share` - file sharing picker -- `sd volume` - volume management -- `sd job` - job list → actions -- `sd logs` - show/follow picker - -**Implementation approach:** - -- Use `dialoguer` crate for prompts -- Show contextual info before prompts -- Maintain direct command paths for scripting -- Pattern: detect when called with no subcommand/args - -### Phase 5: Hybrid SdPath Support - -**Goal**: Support both traditional paths and SdPath URIs - -**Tasks:** - -1. Create URI parser that accepts both formats -2. Update file operations (ls, cp, mv, rm, info, browse): - - Parse traditional paths: `/Users/me/file.txt` - - Parse SdPath URIs: `local://device/path`, `s3://bucket/key`, `content://uuid` - - Support shortcut flags: `--device`, `--cloud` - -3. Add resolution logic: - - Convert traditional paths to SdPath internally - - Resolve URIs to actual storage locations - - Handle cross-device operations - -4. Error handling: - - Clear messages for malformed URIs - - Suggestions for common mistakes - -### Phase 6: Documentation & Polish - -**Goal**: Comprehensive documentation and UX refinement - -**Tasks:** - -1. Update help text for all commands -2. Add examples in `--help` output -3. Update documentation files in `docs/cli/` -4. Create migration guide from old commands to new -5. Add shell completions (bash, zsh, fish) -6. Test all interactive flows -7. Ensure `--format json` works consistently - -## Success Criteria - -- All commands follow consistent patterns -- Interactive mode works for all designated commands -- Direct command paths work for scripting -- SdPath URIs work across file operations -- Browse intelligently uses location index when available -- Search provides powerful filtering -- Documentation is comprehensive -- No regression in existing functionality -- Shell completions work - -## Migration Notes - -**For users upgrading from current CLI:** - -Breaking changes: - -- `sd index` removed → use `sd location add` or `sd browse` -- `sd network pair` → `sd pair` -- `sd network spacedrop` → `sd share` -- `sd status` → `sd library` - -Non-breaking: - -- All location commands remain the same -- Library commands remain the same -- Job commands remain the same -- Logs commands remain the same - -## Design Rationale - -### Why `browse` is separate from `ls` - -- `browse` is an interactive navigator with TUI -- `ls` is a simple list command (like traditional Unix ls) -- Different use cases: exploration vs scripting - -### Why `search` is global while `tag search` exists - -- `sd search` searches file content, names, metadata across entire library -- `sd tag search` searches for tag names themselves -- Different domains: files vs tags - -### Why split `network` into `pair`, `devices`, `share` - -- Each has distinct mental models and workflows -- Pairing is an infrequent setup task -- Devices is for ongoing management -- Share is a frequent operation that should be quick - -### Why remove `sd status` - -- `sd library` provides library-level status (the most common query) -- System-level status can be `sd daemon status` if needed -- Reduces command clutter - -### Why keep `sd config` separate - -- Global configuration spans libraries -- Different scope than library-specific settings -- Common pattern in other CLIs (git config, npm config) diff --git a/docs/components/ai/page-actions.tsx b/docs/components/ai/page-actions.tsx new file mode 100644 index 000000000000..0f09bd612d53 --- /dev/null +++ b/docs/components/ai/page-actions.tsx @@ -0,0 +1,179 @@ +'use client'; +import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'; +import { Check, ChevronDown, Copy, ExternalLinkIcon, TextIcon } from 'lucide-react'; +import { type ComponentProps, useMemo, useState } from 'react'; +import { cn } from '@/lib/cn'; +import { buttonVariants } from '@/components/ui/button'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; + +const cache = new Map>(); + +export function MarkdownCopyButton({ + markdownUrl, + ...props +}: ComponentProps<'button'> & { + markdownUrl: string; +}) { + const [isLoading, setLoading] = useState(false); + const [checked, onClick] = useCopyButton(async () => { + const cached = cache.get(markdownUrl); + if (cached) return navigator.clipboard.writeText(await cached); + + setLoading(true); + + try { + const promise = fetch(markdownUrl).then((res) => res.text()); + cache.set(markdownUrl, promise); + await navigator.clipboard.write([ + new ClipboardItem({ + 'text/plain': promise, + }), + ]); + } finally { + setLoading(false); + } + }); + + return ( + + ); +} + +export function ViewOptionsPopover({ + markdownUrl, + githubUrl, + ...props +}: ComponentProps & { + markdownUrl: string; + githubUrl: string; +}) { + const items = useMemo(() => { + const pageUrl = + typeof window !== 'undefined' ? window.location.href : 'loading'; + const q = `Read ${pageUrl}, I want to ask questions about it.`; + + return [ + { + title: 'Open in GitHub', + href: githubUrl, + icon: ( + + GitHub + + + ), + }, + { + title: 'View as Markdown', + href: markdownUrl, + icon: , + }, + { + title: 'Open in ChatGPT', + href: `https://chatgpt.com/?${new URLSearchParams({ + hints: 'search', + q, + })}`, + icon: ( + + OpenAI + + + ), + }, + { + title: 'Open in Claude', + href: `https://claude.ai/new?${new URLSearchParams({ + q, + })}`, + icon: ( + + Anthropic + + + ), + }, + { + title: 'Open in Cursor', + href: `https://cursor.com/link/prompt?${new URLSearchParams({ + text: q, + })}`, + icon: ( + + Cursor + + + ), + }, + ]; + }, [githubUrl, markdownUrl]); + + return ( + + + Open + + + + {items.map((item) => ( + + {item.icon} + {item.title} + + + ))} + + + ); +} diff --git a/docs/components/flow-diagram.tsx b/docs/components/flow-diagram.tsx new file mode 100644 index 000000000000..669b472eb2cc --- /dev/null +++ b/docs/components/flow-diagram.tsx @@ -0,0 +1,69 @@ +type FlowStep = { + title: string; + description?: string; + items?: string[]; + metrics?: Record; +}; + +export function FlowDiagram({ steps = [] }: { steps?: FlowStep[] }) { + return ( +
+ {steps.map((step, index) => ( +
+
+
+
+ {index + 1} +
+
+

+ {step.title} +

+ {step.description && ( +

+ {step.description} +

+ )} + {step.items && step.items.length > 0 && ( +
+ {step.items.map((item) => ( + + {item} + + ))} +
+ )} + {step.metrics && ( +
+ {Object.entries(step.metrics).map(([key, value]) => ( +
+ + {key}: + {' '} + + {value} + +
+ ))} +
+ )} +
+
+
+ {index < steps.length - 1 && ( +
+
+
+
+ )} +
+ ))} +
+ ); +} diff --git a/docs/components/ui/button.tsx b/docs/components/ui/button.tsx new file mode 100644 index 000000000000..901b700c26aa --- /dev/null +++ b/docs/components/ui/button.tsx @@ -0,0 +1,28 @@ +import { cva, type VariantProps } from 'class-variance-authority'; + +const variants = { + primary: + 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80 disabled:bg-fd-secondary disabled:text-fd-secondary-foreground', + outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground', + ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground', + secondary: + 'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground', +} as const; + +export const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring', + { + variants: { + variant: variants, + color: variants, + size: { + sm: 'gap-1 px-2 py-1.5 text-xs', + icon: 'p-1.5 [&_svg]:size-5', + 'icon-sm': 'p-1.5 [&_svg]:size-4.5', + 'icon-xs': 'p-1 [&_svg]:size-4', + }, + }, + }, +); + +export type ButtonProps = VariantProps; diff --git a/docs/components/ui/popover.tsx b/docs/components/ui/popover.tsx new file mode 100644 index 000000000000..1667fa2ef439 --- /dev/null +++ b/docs/components/ui/popover.tsx @@ -0,0 +1,32 @@ +'use client'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; +import * as React from 'react'; +import { cn } from '@/lib/cn'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +const PopoverClose = PopoverPrimitive.PopoverClose; + +export { Popover, PopoverTrigger, PopoverContent, PopoverClose }; diff --git a/docs/cli/index-verify.mdx b/docs/content/docs/cli/index-verify.mdx similarity index 97% rename from docs/cli/index-verify.mdx rename to docs/content/docs/cli/index-verify.mdx index fb6a05dd3746..29754dd6bc8d 100644 --- a/docs/cli/index-verify.mdx +++ b/docs/content/docs/cli/index-verify.mdx @@ -1,4 +1,7 @@ -# Index Integrity Verification Command +--- +title: "Index Integrity Verification Command" +description: "Verify that the Spacedrive index matches the filesystem state and report discrepancies." +--- ## Overview @@ -140,7 +143,7 @@ fi | Path Size | Typical Duration | Memory Usage | | ------------- | ---------------- | ------------ | -| 100 files | <1 second | ~10 MB | +| 100 files | `<1 second` | ~10 MB | | 1,000 files | 1-3 seconds | ~50 MB | | 10,000 files | 10-30 seconds | ~200 MB | | 100,000 files | 1-5 minutes | ~1 GB | diff --git a/docs/cli/library-sync-setup.mdx b/docs/content/docs/cli/library-sync-setup.mdx similarity index 98% rename from docs/cli/library-sync-setup.mdx rename to docs/content/docs/cli/library-sync-setup.mdx index 35060bf8d749..c2ee26084778 100644 --- a/docs/cli/library-sync-setup.mdx +++ b/docs/content/docs/cli/library-sync-setup.mdx @@ -1,4 +1,7 @@ -# CLI: Library Sync Setup Commands +--- +title: "CLI: Library Sync Setup Commands" +description: "Configure library synchronization between paired Spacedrive devices from the CLI." +--- ## Overview @@ -495,4 +498,4 @@ sd library sync-setup setup --local-library --remote-device [OPTIONS] # Libraries sd library list # List local libraries sd library info # Show library details -``` \ No newline at end of file +``` diff --git a/docs/cli/linux-deployment.mdx b/docs/content/docs/cli/linux-deployment.mdx similarity index 100% rename from docs/cli/linux-deployment.mdx rename to docs/content/docs/cli/linux-deployment.mdx diff --git a/docs/content/docs/cli/meta.json b/docs/content/docs/cli/meta.json new file mode 100644 index 000000000000..2aa663ad9086 --- /dev/null +++ b/docs/content/docs/cli/meta.json @@ -0,0 +1,11 @@ +{ + "title": "CLI", + "defaultOpen": true, + "pages": [ + "overview", + "linux-deployment", + "library-sync-setup", + "multi-instance", + "index-verify" + ] +} diff --git a/docs/cli/multi-instance.mdx b/docs/content/docs/cli/multi-instance.mdx similarity index 95% rename from docs/cli/multi-instance.mdx rename to docs/content/docs/cli/multi-instance.mdx index f211da5fd83f..5fec8fc20861 100644 --- a/docs/cli/multi-instance.mdx +++ b/docs/content/docs/cli/multi-instance.mdx @@ -1,4 +1,7 @@ -# Multi-Instance Daemon Support +--- +title: "Multi-Instance Daemon Support" +description: "Run multiple isolated Spacedrive daemon instances on a single machine for testing, development, and multi-device scenarios." +--- Spacedrive CLI now supports running multiple daemon instances simultaneously, enabling local testing of device pairing and other multi-device features. @@ -179,4 +182,4 @@ The implementation maintains full backwards compatibility: - Instance names must be valid filenames (no special characters) - Socket discovery happens automatically via filesystem scanning - Daemon startup checks for instance conflicts -- Each instance runs independently with separate process trees \ No newline at end of file +- Each instance runs independently with separate process trees diff --git a/docs/cli/overview.mdx b/docs/content/docs/cli/overview.mdx similarity index 97% rename from docs/cli/overview.mdx rename to docs/content/docs/cli/overview.mdx index 90910f573ced..83908538472d 100644 --- a/docs/cli/overview.mdx +++ b/docs/content/docs/cli/overview.mdx @@ -1,4 +1,7 @@ -# Spacedrive CLI +--- +title: Spacedrive CLI +description: Comprehensive command-line interface for managing Spacedrive Core with full daemon architecture, real-time monitoring, and cross-device file management. +--- A comprehensive command-line interface for managing Spacedrive Core with full daemon architecture, real-time monitoring, and cross-device file management. @@ -111,9 +114,9 @@ sd daemon uninstall The install command creates a LaunchAgent that starts the daemon on login and restarts it if it crashes. Logs write to your data directory at `~/Library/Application Support/spacedrive/logs/`. This works with multiple instances by passing `--instance` before the daemon command. - + Auto-start is currently macOS-only. Linux systemd support is planned. - + ### Multiple Daemon Instances @@ -409,4 +412,4 @@ The CLI follows a modular architecture with clear separation of concerns: 2. **Persistence**: Operations continue after CLI exits 3. **Concurrency**: Multiple CLI clients can connect 4. **Monitoring**: Real-time tracking of background tasks -5. **Isolation**: Multiple instances for testing \ No newline at end of file +5. **Isolation**: Multiple instances for testing diff --git a/docs/core/addressing.mdx b/docs/content/docs/core/addressing.mdx similarity index 97% rename from docs/core/addressing.mdx rename to docs/content/docs/core/addressing.mdx index 38f3c9e2f78e..6f9f642dd50d 100644 --- a/docs/core/addressing.mdx +++ b/docs/content/docs/core/addressing.mdx @@ -1,6 +1,5 @@ --- title: Unified Addressing -sidebarTitle: Addressing --- Spacedrive uses a unified addressing scheme that works seamlessly across local devices, cloud storage, and content-addressed files. Every file has a single, intuitive URI that tells you exactly where it lives - whether it's on your laptop, in S3, or stored by content hash. @@ -48,10 +47,10 @@ Where: - `{identifier}`: Service-specific root (bucket name, folder, container) - `{path}`: Path within the service - + These URIs match AWS CLI, gsutil, and other cloud tools exactly. Copy a path from Spacedrive and paste it into `aws s3 cp` without modification. - + ### Local Files - Device-Aware URIs @@ -70,11 +69,11 @@ Where: - `{device-slug}`: URL-safe device identifier (generated from device name) - `{path}`: Absolute path on that device - + Device slugs are generated automatically from your device name by converting to lowercase and replacing non-alphanumeric characters with hyphens. "Jamie's MacBook Pro" becomes `jamies-macbook-pro`. - + ### Content-Addressed Files @@ -115,11 +114,11 @@ Examples: Each device's slug must be unique within your Spacedrive network. The database enforces this with a unique constraint on the `slug` column. If you try to add a device with a duplicate name, you'll need to rename one device. - + If two devices have the same name (e.g., both "MacBook Pro"), you must give them distinct names before they can sync. Spacedrive will warn you during pairing. - + ### Resolution @@ -450,7 +449,7 @@ This enables multiple configurations for the same cloud resource. ## Related Documentation -- [Devices](/docs/core/devices) - Device management and pairing -- [Volumes](/docs/core/volumes) - Volume system and cloud integration -- [Cloud Integration](/docs/core/cloud-integration) - Cloud storage details -- [Data Model](/docs/core/data-model) - SdPath internal representation +- [Devices](/core/devices) - Device management and pairing +- [Volumes](/core/volumes) - Volume system and cloud integration +- [Cloud Integration](/core/cloud-integration) - Cloud storage details +- [Data Model](/core/data-model) - SdPath internal representation diff --git a/docs/core/api.mdx b/docs/content/docs/core/api.mdx similarity index 97% rename from docs/core/api.mdx rename to docs/content/docs/core/api.mdx index 22f0309316f6..46b0a5a740a7 100644 --- a/docs/core/api.mdx +++ b/docs/content/docs/core/api.mdx @@ -1,11 +1,10 @@ --- title: API -sidebarTitle: API --- - + The API infrastructure is under active development. Some features described here are planned but not yet fully implemented. This document reflects both current implementation and planned architecture. - + The API infrastructure provides a secure, unified entry point for all Spacedrive operations. It handles authentication, authorization, session management, and request routing across CLI, Tauri desktop app, and native applications. @@ -146,9 +145,9 @@ Errors automatically map to HTTP status codes for REST endpoints. ## Middleware Pipeline - + Middleware pipeline is partially implemented. The infrastructure exists but middleware chaining is not yet active. - + Cross-cutting concerns are handled through composable middleware: @@ -190,17 +189,17 @@ pub enum RequestSource { } ``` - + Tauri desktop app currently uses `Other("tauri")` or `Internal`. A dedicated `Tauri` variant is planned. - + This enables source-specific behavior like CLI-friendly error messages. ## API Discovery - + API discovery is a planned feature, not yet implemented. - + Applications will be able to query available operations: @@ -302,7 +301,7 @@ Planned improvements include: **WebSocket Support**: Real-time operation subscriptions. - + The API infrastructure is designed to be transport-agnostic. While examples show Rust usage, the same patterns apply to REST endpoints and FFI boundaries. - + diff --git a/docs/core/architecture.mdx b/docs/content/docs/core/architecture.mdx similarity index 93% rename from docs/core/architecture.mdx rename to docs/content/docs/core/architecture.mdx index 980048ab9a09..162a85fb0d31 100644 --- a/docs/core/architecture.mdx +++ b/docs/content/docs/core/architecture.mdx @@ -1,6 +1,5 @@ --- title: Architecture Overview -sidebarTitle: Overview --- Spacedrive operates as a distributed, local-first system. Your data stays on your devices. A Rust core manages everything through a Virtual Distributed File System (VDFS). No cloud servers control your files. @@ -9,10 +8,10 @@ Spacedrive operates as a distributed, local-first system. Your data stays on you The architecture follows a client-server pattern on your own hardware. A daemon process runs the core engine. Clients connect to access functionality. Every device functions as an equal peer in the network. - + The daemon runs continuously in the background, indexing files and syncing data even when no UI is open. - + ## Core Components @@ -44,10 +43,10 @@ This ensures mutations are safe and auditable. Moving a file first previews the **Queries** read data without side effects. Listing files or searching tags executes instantly without modifying state. - + The strict separation between commands and queries prevents accidental data modification during read operations. - + ## Code Organization @@ -85,10 +84,10 @@ Your phone can: - Sync directly with other devices - Operate without a desktop connection - + The embedded core enables camera roll backup directly from your phone to any device on your network. - + ## System Lifecycle @@ -127,10 +126,10 @@ Every component enforces security at multiple levels: Database encryption protects data at rest. RPC authentication prevents unauthorized access. Peer verification ensures you only sync with trusted devices. - + The daemon's RPC interface binds to localhost only. Remote access requires explicit SSH tunneling or VPN configuration. - + ## Extension System @@ -145,12 +144,12 @@ The SDK enables developers to: Extensions can share models and build on each other's data. A photo management extension can provide face data to a contacts extension. An email archive can feed into knowledge management tools. -Learn more about building extensions in the [Spacedrive Developer SDK](/docs/extensions/introduction) documentation. +Learn more about building extensions in the [Spacedrive Developer SDK](/extensions/introduction) documentation. ## Next Steps Understanding the architecture provides the foundation for working with Spacedrive. Explore these topics next: -- [Libraries](/docs/core/libraries) - How Spacedrive organizes your data -- [Networking](/docs/core/networking) - Peer-to-peer communication details -- [Sync](/docs/core/sync) - Multi-device synchronization protocols +- [Libraries](/core/libraries) - How Spacedrive organizes your data +- [Networking](/core/networking) - Peer-to-peer communication details +- [Sync](/core/sync) - Multi-device synchronization protocols diff --git a/docs/core/cli.mdx b/docs/content/docs/core/cli.mdx similarity index 100% rename from docs/core/cli.mdx rename to docs/content/docs/core/cli.mdx diff --git a/docs/core/cloud-integration.mdx b/docs/content/docs/core/cloud-integration.mdx similarity index 96% rename from docs/core/cloud-integration.mdx rename to docs/content/docs/core/cloud-integration.mdx index 61a3df9d2275..0227ba0671a4 100644 --- a/docs/core/cloud-integration.mdx +++ b/docs/content/docs/core/cloud-integration.mdx @@ -1,6 +1,5 @@ --- title: Cloud Integration -sidebarTitle: Cloud Integration --- Spacedrive treats cloud storage as native volumes, enabling seamless file management across local and cloud storage. The system leverages OpenDAL to support over 40 cloud services while maintaining consistent performance and behavior. @@ -33,11 +32,11 @@ dispatcher.execute_library_action::(input, ctx).await?; Once connected, the volume behaves like any other storage device. Browse directories, search files, and perform operations without thinking about the underlying storage type. - + Once indexed, you can search, browse thumbnails, and view metadata for cloud files even when offline. Only file content operations require an active internet connection. - + ### Path Representation @@ -75,7 +74,7 @@ pub enum SdPath { The service and identifier are stored directly in the path, enabling self-contained cloud addressing. The VolumeManager maintains an in-memory cache mapping mount points to volume fingerprints for O(1) lookups during volume resolution. -See [Unified Addressing](/docs/core/addressing) for complete details on URI formats and resolution. +See [Unified Addressing](/core/addressing) for complete details on URI formats and resolution. ## Supported Services @@ -125,10 +124,10 @@ credential_manager API keys and access credentials remain encrypted at rest. Removing a cloud volume purges its credentials from storage immediately. - + Spacedrive never stores credentials in plain text or transmits them outside the secure credential flow. - + ## Indexing Cloud Content @@ -207,10 +206,10 @@ library.jobs().dispatch(job).await?; Operations optimize based on source and destination capabilities. Same-cloud moves use native APIs when available. Cross-cloud transfers stream data efficiently without temporary files. - + Large file operations between cloud services depend on your internet bandwidth. Monitor transfer progress through the job system. - + ## Caching Strategy @@ -360,10 +359,10 @@ When files change outside Spacedrive: 3. Check cloud provider's version history 4. Resolve conflicts through the UI - + Enable cloud provider webhooks when available for real-time change notifications. - + ## Known Issues @@ -375,7 +374,7 @@ When files change outside Spacedrive: ## Related Documentation -- [Volumes](/docs/core/volumes) - Volume system fundamentals -- [Indexing](/docs/core/indexing) - How Spacedrive indexes files -- [Jobs](/docs/core/jobs) - Monitor long-running operations -- [Security](/docs/security) - Credential and encryption details +- [Volumes](/core/volumes) - Volume system fundamentals +- [Indexing](/core/indexing) - How Spacedrive indexes files +- [Jobs](/core/jobs) - Monitor long-running operations +- [Security](/security) - Credential and encryption details diff --git a/docs/core/data-model.mdx b/docs/content/docs/core/data-model.mdx similarity index 97% rename from docs/core/data-model.mdx rename to docs/content/docs/core/data-model.mdx index 4cf2a65f99d9..2cf002947bd4 100644 --- a/docs/core/data-model.mdx +++ b/docs/content/docs/core/data-model.mdx @@ -1,6 +1,5 @@ --- title: Data Model -sidebarTitle: Data Model --- Spacedrive's data model powers a Virtual Distributed File System (VDFS) that unifies files across all your devices. It enables instant organization, content deduplication, and powerful semantic search while maintaining performance at scale. @@ -83,13 +82,13 @@ The addressing system uses: - **Service-native URIs** for cloud storage (e.g., `s3://`, `gdrive://`, `onedrive://`) - **Content UUIDs** for location-independent references -See [Unified Addressing](/docs/core/addressing) for complete details on URI formats and resolution. +See [Unified Addressing](/core/addressing) for complete details on URI formats and resolution. ## Entry The `Entry` is the core entity representing a file or directory. The database entity (`entities::entry::Model`) stores the fundamental hierarchy and metadata. -```rust Expandable theme={null} +```rust pub struct Entry { pub id: i32, // Database primary key pub uuid: Option, // Global identifier (assigned immediately during indexing) @@ -156,7 +155,7 @@ This structure allows instant queries like "find all files under this directory" ContentIdentity represents unique file content, enabling deduplication across your entire library: -```rust Expandable theme={null} +```rust pub struct ContentIdentity { pub id: i32, pub uuid: Option, // Globally deterministic from content_hash only @@ -199,7 +198,7 @@ Multiple entries can point to the same ContentIdentity. When you have duplicate UserMetadata stores how you organize your files: -```rust Expandable theme={null} +```rust pub struct UserMetadata { pub id: i32, pub uuid: Uuid, @@ -232,7 +231,7 @@ UserMetadata can be scoped two ways: Spacedrive uses a graph-based tagging system that understands context and relationships: -```rust Expandable theme={null} +```rust pub struct Tag { pub id: i32, pub uuid: Uuid, @@ -307,7 +306,7 @@ Tag hierarchies use closure tables for efficient ancestor/descendant queries, si Locations are directories that Spacedrive monitors: -```rust Expandable theme={null} +```rust pub struct Location { pub id: i32, pub uuid: Uuid, @@ -342,7 +341,7 @@ pub struct Location { Devices represent machines in your Spacedrive network: -```rust Expandable theme={null} +```rust pub struct Device { pub id: i32, pub uuid: Uuid, @@ -377,7 +376,7 @@ pub struct Device { Volumes track physical drives and partitions: -```rust Expandable theme={null} +```rust pub struct Volume { pub id: i32, pub uuid: Uuid, @@ -415,13 +414,13 @@ pub struct Volume { } ``` -Volumes serve as the ownership anchor for entries. The `device_id` field determines which device owns all entries on this volume. When a portable drive moves between machines, updating this single field transfers ownership of the entire volume's contents. See [Library Sync](/docs/core/library-sync) for details on portable volume handling. +Volumes serve as the ownership anchor for entries. The `device_id` field determines which device owns all entries on this volume. When a portable drive moves between machines, updating this single field transfers ownership of the entire volume's contents. See [Library Sync](/core/library-sync) for details on portable volume handling. ## Sidecar Sidecars store generated content like thumbnails: -```rust Expandable theme={null} +```rust pub struct Sidecar { pub id: i32, pub uuid: Uuid, @@ -448,19 +447,19 @@ pub struct Sidecar { } ``` - + Sidecars link to ContentIdentity, not Entry. This means one thumbnail serves all duplicate files. - + ## Extension Models Extensions create custom tables at runtime to store domain-specific data. These integrate seamlessly with core tagging and organization. - + The extension system is currently a work in progress. The API and implementation details described here are subject to change. - + ### Table Naming diff --git a/docs/core/database.mdx b/docs/content/docs/core/database.mdx similarity index 99% rename from docs/core/database.mdx rename to docs/content/docs/core/database.mdx index 50dabbb7048e..e749da2f2b98 100644 --- a/docs/core/database.mdx +++ b/docs/content/docs/core/database.mdx @@ -1,6 +1,5 @@ --- title: Database -sidebarTitle: Database --- Spacedrive uses SQLite with SeaORM for database operations. The database is embedded within each library, providing fast local queries and simple backup strategies. @@ -210,10 +209,10 @@ WAL mode provides automatic crash recovery. If Spacedrive crashes, SQLite automa The WAL file contains all pending writes. SQLite replays this journal to restore database consistency. - + Never delete the `-wal` or `-shm` files manually. SQLite uses these for recovery. - + ## Maintenance Operations diff --git a/docs/core/devices.mdx b/docs/content/docs/core/devices.mdx similarity index 91% rename from docs/core/devices.mdx rename to docs/content/docs/core/devices.mdx index 7d99e744b24f..d9dbb3b19fee 100644 --- a/docs/core/devices.mdx +++ b/docs/content/docs/core/devices.mdx @@ -1,6 +1,5 @@ --- title: Devices -sidebarTitle: Devices --- Devices are machines running Spacedrive. Each device has a unique identity, can pair with others, and participates in library synchronization. @@ -44,11 +43,11 @@ pub struct Device { Devices are stored per library, not globally. Each library database contains device records for all participating devices. - + The `devices` table is the **source of truth** for sync state within a library. It tracks which devices are online, when they were last seen, and their sync watermarks. - + ### Device Slugs for Unified Addressing @@ -75,16 +74,16 @@ local://work-desktop/C:/Projects/spacedrive/README.md The database enforces slug uniqueness with a UNIQUE constraint. If two devices would have the same slug (e.g., both named "MacBook Pro"), one must be renamed before they can sync. -See [Unified Addressing](/docs/core/addressing) for complete details on URI formats and slug resolution. +See [Unified Addressing](/core/addressing) for complete details on URI formats and slug resolution. ### Network Layer Handles P2P connections and device pairing through Iroh. - + The network layer uses mDNS for local discovery and QUIC for encrypted communication. - + ## Device Pairing @@ -93,7 +92,9 @@ Pairing establishes trust between devices using a cryptographic handshake. ### Pairing Flow - + +### Generate Code + Device A generates a pairing code valid for 5 minutes. ```typescript const pairing = await client.action("network.pair.generate", {}); @@ -101,7 +102,9 @@ console.log(`Pairing code: ${pairing.code}`); // "ABCD-1234-EFGH" ``` - + +### Enter Code + Device B joins using the pairing code. ```typescript await client.action("network.pair.join", { @@ -110,7 +113,9 @@ await client.action("network.pair.join", { ``` - + +### Establish Trust + Devices exchange cryptographic signatures and derive session keys for future communication. @@ -144,16 +149,16 @@ Ownership flows through volumes. Each device owns its volumes, and volumes own t This indirection enables portable storage: when an external drive moves between machines, updating the volume's device reference transfers ownership of all associated data instantly. - -See [Library Sync](/docs/core/library-sync) for details on the ownership chain and portable volume handling. - + +See [Library Sync](/core/library-sync) for details on the ownership chain and portable volume handling. + ## Sync Participation - + Spacedrive uses a leaderless sync model. All devices are peers with no central authority. - + The `devices` table tracks device state within a library: @@ -161,7 +166,7 @@ The `devices` table tracks device state within a library: - **Sync Enablement**: `sync_enabled` controls whether a device participates - **Last Sync**: `last_sync_at` records when the device last synced -Watermarks for incremental sync are stored separately in sync.db on a per-resource basis. See [Library Sync](/docs/core/library-sync) for details. +Watermarks for incremental sync are stored separately in sync.db on a per-resource basis. See [Library Sync](/core/library-sync) for details. ### How Devices Participate in Sync @@ -170,7 +175,7 @@ Devices sync data using two protocols based on ownership: - **Device-owned data** (volumes, locations, entries): Owner broadcasts state, peers apply. Entries inherit ownership through their volume's device. Tracked via `last_state_watermark`. - **Shared resources** (tags, collections): Any device can modify. Changes ordered via HLC. Tracked via `last_shared_watermark`. -For detailed protocol documentation, see [Library Sync](/docs/core/library-sync). +For detailed protocol documentation, see [Library Sync](/core/library-sync). ### Sync State Management @@ -193,7 +198,7 @@ let online_devices = entities::device::Entity::find() ### Incremental Sync -When devices reconnect after being offline, they use watermarks to determine what data needs synchronization, avoiding full re-sync. Watermarks are tracked per-resource in sync.db. See [Library Sync](/docs/core/library-sync) for implementation details. +When devices reconnect after being offline, they use watermarks to determine what data needs synchronization, avoiding full re-sync. Watermarks are tracked per-resource in sync.db. See [Library Sync](/core/library-sync) for implementation details. ## API Reference @@ -267,9 +272,9 @@ Each device maintains: - **Session keys**: Derived after pairing for encrypted communication - **Trust levels**: Verified (paired) or blocked - + Never share device keys or pairing codes over insecure channels. - + ## Troubleshooting @@ -370,9 +375,9 @@ CREATE TABLE devices ( ); ``` - -Watermarks for incremental sync are stored in sync.db, not the devices table. See [Library Sync](/docs/core/library-sync) for watermark tracking details. - + +Watermarks for incremental sync are stored in sync.db, not the devices table. See [Library Sync](/core/library-sync) for watermark tracking details. + ### Connection State Management @@ -409,6 +414,6 @@ let is_connected = registry.is_device_connected(device_id); ## Related Documentation -- [Sync System](/docs/core/sync) - How devices synchronize data -- [Libraries](/docs/core/libraries) - Multi-device library management -- [Networking](/docs/core/networking) - P2P connection details +- [Sync System](/core/sync) - How devices synchronize data +- [Libraries](/core/libraries) - Multi-device library management +- [Networking](/core/networking) - P2P connection details diff --git a/docs/core/events.mdx b/docs/content/docs/core/events.mdx similarity index 95% rename from docs/core/events.mdx rename to docs/content/docs/core/events.mdx index 32fa8511ec15..a689888766f4 100644 --- a/docs/core/events.mdx +++ b/docs/content/docs/core/events.mdx @@ -1,6 +1,5 @@ --- title: Event System -sidebarTitle: Events --- Spacedrive's event system broadcasts real-time updates to all connected clients using a **unified resource event architecture** that eliminates per-resource event variants in favor of generic, horizontally-scalable events. @@ -50,9 +49,9 @@ Event::ResourceDeleted { - `user_metadata` - User-added metadata (notes, favorites, etc.) - `content_identity` - Deduplicated content records - + Volume events (`VolumeAdded`, `VolumeUpdated`, etc.) and indexing events (`IndexingStarted`, `IndexingProgress`, etc.) are deprecated. Use `ResourceChanged` for volumes and job events for indexing progress. - + ### Infrastructure Events @@ -112,9 +111,9 @@ pub async fn create_collection( ``` The TransactionManager emits `ResourceChanged` after successful commits, ensuring: -- ✅ Events always match database state -- ✅ No forgotten emissions -- ✅ Automatic sync log integration +- Events always match database state +- No forgotten emissions +- Automatic sync log integration ### Manual Emission (Infrastructure Only) @@ -343,6 +342,6 @@ impl EventBus { ## Related Documentation -- **Sync System**: See [sync.md](./sync.md) for event emission during sync -- **Normalized Cache**: See [normalized_cache.md](./normalized_cache.md) for client-side event handling -- **TransactionManager**: See [transactions.md](./transactions.md) for automatic event emission +- **Sync System**: See [sync](./sync) for event emission during sync +- **Normalized Cache**: See [normalized_cache](./normalized_cache) for client-side event handling +- **TransactionManager**: See [transactions](./transactions) for automatic event emission diff --git a/docs/core/file-copy-operations.mdx b/docs/content/docs/core/file-copy-operations.mdx similarity index 98% rename from docs/core/file-copy-operations.mdx rename to docs/content/docs/core/file-copy-operations.mdx index 818779aec36f..3cb9f4d29e7a 100644 --- a/docs/core/file-copy-operations.mdx +++ b/docs/content/docs/core/file-copy-operations.mdx @@ -1,6 +1,5 @@ --- title: File Copy Operations -sidebarTitle: Copy Operations --- Spacedrive uses a strategy-based copy system that automatically selects the optimal method for moving or copying files. The system analyzes your hardware topology, filesystem capabilities, and operation context to choose between instant atomic operations, zero-copy techniques, streaming transfers, or encrypted network transfers. @@ -28,9 +27,9 @@ Moves files on the same volume using atomic filesystem renames. The operation co fs::rename(source_path, dest_path).await?; ``` - + This strategy only works when source and destination are on the same physical volume. Cross-volume moves automatically fall back to streaming copy with source deletion. - + ### FastCopyStrategy @@ -55,9 +54,9 @@ The implementation supports: - Permission and timestamp preservation - Directory recursion with per-file progress - + Buffer sizes automatically adjust based on source and destination volumes. SSDs typically use larger buffers than HDDs, and the strategy picks the minimum of both volumes for cross-device transfers. - + ### RemoteTransferStrategy @@ -123,9 +122,9 @@ Copy operations collect errors in a `failed_copies` vector rather than stopping For move operations, if the copy succeeds but source deletion fails, the operation logs the error but doesn't mark the file as failed. The file exists at the destination, which is the primary goal of the operation. - + When using checksum verification, corrupted copies are automatically deleted to prevent partial or incorrect files from persisting. - + ## Job Resumption diff --git a/docs/core/file-sync.mdx b/docs/content/docs/core/file-sync.mdx similarity index 98% rename from docs/core/file-sync.mdx rename to docs/content/docs/core/file-sync.mdx index 40cc21920d20..9f7bda79ca33 100644 --- a/docs/core/file-sync.mdx +++ b/docs/content/docs/core/file-sync.mdx @@ -1,9 +1,8 @@ --- title: File Sync -sidebarTitle: File Sync --- -This is provisional documentation describing the planned File Sync implementation. +This is provisional documentation describing the planned File Sync implementation. File Sync orchestrates content synchronization between locations by leveraging Spacedrive's Virtual Distributed File System (VDFS) index. Unlike traditional sync tools that scan filesystems, File Sync operates entirely through index queries. This makes sync state resolution a simple database operation and ensures perfect consistency with your indexing rules. @@ -100,9 +99,9 @@ For bidirectional sync, the system uses timestamps to detect conflicts when both ### Index Consistency - + Files excluded from indexing (like `node_modules`) are automatically excluded from sync. This matches user expectations - if you told Spacedrive to ignore it, it won't sync either. - + For cases requiring full directory sync including normally-ignored files, sync conduits support an index mode override: @@ -316,4 +315,4 @@ File Sync excels at personal file management workflows: **Document Archive**: Move completed work to cloud storage automatically **Selective Cache**: Keep recent files local while older content lives in cloud -The index-based design ensures these workflows remain fast and reliable while respecting your organization preferences. \ No newline at end of file +The index-based design ensures these workflows remain fast and reliable while respecting your organization preferences. diff --git a/docs/content/docs/core/filesystems.mdx b/docs/content/docs/core/filesystems.mdx new file mode 100644 index 000000000000..c5079d14794a --- /dev/null +++ b/docs/content/docs/core/filesystems.mdx @@ -0,0 +1,222 @@ +--- +title: Filesystems +--- + +Spacedrive treats the native filesystem as the substrate for everything above it. Detection, capacity reporting, copy-on-write, visibility filtering, and same-storage checks all have filesystem-specific behavior because the abstractions leak: `df` lies about ZFS pool sizes, APFS volumes share containers, Btrfs subvolumes look independent but aren't, and Windows mount points rename themselves. + +This page documents what Spacedrive knows about each filesystem, how it detects them, and where the abstraction boundaries are. + +## Support Matrix + +| Filesystem | Platform | CoW / Clones | Pool-aware | Visibility filter | Capacity correction | +|---|---|---|---|---|---| +| APFS | macOS, iOS | yes (clonefile) | yes (containers) | yes (system volumes) | no | +| Btrfs | Linux | yes (reflink) | yes (subvolumes) | yes (via Linux rules) | no | +| ZFS | Linux | yes (reflink on recent ZoL) | yes (pools) | yes (system pools, apps) | yes (pool root) | +| ReFS | Windows | yes (block clone) | no | no | no | +| NTFS | Windows | no | no | no | no | +| ext2/3/4 | Linux | no | no | yes (via Linux rules) | no | +| XFS | Linux | no | no | yes (via Linux rules) | no | +| FAT32, exFAT | all | no | no | no | no | +| HFS+ | macOS | no | no | yes (system volumes) | no | + +"CoW / Clones" means `std::fs::copy` and the `FastCopyStrategy` produce metadata-only copies when source and destination are on the same filesystem. Everything else falls back to `LocalStreamCopyStrategy` which streams bytes with progress reporting. + +## Detection + +Volume detection runs at startup and on mount/unmount events. Each platform uses a different primary source: + +### macOS (`core/src/volume/platform/macos.rs`) + +Primary: `diskutil apfs list` — gives APFS container topology, volume roles (`Data`, `System`, `VM`, `Preboot`, `Recovery`, `Update`), and mount points. Containers group volumes that share physical storage and space (`ApfsContainer`). + +Fallback: `df -h -T` for non-APFS volumes (HFS+, external FAT32, etc.). + +Classification: +- `/`, `/System/Volumes/Data`, `/System/Volumes/Preboot` etc. are system roles — hidden from user-visible view but still fingerprinted. +- `/Volumes/*` that aren't system roles are External. + +### Linux (`core/src/volume/platform/linux.rs`) + +Primary: `df -h -T` — one line per mounted filesystem with device, type, size, available, mount point. + +Secondary: `/sys/block//queue/rotational` to distinguish SSD from HDD. `/proc/mounts` is also parseable via `parse_proc_mounts()` as an alternative source. + +ZFS datasets get a second pass via `zfs list -H -o name,mountpoint,used,available,type -t filesystem` to enrich each volume with dataset/pool information (see [Capacity Reporting](#capacity-reporting) below). + +### Windows (`core/src/volume/platform/windows.rs`) + +Uses Win32 APIs via `windows-sys`: +- `GetLogicalDrives` to enumerate drive letters. +- `GetVolumeInformationW` for filesystem type and volume label. +- `GetDiskFreeSpaceExW` for capacity. +- `GetVolumeNameForVolumeMountPointW` for the stable `\\?\Volume{GUID}\` path — used as a hardware identifier that survives drive letter changes. + +### iOS (`core/src/volume/platform/ios.rs`) + +Uses the macOS APFS code path but restricted to app-accessible volumes (sandboxed; detection is mostly informational). + +## FilesystemHandler trait + +`core/src/volume/fs/mod.rs` defines a trait each filesystem implements: + +```rust +#[async_trait] +pub trait FilesystemHandler: Send + Sync { + /// Add filesystem-specific fields to a Volume (dataset info, container, subvolume, etc.) + async fn enhance_volume(&self, volume: &mut Volume) -> VolumeResult<()>; + + /// Can these two paths use fast same-storage operations (clone/reflink)? + async fn same_physical_storage(&self, path1: &Path, path2: &Path) -> bool; + + /// Copy strategy to use for this filesystem + fn get_copy_strategy(&self) -> Box; + + /// Filesystem-specific contains-path check (accounts for datasets/subvolumes/etc.) + fn contains_path(&self, volume: &Volume, path: &Path) -> bool; +} +``` + +`get_filesystem_handler(FileSystem)` returns the right implementation, falling back to `GenericFilesystemHandler` for anything unrecognized. + +## Per-filesystem details + +### APFS (`core/src/volume/fs/apfs.rs`) + +- **Containers**: APFS groups volumes into containers that share physical space. `ApfsContainer` is populated from `diskutil apfs list` and attached to each volume. `same_physical_storage` returns true when two paths are on volumes in the same container — that's when `clonefile(2)` produces instant clones. +- **Firmlinks**: macOS silently maps paths like `/Users` onto `/System/Volumes/Data/Users`. `generate_macos_path_mappings()` materializes these mappings so `contains_path` resolves correctly. +- **Role-based visibility**: volumes with roles `System`, `VM`, `Preboot`, `Recovery`, `Update` are marked `is_user_visible = false`. Only `Data` and unroled external volumes appear in the default UI. + +### Btrfs (`core/src/volume/fs/btrfs.rs`) + +- **Subvolumes**: `btrfs subvolume show ` populates `SubvolumeInfo`. Subvolumes on the same Btrfs filesystem share storage. +- **Reflinks**: `same_physical_storage` checks whether two paths share the top-level Btrfs filesystem via `btrfs filesystem show`. If yes, reflinks work between them. + +### ZFS (`core/src/volume/fs/zfs.rs`) + +ZFS is the most-developed filesystem integration because TrueNAS Scale is a common Spacedrive server target. + +- **Datasets and pools**: `zfs list` output is parsed once per detection pass via `fetch_zfs_list_output()` (not per-volume — important for servers with many datasets). Each volume gets matched to its dataset via `find_dataset_for_path`, and the dataset's pool is extracted from the name (`pool/a/b` → pool `pool`). +- **Pool root capacity correction**: see [Capacity Reporting](#capacity-reporting). +- **System pool filter**: `is_system_zfs_pool` matches `boot-pool`, `rpool`, `zroot`. Datasets on these pools are marked `VolumeType::System`, `is_user_visible = false`, and never auto-tracked. +- **App-managed dataset filter**: `is_app_managed_dataset` matches names containing `/ix-applications/`, `/.ix-apps/`, `/docker/`, or `/containerd/`. These are hidden from user view. TrueNAS Scale apps create dozens of nested datasets per app — without this filter the volume list becomes unusable. +- **Clone support**: `supports_clones` returns true for any read-write dataset. ZoL 2.2+ supports reflinks; older versions fall back to streaming copy. + +### ReFS (`core/src/volume/fs/refs.rs`) + +- **Block cloning**: checks for ReFS integrity stream support via `DeviceIoControl` / `FSCTL_DUPLICATE_EXTENTS_TO_FILE`. Sets `supports_block_cloning` on the volume. +- **Version gating**: ReFS 3.x supports block cloning; 2.x doesn't. The handler feature-detects rather than version-checks. + +### NTFS (`core/src/volume/fs/ntfs.rs`) + +No CoW primitive on NTFS, so `get_copy_strategy` returns `LocalStreamCopyStrategy`. The handler mainly exists to provide NTFS-aware `same_physical_storage` (compares Volume GUIDs, not drive letters). + +### Generic (`core/src/volume/fs/generic.rs`) + +Fallback for ext2/3/4, XFS, FAT32, exFAT, HFS+, and anything unrecognized. `same_physical_storage` compares mount point roots. Copy strategy is always `LocalStreamCopyStrategy`. + +## Visibility rules + +Spacedrive tracks far more volumes than it shows. Hidden volumes still get stable fingerprints so locations on them survive remounts, but they don't clutter the default UI and aren't eligible for auto-tracking. + +Two flags drive this: +- `is_user_visible: bool` — shown in the default volume list. +- `auto_track_eligible: bool` — picked up by `volumes.scan`. Always implies `is_user_visible`. + +### Linux rules (`core/src/volume/utils.rs`) + +`is_virtual_filesystem(fs_type)` drops anything backed by kernel memory: `tmpfs`, `proc`, `sysfs`, `devtmpfs`, `cgroup`, `cgroup2`, `squashfs`, `efivarfs`, `overlay`, `fuse`, and ~20 more. These are hidden even before classification. + +`is_system_mount_point(path)` matches Linux OS paths: +- Exact: `/`, `/usr`, `/var`, `/etc`, `/opt`, `/srv`, `/root`, `/boot`, `/home`, `/run`, `/dev`, `/proc`, `/sys`, `/tmp`, `/audit`, `/data`, `/conf`, `/mnt`, `/lost+found`. +- Prefixes: `/boot/`, `/sys/`, `/proc/`, `/dev/`, `/run/`, `/var/log`, `/var/db/`, `/var/lib/systemd`, `/var/local/`, `/var/cache/`. + +The exact-match list includes TrueNAS Scale's split-root datasets (it mounts `/usr`, `/var`, `/etc` as separate ZFS datasets for atomic OS updates). + +`is_nested_app_mount(path)` matches container/app mounts: +- Anything under `ix-applications/` or `.ix-apps/` (TrueNAS apps — one app creates dozens of datasets). +- `docker/overlay2/`, `containerd/`, `kubelet/`, `snap/`. +- `.snapshots/`, `.zfs/snapshot/` (ZFS snapshot browsing mounts). + +`should_hide_by_mount_path(path)` is the combined check. It's applied at: +1. **Detection** — so newly-discovered volumes get `is_user_visible = false` persistently. +2. **Volume list query** (`core/src/ops/volumes/list/query.rs`) — retroactively for tracked volumes whose DB rows predate these filters. +3. **Stats calculation** (`core/src/library/mod.rs`) — so `total_capacity` and `available_capacity` exclude hidden volumes even if the DB flag is stale. + +### ZFS-specific rules + +Applied during ZFS enhancement after `should_hide_by_mount_path`: +- Datasets on `is_system_zfs_pool` pools (boot-pool, rpool, zroot) → hidden + `VolumeType::System`. +- Datasets matching `is_app_managed_dataset` → hidden. + +### macOS rules + +APFS role-based: `System`, `VM`, `Preboot`, `Recovery`, `Update` roles are hidden. Also `/System/Volumes/*` except `/System/Volumes/Data` is hidden by path. + +## Capacity reporting + +### The df-for-ZFS problem + +`df -T` reports `Size = used + available` per mounted dataset. For a ZFS **leaf** dataset this is fine. For a ZFS **pool root** it's misleading: + +``` +$ df -T /mnt/pool +Filesystem Type Size Used Available Mount +pool zfs 15.0T 199M 14.9T /mnt/pool +``` + +The pool root's own `used` is tiny (199 MB) because all the real data lives in descendant datasets. `df` doesn't know that. On a 60 TB pool that's 75% full, `df` says the pool root is "15 TB" — essentially just the free space. + +ZFS's native `used` property on the pool root *does* include descendants: + +``` +$ zfs get used,available pool +pool used 47.0T +pool available 14.9T +``` + +47 T + 14.9 T ≈ 62 T = the real pool capacity after raidz2 parity. + +### Correction + +`enhance_volume_with_cached_output` in `zfs.rs` detects pool-root volumes (`dataset.name == dataset.pool_name`) and overwrites `total_capacity` with `used + available` from `zfs list`. Leaf datasets keep their df-derived values — they're accurate for single-dataset views. + +### Library statistics + +`calculate_volume_capacity` (and `_static`) in `core/src/library/mod.rs` aggregates per-volume capacity with three passes: + +1. Filter by `volume_type` (`Primary`, `UserData`, `External`, `Secondary`). +2. Filter by visibility (`is_user_visible = true` *and* `!should_hide_by_mount_path(mount)`). +3. Deduplicate by fingerprint. +4. Sort by mount-path length (shortest first). +5. For each volume: skip if it's a subpath of an already-counted volume on the same device; otherwise add its capacity to the running totals. + +Subpath dedup handles the common ZFS case: when `/mnt/pool` is tracked along with `/mnt/pool/footage` and `/mnt/pool/cctv`, only `/mnt/pool` gets counted (once). + +### Pool-aware dedup limitation + +Subpath dedup breaks if the user tracks only leaf datasets without the pool root. Each leaf reports the full `available` as its own — summing them over-counts by the pool's free space per extra leaf. + +On TrueNAS this doesn't bite because `df` always detects the pool root. For other setups, proper fix requires either persisting `pool_name` on the volume record or a second dedup pass keyed on `(device_id, file_system=ZFS, available_capacity)`. Neither is implemented yet. + +## Copy strategies + +`core/src/ops/files/copy/strategy.rs` defines three strategies: + +- **`LocalMoveStrategy`** — `fs::rename()` for same-volume moves. Metadata-only. +- **`FastCopyStrategy`** — `std::fs::copy()` which invokes platform CoW primitives (`clonefile` on APFS, `ficlone`/`FICLONERANGE` on Btrfs/ZFS, block cloning on ReFS) when source and destination are on the same filesystem. Falls back to streaming if CoW fails. +- **`LocalStreamCopyStrategy`** — chunked buffered copy with progress events. Used for cross-volume copies and for filesystems without CoW. + +`FilesystemHandler::get_copy_strategy` picks `FastCopyStrategy` for APFS, Btrfs, ZFS, ReFS. Everything else gets `LocalStreamCopyStrategy`. + +Note that `std::fs::copy` itself picks the right syscall — the `FastCopyStrategy`/`LocalStreamCopyStrategy` split is about *whether to try fast copy at all* and how to report progress, not about which syscall to call. + +See [File Copy Operations](/core/file-copy-operations) for the higher-level copy/move API. + +## Known limitations + +- **Leaf-only ZFS dataset tracking** — see [Pool-aware dedup limitation](#pool-aware-dedup-limitation). +- **Windows detection is shallow** — we get capacity and FS type, but not the storage-pool topology that Storage Spaces / ReFS mirroring exposes. Same-pool detection across ReFS volumes isn't implemented. +- **Btrfs subvolume visibility** — we detect subvolumes but don't hide nested subvolumes created by Docker or snapper. Equivalent to ZFS `is_app_managed_dataset` would need a similar name-based filter. +- **Network filesystems (NFS, SMB)** — treated as `MountType::Network` but no protocol-aware capacity or CoW handling. `Available` comes from whatever the server reports via statvfs. +- **Encrypted volumes (LUKS, FileVault, BitLocker)** — opaque to us once mounted; they appear as whatever filesystem is layered on top. diff --git a/docs/core/indexing.mdx b/docs/content/docs/core/indexing.mdx similarity index 98% rename from docs/core/indexing.mdx rename to docs/content/docs/core/indexing.mdx index 29f61beb026f..5bb4a0e213a3 100644 --- a/docs/core/indexing.mdx +++ b/docs/content/docs/core/indexing.mdx @@ -1,6 +1,5 @@ --- title: Indexing -sidebarTitle: Indexing --- Spacedrive's indexing system solves a specific challenge: How do you build a distributed database that feels as fast as a local file explorer? @@ -32,9 +31,9 @@ The critical innovation is how these two layers communicate. When you add a loca - **UI Consistency**: Because UUIDs remain stable, the UI doesn't flicker or reset. Selections, active tabs, and view states remain intact - **Phase Continuation**: The indexer essentially "resumes" from Phase 1, flushing discovered entries to SQLite and proceeding to Phase 2 (Processing) and Phase 3 (Content Analysis) - + This architecture allows Spacedrive to act as your daily driver file explorer. You get instant access to files immediately, with the option to progressively "deepen" the index for files that matter. - + ## Architecture Overview @@ -56,10 +55,10 @@ This dual-implementation architecture unifies watcher and job pipelines, elimina The system integrates deeply with Spacedrive's job infrastructure, which provides automatic state persistence through MessagePack serialization. When you pause an indexing operation, the entire job state is saved to a dedicated jobs database, allowing seamless resumption even after application restarts. - + Indexing jobs can run for hours on large directories. The resumable architecture ensures no work is lost if interrupted. - + ## Database Architecture @@ -265,10 +264,10 @@ Memory usage is around 50 bytes per entry vs 200+ bytes with naive approaches— The `EphemeralIndexCache` tracks which paths have been indexed, are currently being indexed, or are registered for filesystem watching. When a watched path receives filesystem events, the system updates the in-memory index in real-time through the unified `ChangeHandler` trait (shared with persistent storage). - + Ephemeral mode lets you explore USB drives or network shares without permanently adding them to your library. - + ## Data Structures & Optimizations @@ -282,9 +281,9 @@ The ephemeral index doesn't use standard HashMaps. Instead, it uses a memory-map In typical filesystems, filenames like `index.js`, `.DS_Store`, or `conf.yaml` repeat thousands of times. The **NameCache** interns these strings, storing them once and referencing them by pointer. Multiple directory trees can coexist in the same `EphemeralIndex` (browsing both `/mnt/nas` and `/media/usb` simultaneously), sharing the string interning pool for maximum deduplication. - + **Future Roadmap**: We plan to port the Name Pooling strategy from the ephemeral engine to the SQLite database schema. This will significantly reduce the storage footprint of the persistent library by deduplicating filename strings at the database level. - + ### Directory Path Caching (Persistent) @@ -489,14 +488,14 @@ spacedrive job info --detailed 4. **Schedule deep scans** during low-usage periods for large photo/video libraries 5. **Enable checkpointing** for locations over 100K files to survive interruptions - + Always let indexing jobs complete or pause them properly. Force-killing can corrupt the job state and require reindexing from scratch. - + ## Related Documentation -- [Jobs](/docs/core/jobs) - Job system architecture -- [Locations](/docs/core/locations) - Directory management -- [Search](/docs/core/search) - Querying indexed data -- [Performance](/docs/core/performance) - Optimization guide +- [Jobs](/core/jobs) - Job system architecture +- [Locations](/core/locations) - Directory management +- [Search](/core/search) - Querying indexed data +- [Performance](/core/performance) - Optimization guide diff --git a/docs/core/jobs.mdx b/docs/content/docs/core/jobs.mdx similarity index 88% rename from docs/core/jobs.mdx rename to docs/content/docs/core/jobs.mdx index 12454eed536a..72ecba1c2c73 100644 --- a/docs/core/jobs.mdx +++ b/docs/content/docs/core/jobs.mdx @@ -1,6 +1,5 @@ --- title: Job System -sidebarTitle: Jobs --- The job system powers long-running operations in Spacedrive. It provides automatic persistence, progress tracking, and graceful interruption handling for tasks like indexing, file processing, and sync operations. @@ -11,26 +10,42 @@ Jobs execute asynchronously with minimal boilerplate. They persist their state t A job represents a resumable unit of work. Jobs report progress, handle interruptions, and maintain state across executions. The system manages job lifecycles automatically. - + Jobs are library-scoped. Each library maintains its own job database and execution queue. - + ### Job Lifecycle Jobs transition through defined states during execution: - + + +### Queued + Job created and waiting for execution. Initial state after dispatch. + - + + +### Running + Job actively executing. Progress updates flow to subscribers. + - + + +### Paused + Job interrupted but resumable. State persisted to database. + - + + +### Completed + Job finished successfully. Moved to history table. + @@ -100,9 +115,9 @@ impl JobHandler for ProcessFilesJob { } ``` - + The `#[typetag::serde]` attribute enables polymorphic serialization. Jobs must be serializable to support resumption. - + ### Progress Reporting @@ -148,9 +163,9 @@ if corrupt_data() { ctx.report_non_critical_error("Skipped locked file").await; ``` - + Always check `ctx.check_interrupted()` in loops. This enables graceful shutdown and pause operations. - + ## Dispatching Jobs @@ -204,40 +219,21 @@ while let Ok(progress) = progress_rx.recv().await { Jobs persist to a dedicated SQLite database (`jobs.db`) with three tables: - -Active job records containing: - - -Unique job identifier - - -Job type name for registry lookup - - -Schema version for migrations - - -Current job state - - -MessagePack serialized job state - - -Latest progress snapshot - - -Performance statistics JSON - - - - - -Completed jobs moved here for audit trails - - - -Resumption checkpoints for long-running jobs - +**`jobs`** - Active job records containing: + +| Column | Type | Description | +| --- | --- | --- | +| `id` | `UUID` | Unique job identifier | +| `name` | `TEXT` | Job type name for registry lookup | +| `version` | `INTEGER` | Schema version for migrations | +| `status` | `TEXT` | Current job state | +| `data` | `BLOB` | MessagePack serialized job state | +| `progress` | `BLOB` | Latest progress snapshot | +| `metrics` | `TEXT` | Performance statistics JSON | + +**`job_history`** - Completed jobs moved here for audit trails + +**`job_checkpoints`** - Resumption checkpoints for long-running jobs ## Advanced Features @@ -276,9 +272,9 @@ The job system optimizes for throughput and resumability: - Database operations use prepared statements - Channels use bounded capacity to prevent memory growth - + For high-frequency operations, batch work into larger chunks. This reduces checkpoint overhead and improves performance. - + ## Integration Points @@ -372,8 +368,8 @@ println!("Execution time: {}s", metrics.elapsed_seconds); println!("Memory used: {}MB", metrics.memory_mb); ``` - + Never block the job executor thread. Use `tokio::task::spawn_blocking` for CPU-intensive work. - + -The job system provides the foundation for reliable background processing in Spacedrive. Its resumable design ensures operations complete despite interruptions, while the progress system keeps users informed of ongoing work. \ No newline at end of file +The job system provides the foundation for reliable background processing in Spacedrive. Its resumable design ensures operations complete despite interruptions, while the progress system keeps users informed of ongoing work. diff --git a/docs/core/key-manager.mdx b/docs/content/docs/core/key-manager.mdx similarity index 96% rename from docs/core/key-manager.mdx rename to docs/content/docs/core/key-manager.mdx index f97a50510b2c..01f2d9193341 100644 --- a/docs/core/key-manager.mdx +++ b/docs/content/docs/core/key-manager.mdx @@ -1,6 +1,5 @@ --- title: Key Manager -sidebarTitle: Key Manager --- The KeyManager is Spacedrive's unified cryptographic secret storage system. It provides encrypted storage for all sensitive data including device keys, library encryption keys, paired device session keys, and cloud credentials. @@ -152,10 +151,10 @@ User's device ### Key Rotation - + Device key rotation is not currently supported. Rotating the device key would require re-encrypting all secrets in the database. - + Library keys are automatically generated and do not require manual rotation. @@ -294,14 +293,14 @@ The database is compacted automatically by redb. ## Migration from Legacy Storage - + Prior to the unified KeyManager, Spacedrive stored secrets in multiple locations: - Device key: `/master_key` file - Paired devices: `/networking/paired_devices.json` (AES-256-GCM encrypted) - Cloud credentials: OS keychain (unreliable) These systems have been consolidated into KeyManager for better security and reliability. - + ## Troubleshooting @@ -327,10 +326,10 @@ rm ~/.spacedrive/secrets.redb # Restart Spacedrive (new database will be created) ``` - + Deleting `secrets.redb` will unpair all devices and remove cloud credentials. You'll need to re-pair devices and re-authenticate cloud services. - + ### Inspecting Secrets (Development) @@ -388,7 +387,7 @@ cmdkey /list | findstr Spacedrive ## Related Documentation -- [Devices](/docs/core/devices) - Device identity and pairing -- [Networking](/docs/core/networking) - P2P communication using session keys -- [Cloud Integration](/docs/core/cloud-integration) - Cloud credential storage -- [Security](/docs/core/security) - Overall security architecture +- [Devices](/core/devices) - Device identity and pairing +- [Networking](/core/networking) - P2P communication using session keys +- [Cloud Integration](/core/cloud-integration) - Cloud credential storage +- [Security](/core/security) - Overall security architecture diff --git a/docs/core/library-sync.mdx b/docs/content/docs/core/library-sync.mdx similarity index 97% rename from docs/core/library-sync.mdx rename to docs/content/docs/core/library-sync.mdx index 53ddfbf13dd0..5181ef5f9232 100644 --- a/docs/core/library-sync.mdx +++ b/docs/content/docs/core/library-sync.mdx @@ -1,6 +1,5 @@ --- title: Library Sync -sidebarTitle: Library Sync --- Spacedrive synchronizes library metadata across all your devices using a leaderless peer-to-peer model. Every device is equal. No central server, no single point of failure. @@ -13,11 +12,11 @@ Sync uses two protocols based on data ownership: **Shared resources** (tags, collections): Any device can modify. Changes are ordered using Hybrid Logical Clocks (HLC) to ensure consistency across all devices. - + Library Sync handles metadata synchronization. For file content synchronization between storage locations, see [File - Sync](/docs/core/file-sync). - + Sync](/core/file-sync). + ## Quick Reference @@ -119,9 +118,9 @@ This prevents permanent sync failures from transient network issues. ### State-Based Sync (Device-Owned) - + See `core/tests/sync_backfill_test.rs` and `core/tests/sync_realtime_test.rs` for sync protocol tests. - + State-based sync uses two mechanisms depending on the scenario: @@ -144,9 +143,9 @@ No version tracking needed. The owner's state is always authoritative. ### Log-Based Sync (Shared Resources) - + See `core/tests/sync_realtime_test.rs` for shared resource sync tests. - + Shared resources sync through an ordered log. When you create a tag, the device inserts it locally, generates an HLC timestamp, appends to the sync log, and broadcasts a `SharedChange` message. Receiving devices apply changes in HLC order, then acknowledge receipt. Once all peers acknowledge, the entry is pruned from the log. @@ -156,9 +155,9 @@ For large datasets, the system uses HLC-based pagination. Each request includes ## Hybrid Logical Clocks - + HLC conflict resolution is covered in `core/tests/sync_realtime_test.rs`. - + HLCs provide global ordering without synchronized clocks: @@ -366,10 +365,10 @@ CREATE TABLE backfill_checkpoints ( ); ``` - + The sync database stays small (under 1MB) due to aggressive pruning after acknowledgments. - + ## Using the Sync API @@ -555,12 +554,12 @@ for (record, fk_field, missing_uuid) in result.failed { } ``` - + **Separation of Concerns:** `sync_depends_on()` determines the **order** of model synchronization at a high level. `foreign_key_mappings()` handles the **translation** of specific foreign key fields within a model during the actual data transfer. - + ### Dependency Tracking @@ -593,31 +592,37 @@ The tracker maintains a map of `missing_uuid → Vec`. When a r ## Sync Flows - + See `core/tests/sync_backfill_test.rs` and `core/tests/sync_realtime_test.rs` for sync flow tests. - + ### Creating a Location - + Location and entry sync is tested in `test_initial_backfill_alice_indexes_first` in `core/tests/sync_backfill_test.rs`. - + - + +### Device A Creates Location + User adds `/Users/alice/Documents`: - Insert into local database - Call `library.sync_model(&location)` - Send `StateChange` message to connected peers via unidirectional stream - + +### Device B Receives Update + Receives `StateChange` message: - Map device UUID to local ID - Insert location (read-only view) - Update UI instantly - + +### Complete + No conflicts possible (ownership is exclusive) @@ -625,7 +630,9 @@ No conflicts possible (ownership is exclusive) ### Creating a Tag - + +### Device A Creates Tag + User creates "Important" tag: - Insert into local database - Generate HLC timestamp @@ -633,12 +640,16 @@ User creates "Important" tag: - Broadcast to peers - + +### Device B Applies Change + Receives tag creation: - Update local HLC - Apply change in order - Send acknowledgment - + +### Log Cleanup + After all acknowledgments: - Remove from sync log - Log stays small @@ -648,7 +659,9 @@ After all acknowledgments: ### New Device Joins - + +### Pull Shared Resources First + New device sends `SharedChangeRequest`: - Peer responds with recent changes from sync log - If log was pruned, includes current state snapshot @@ -657,7 +670,9 @@ New device sends `SharedChangeRequest`: - Shared resources sync first to satisfy foreign key dependencies (entries reference content identities) - + +### Pull Device-Owned Data + New device sends `StateRequest` to each peer: - Request locations, entries, volumes owned by peer - Peer responds with `StateResponse` containing records in batches - For large datasets, automatically paginates using @@ -665,7 +680,9 @@ New device sends `SharedChangeRequest`: then entries) - + +### Catch Up and Go Live + Process any changes that occurred during backfill from the buffer queue. Transition to Ready state. Begin receiving real-time broadcasts. @@ -676,9 +693,9 @@ Begin receiving real-time broadcasts. ### Transitive Sync - + See `core/tests/sync_backfill_test.rs` for backfill scenarios. - + Spacedrive does not require a direct connection between all devices to keep them in sync. Changes can propagate transitively through intermediaries, ensuring the entire library eventually reaches a consistent state. @@ -691,21 +708,29 @@ This is made possible by two core architectural principles: **How it Works in Practice:** - + + ### 1. Device A syncs to B + Device A creates a new tag. It connects to Device B and syncs the tag. The tag is now stored in the database on both A and B. Device A then goes offline. - + + ### 2. Device C connects to B + Device C comes online and connects only to Device B. It has never communicated with Device A. - + + ### 3. Device C Backfills from B + Device C requests the complete state of all shared resources from Device B. Since Device B has a full copy of the library state (including the tag from Device A), it sends that tag to Device C. - + + ### 4. Library is Consistent + Device C now has the tag created by Device A, even though they never connected directly. The change has propagated transitively. @@ -766,9 +791,9 @@ This prevents creation conflicts for system resources while allowing polymorphic ### Delete Handling - + See `core/tests/sync_realtime_test.rs` for deletion sync tests. - + **Device-owned deletions** use tombstones that sync via `StateResponse`. When you delete a location or folder with thousands of files, only the root UUID is tombstoned. Receiving devices cascade the deletion through their local tree automatically. @@ -791,9 +816,9 @@ Receiving devices look up each deleted UUID and call the same deletion logic use ### Pre-Sync Data - + Pre-sync data backfill is tested in `core/tests/sync_backfill_test.rs`. - + Data created before enabling sync is included during backfill. When the peer log has been pruned or contains fewer items than expected, the response includes a current state snapshot: @@ -813,9 +838,9 @@ The receiving device applies both the incremental changes and the current state ### Watermark-Based Incremental Sync - + See `core/tests/sync_backfill_test.rs` for incremental sync tests. - + When devices reconnect after being offline, they use watermarks to avoid full re-sync. @@ -851,10 +876,10 @@ This syncs only changed records instead of re-syncing the entire dataset. ### Pagination for Large Datasets - + Pagination ensures backfill works reliably for libraries with millions of records. - + Both device-owned and shared resources use cursor-based pagination for large datasets. Batch size is configurable via `SyncConfig`. @@ -1000,9 +1025,9 @@ WatermarkExchangeResponse { ### Connection State Tracking - + See `core/tests/sync_realtime_test.rs` for connection handling tests. - + The sync system uses the Iroh networking layer as the source of truth for device connectivity. When checking if a peer is online, the system queries Iroh's active connections directly rather than relying on cached state. @@ -1074,19 +1099,25 @@ When you move a volume from one device to another, the `Location` associated wit It is handled using a **Hybrid Ownership Sync** model: - + + ### Ownership Change is Requested + When a device detects a known volume that it does not own, it broadcasts a special `RequestLocationOwnership` event. Unlike normal device-owned data, this event is sent to the HLC-ordered log, treating it like a shared resource update. - + + ### Peers Process the Change + Every device in the library processes this event in the same, deterministic order. Upon processing, each peer performs a single, atomic update on its local database: `UPDATE locations SET device_id = 'new_owner_id' WHERE uuid = 'location_uuid'` - + + ### Ownership is Transferred Instantly + This single-row update is all that is required. Because an `Entry`'s ownership is inherited from its parent `Location` at runtime, this change instantly transfers ownership of millions of files. No bulk updates are @@ -1401,7 +1432,7 @@ This reduces network overhead during rapid operations (e.g., bulk tagging). ## Implementation Status -See `core/tests/sync_backfill_test.rs`, `core/tests/sync_realtime_test.rs`, and `core/tests/sync_metrics_test.rs` for the test suite. +See `core/tests/sync_backfill_test.rs`, `core/tests/sync_realtime_test.rs`, and `core/tests/sync_metrics_test.rs` for the test suite. ### Production Ready @@ -1474,7 +1505,7 @@ All models sync automatically during creation, updates, and deletions. File inde ## Extension Sync -Extension sync framework is ready. SDK integration pending. +Extension sync framework is ready. SDK integration pending. Extensions can define syncable models using the same infrastructure as core models. The registry pattern automatically handles new model types without code changes to the sync system. @@ -1581,6 +1612,6 @@ The system is production-ready with all core models syncing automatically. Exten ## Related Documentation -- [Devices](/docs/core/devices) - Device pairing and management -- [Networking](/docs/core/networking) - Network transport layer -- [Libraries](/docs/core/libraries) - Library structure and management +- [Devices](/core/devices) - Device pairing and management +- [Networking](/core/networking) - Network transport layer +- [Libraries](/core/libraries) - Library structure and management diff --git a/docs/core/library.mdx b/docs/content/docs/core/library.mdx similarity index 76% rename from docs/core/library.mdx rename to docs/content/docs/core/library.mdx index 6366a752be8b..a86c7840e216 100644 --- a/docs/core/library.mdx +++ b/docs/content/docs/core/library.mdx @@ -1,6 +1,5 @@ --- title: Library -sidebarTitle: Library --- Libraries are self-contained directories that store all your data, metadata, and thumbnails in one place. Each library lives in a single folder with a `.sdlibrary` extension. You can move libraries between machines, back them up to external drives, or share them with others by copying the directory. When you open a library, Spacedrive loads its database and configuration into memory and locks it to prevent corruption from concurrent access. @@ -29,34 +28,18 @@ Libraries store thumbnails using content-addressed storage with two-level direct The `library.json` file stores all library settings: - - - - Schema version for compatibility - - - - Unique UUID for the library - - - - Display name of the library - - - - User-configurable options including thumbnail quality and sync preferences - - - - Cached counts and sizes for quick display - - - +| Field | Type | Description | +| --- | --- | --- | +| `version` | `number` | Schema version for compatibility | +| `id` | `string` | Unique UUID for the library | +| `name` | `string` | Display name of the library | +| `settings` | `object` | User-configurable options including thumbnail quality and sync preferences | +| `statistics` | `object` | Cached counts and sizes for quick display | ## Portability Self-contained libraries work immediately after copying to a new location with zero configuration. Copy the entire folder to create a complete backup. Store libraries on external drives, network shares, or cloud-synced folders for automatic backup. - + Future versions will add new directories for features like search indexes and version history without breaking existing libraries. - + diff --git a/docs/core/locations.mdx b/docs/content/docs/core/locations.mdx similarity index 95% rename from docs/core/locations.mdx rename to docs/content/docs/core/locations.mdx index 305a91084fb3..6863071a869b 100644 --- a/docs/core/locations.mdx +++ b/docs/content/docs/core/locations.mdx @@ -1,6 +1,5 @@ --- title: Locations -sidebarTitle: Locations --- Locations are directories that Spacedrive tracks and monitors. When you add a location, Spacedrive indexes its contents and watches for changes in real-time. @@ -135,7 +134,7 @@ Entry { 2. **No redundant storage** - Avoids storing device_id on millions of entry records -3. **Instant ownership transfer** - When you move an external drive between devices, just update the location's `device_id`. All millions of files instantly change ownership without touching the entries table. See [Library Sync - Portable Volumes](/docs/core/library-sync#portable-volumes--ownership-changes) for details. +3. **Instant ownership transfer** - When you move an external drive between devices, just update the location's `device_id`. All millions of files instantly change ownership without touching the entries table. See [Library Sync - Portable Volumes](/core/library-sync#portable-volumes--ownership-changes) for details. **Sync implications:** - Only the owning device can modify entries in a location @@ -163,9 +162,9 @@ pub struct Location { } ``` - - See [Data Model - Location](/docs/core/data-model#location) for the complete database schema. - + + See [Data Model - Location](/core/data-model#location) for the complete database schema. + ### Index Modes @@ -291,9 +290,9 @@ RuleToggles { } ``` - + Currently, the watcher uses hardcoded filtering instead of per-location indexer rules. Integration with the indexer rules engine is planned, which will allow each location to have custom filtering rules configured through the UI. - + When indexer rules integration is complete, the watcher will automatically filter events based on each location's configured rules, ensuring consistency between initial indexing and real-time change detection. @@ -391,10 +390,10 @@ Stop tracking a directory: spacedrive location remove ``` - + Removing a location doesn't delete files. It only stops Spacedrive from tracking them. - + ## Troubleshooting @@ -541,9 +540,9 @@ CREATE TABLE locations ( ); ``` -**entries** - All files and directories (see [Data Model](/docs/core/data-model#entry)) +**entries** - All files and directories (see [Data Model](/core/data-model#entry)) -**entry_closure** - Transitive closure table for hierarchy (see [Data Model](/docs/core/data-model#hierarchical-queries)) +**entry_closure** - Transitive closure table for hierarchy (see [Data Model](/core/data-model#hierarchical-queries)) **directory_paths** - Directory path cache ```sql @@ -583,7 +582,7 @@ WHERE e.id = ?; ## Related Documentation -- [Data Model](/docs/core/data-model) - Complete database schema -- [Indexing](/docs/core/indexing) - How files are analyzed -- [Sync](/docs/core/sync) - Cross-device synchronization -- [Events](/docs/core/events) - Event system architecture +- [Data Model](/core/data-model) - Complete database schema +- [Indexing](/core/indexing) - How files are analyzed +- [Sync](/core/sync) - Cross-device synchronization +- [Events](/core/events) - Event system architecture diff --git a/docs/core/memory.mdx b/docs/content/docs/core/memory.mdx similarity index 95% rename from docs/core/memory.mdx rename to docs/content/docs/core/memory.mdx index ce958f1f5dd6..107445274fd8 100644 --- a/docs/core/memory.mdx +++ b/docs/content/docs/core/memory.mdx @@ -1,6 +1,5 @@ --- title: Memory Files -sidebarTitle: Memory Files --- Memory files are Spacedrive's knowledge management primitive. They make AI context portable, persistent, and owned by you. Create a memory file for any task—analyzing financial records, organizing research, refactoring code, understanding email archives—and the knowledge stays with your files forever. @@ -34,9 +33,9 @@ my-task.memory (single file) Updates work by appending new versions of files. The index at the end points to the latest version of each file. Reading requires a single seek operation to the index, then another seek to the file data. - + Memory files are recognized by magic bytes `SDMEMORY` and the `.memory` extension. They appear as document files in Spacedrive. - + ## Structure @@ -106,20 +105,36 @@ MemoryScope::Standalone Memory files integrate with AI agents through a loading mechanism. When an agent loads a memory, it receives curated context instead of discovering it through search. - + + +### Create Memory + Create a memory file for your task or domain. This can happen automatically during agent conversations or manually through the UI. + - + + +### Add Knowledge + Documents and facts accumulate as you work. High-quality conversations automatically generate facts. Manual curation refines the knowledge base. + - + + +### Load in Agent + Open the memory file or load it into a chat session. The agent receives instant context without searching your filesystem. + - + + +### Continuous Improvement + As you continue working, the memory grows. Facts get verified, new documents added, relevance scores adjusted. + @@ -195,9 +210,9 @@ agent.load_memories(vec![ The agent combines knowledge from all loaded memories. This enables reasoning across domains without maintaining a single monolithic knowledge base. - + Create focused memories for specific tasks. Compose them as needed rather than building one large memory for everything. - + ## Performance @@ -221,9 +236,9 @@ Memory files are your data. They live in your filesystem, sync through Spacedriv You can copy memory files like any document. Share them with colleagues to transfer domain expertise. Back them up with your files. Version them in git. The knowledge is yours. - + Memory files are regular Spacedrive content. They sync across devices, appear in search results, and can be tagged and organized like any other file. - + ## Use Cases @@ -247,6 +262,6 @@ This approach enables capabilities impossible with cloud services. Share a memor ## Related Documentation -- [Virtual Sidecars](/docs/core/virtual-sidecars) - Pre-analyzed file data -- [Extensions](/docs/extensions/introduction) - Building extensions with memory support -- [Library Sync](/docs/core/library-sync) - How memories sync across devices +- [Virtual Sidecars](/core/virtual-sidecars) - Pre-analyzed file data +- [Extensions](/extensions/introduction) - Building extensions with memory support +- [Library Sync](/core/library-sync) - How memories sync across devices diff --git a/docs/content/docs/core/meta.json b/docs/content/docs/core/meta.json new file mode 100644 index 000000000000..603234513f28 --- /dev/null +++ b/docs/content/docs/core/meta.json @@ -0,0 +1,40 @@ +{ + "title": "Core", + "defaultOpen": true, + "pages": [ + "---Architecture---", + "architecture", + "library", + "data-model", + "key-manager", + "addressing", + "jobs", + "ops", + "api", + "events", + "memory", + "---File Management---", + "indexing", + "locations", + "devices", + "volumes", + "filesystems", + "file-copy-operations", + "tagging", + "virtual-sidecars", + "---Sync & Network---", + "networking", + "pairing", + "proxy-pairing", + "library-sync", + "file-sync", + "cloud-integration", + "---Development---", + "database", + "testing", + "releases", + "sync-event-log", + "task-tracking", + "cli" + ] +} diff --git a/docs/core/networking.mdx b/docs/content/docs/core/networking.mdx similarity index 97% rename from docs/core/networking.mdx rename to docs/content/docs/core/networking.mdx index 3de1e0f58225..783bb3fdd3d9 100644 --- a/docs/core/networking.mdx +++ b/docs/content/docs/core/networking.mdx @@ -1,6 +1,5 @@ --- title: Networking -sidebarTitle: Networking --- Spacedrive connects devices directly using Iroh, a peer-to-peer networking library built on QUIC. This enables secure communication between your devices without relying on cloud servers. @@ -102,9 +101,9 @@ let node_addr = NodeAddr { endpoint.connect(node_addr, "sync/1.0").await?; ``` - + Direct addresses work on local networks. The relay URL enables connections across the internet when direct connections fail. - + ## Device Pairing @@ -115,7 +114,9 @@ Pairing establishes trust between devices using cryptographic signatures and use The initiator generates a pairing code that the joiner enters to establish trust. - + +### Generate Pairing Code + The initiator creates a BIP39 mnemonic code: ```rust // Initiator generates code @@ -123,7 +124,9 @@ let code = PairingCode::generate(); // "brave-lion-sunset" ``` - + +### Exchange Device Info + Both devices exchange their information and public keys: ```rust pub struct DeviceInfo { @@ -135,7 +138,9 @@ pub struct DeviceInfo { ``` - + +### Challenge-Response + The initiator challenges the joiner to prove they have the code: ```rust // Initiator sends challenge @@ -149,7 +154,9 @@ identity.verify(&challenge, &signature)?; ``` - + +### Establish Session + Both devices derive session keys for future communication: ```rust // Derive shared secret using ECDH @@ -238,7 +245,9 @@ The file transfer protocol enables secure, resumable file sharing between device ### Transfer Process - + +### Request File + Device A requests a file by its entry ID: ```rust let request = FileRequest { @@ -248,7 +257,9 @@ let request = FileRequest { ``` - + +### Stream Chunks + Device B streams the file in encrypted chunks: ```rust // 256KB chunks @@ -261,7 +272,9 @@ while let Some(chunk) = file.read_chunk(CHUNK_SIZE).await? { ``` - + +### Verify Transfer + Both devices verify the transfer using checksums: ```rust let checksum = blake3::hash(&file_data); @@ -384,9 +397,9 @@ let conn = endpoint.connect(node_addr, alpn).await?; // This may be relayed if direct connection impossible ``` - + Relay servers don't decrypt your data. They only forward encrypted packets between devices. - + ## Security @@ -673,6 +686,6 @@ pub trait RelaySelector: Send + Sync { ## Related Documentation -- [Devices](/docs/core/devices) - Device identity and pairing -- [Sync](/docs/core/sync) - Data synchronization over network -- [Security](/docs/core/security) - Encryption and trust model \ No newline at end of file +- [Devices](/core/devices) - Device identity and pairing +- [Sync](/core/sync) - Data synchronization over network +- [Security](/core/security) - Encryption and trust model diff --git a/docs/core/ops.mdx b/docs/content/docs/core/ops.mdx similarity index 98% rename from docs/core/ops.mdx rename to docs/content/docs/core/ops.mdx index 1410a8ad36d3..849ccfc89e65 100644 --- a/docs/core/ops.mdx +++ b/docs/content/docs/core/ops.mdx @@ -1,6 +1,5 @@ --- title: Operations -sidebarTitle: Operations --- The operations system automatically generates type-safe Swift and TypeScript clients from Rust API definitions. Define your API once in Rust and get native clients for iOS, web, and desktop without manual synchronization. @@ -230,9 +229,9 @@ match validation_result { } ``` - + The `validate()` method takes `&self` (a reference), while `execute()` takes `self` (consumes the action). This ensures validation doesn't modify state, while execution can take ownership to transform the action into its result. - + ### Queries @@ -269,10 +268,10 @@ pub enum OperationError { } ``` - + The `Type` derive is required for all types used in operations. This enables Specta to extract type information for client generation. - + ## Wire Protocol @@ -495,14 +494,14 @@ impl ActionMetadata for DeleteAction { } ``` - + Confirmation is handled dynamically through the `validate()` method, not as static metadata. This allows context-aware confirmations based on the actual operation state. - + - + Run `cargo run --bin generate_swift_types` to debug type extraction issues. Check the generated files in `packages/swift/Sources/SpacedriveClient/Generated/`. - + The operations system eliminates manual API maintenance while providing type-safe, performant clients across all platforms. diff --git a/docs/core/pairing.mdx b/docs/content/docs/core/pairing.mdx similarity index 91% rename from docs/core/pairing.mdx rename to docs/content/docs/core/pairing.mdx index 23c56a480def..dcdced4c09ba 100644 --- a/docs/core/pairing.mdx +++ b/docs/content/docs/core/pairing.mdx @@ -1,6 +1,5 @@ --- title: Device Pairing -sidebarTitle: Device Pairing --- Device pairing establishes trust between Spacedrive instances using cryptographic signatures and user-friendly codes. Once paired, devices can communicate securely and share data directly. @@ -89,7 +88,9 @@ QR codes are recommended for: ### For the Initiator - + +### Generate Code + Call the pairing API to generate a code: ```typescript const result = await client.action("network.pair.generate", {}); @@ -103,19 +104,25 @@ console.log(`QR code data: ${result.qr_json}`); ``` - - The device advertises via mDNS (local) and pkarr (internet) and waits for a joiner. The code expires after 5 minutes. + +### Wait for Connection + +The device advertises via mDNS (local) and pkarr (internet) and waits for a joiner. The code expires after 5 minutes. - **Advertisement includes:** - - Session ID (via mDNS user_data) - - Node address published to dns.iroh.link (via pkarr) +**Advertisement includes:** +- Session ID (via mDNS user_data) +- Node address published to dns.iroh.link (via pkarr) - - When a joiner connects, the initiator sends a cryptographic challenge to verify they have the correct code and own their device keys. + +### Verify Joiner + +When a joiner connects, the initiator sends a cryptographic challenge to verify they have the correct code and own their device keys. - + +### Complete Pairing + After verification, both devices exchange session keys and save the pairing relationship. @@ -123,7 +130,9 @@ After verification, both devices exchange session keys and save the pairing rela ### For the Joiner - + +### Enter Code + Enter the code from the initiator (text or QR): ```typescript // Manual entry (local network only) @@ -144,21 +153,27 @@ await client.action("network.pair.join", { ``` - - The system searches for the initiator using: - - **Local network** (mDNS) - Scans for matching session_id - - **Internet** (pkarr/DNS) - Queries dns.iroh.link for node address (requires node_id) - - **Relay servers** - Automatic fallback if direct connection fails + +### Discover Device + +The system searches for the initiator using: +- **Local network** (mDNS) - Scans for matching session_id +- **Internet** (pkarr/DNS) - Queries dns.iroh.link for node address (requires node_id) +- **Relay servers** - Automatic fallback if direct connection fails - With QR codes, both paths run simultaneously and the first to succeed wins. +With QR codes, both paths run simultaneously and the first to succeed wins. - - Sign a challenge from the initiator to prove you have the code and own your - device keys. + +### Prove Identity + +Sign a challenge from the initiator to prove you have the code and own your +device keys. - + +### Save Relationship + Store the paired device information and session keys for future communication. @@ -167,7 +182,7 @@ Store the paired device information and session keys for future communication. Proxy pairing lets a new device join a network after a single direct pairing. The device that completed the direct pairing can vouch for the new device to other trusted devices. Each receiving device can accept or reject the vouch. -See [Proxy Pairing](/docs/core/proxy-pairing) for the full protocol, resource model, and flows. +See [Proxy Pairing](/core/proxy-pairing) for the full protocol, resource model, and flows. ## Technical Architecture @@ -241,10 +256,10 @@ pub struct PairingSession { } ``` - + Sessions expire after 5 minutes. Users must complete pairing within this time window. - + ## Discovery Mechanisms @@ -291,9 +306,9 @@ endpoint.connect(node_addr, PAIRING_ALPN).await?; - Pkarr returns all connection options (relay + direct) - Takes 5-15 seconds including DNS resolution - + Pkarr uses DNS-based discovery backed by the Mainline DHT. It's more reliable than traditional DHT for NAT traversal and works globally. - + ### Dual-Path Discovery @@ -330,9 +345,9 @@ endpoint.connect(node_addr, PAIRING_ALPN).await?; // Tries direct, then relay - Relay URLs discovered automatically via pkarr - Custom relay support coming soon (configurable per-node) - + Relay servers only forward encrypted QUIC traffic. They cannot decrypt your data or compromise security. - + ## Cryptographic Details @@ -601,7 +616,7 @@ Solutions: ## Related Documentation -- [Networking](/docs/core/networking) - Network transport details -- [Devices](/docs/core/devices) - Device management system -- [Proxy Pairing](/docs/core/proxy-pairing) - Vouching based pairing flow -- [Security](/docs/core/security) - Cryptographic architecture +- [Networking](/core/networking) - Network transport details +- [Devices](/core/devices) - Device management system +- [Proxy Pairing](/core/proxy-pairing) - Vouching based pairing flow +- [Security](/core/security) - Cryptographic architecture diff --git a/docs/core/proxy-pairing.mdx b/docs/content/docs/core/proxy-pairing.mdx similarity index 99% rename from docs/core/proxy-pairing.mdx rename to docs/content/docs/core/proxy-pairing.mdx index 631cc67040f8..d17314cfbb1c 100644 --- a/docs/core/proxy-pairing.mdx +++ b/docs/content/docs/core/proxy-pairing.mdx @@ -1,6 +1,5 @@ --- title: Proxy Pairing -sidebarTitle: Proxy Pairing --- Proxy pairing lets a new device join a network after a single direct pairing. The device that paired directly can vouch for the new device to other trusted devices. This reduces repeated pairing while keeping explicit user approval. diff --git a/docs/core/releases.mdx b/docs/content/docs/core/releases.mdx similarity index 95% rename from docs/core/releases.mdx rename to docs/content/docs/core/releases.mdx index 641bad0ce925..e399efac736c 100644 --- a/docs/core/releases.mdx +++ b/docs/content/docs/core/releases.mdx @@ -1,6 +1,5 @@ --- title: Releases -sidebarTitle: Releases --- Spacedrive uses GitHub Actions workflows, git tags, and a custom xtask command to manage releases. This page covers the full release pipeline from version bump to published artifacts. @@ -88,7 +87,7 @@ Desktop builds go through `tauri-action` which handles code signing (macOS), Tau The final `release` job waits for all builds to complete, downloads all artifacts, and creates a draft release on GitHub. The release is always created as a draft so you can review before publishing. -The release is created as a **draft**. You must manually publish it on GitHub after verifying the artifacts. +The release is created as a **draft**. You must manually publish it on GitHub after verifying the artifacts. ### Server Docker (`server.yml`) @@ -132,7 +131,7 @@ Runs on a self-hosted macOS ARM64 runner (same as desktop macOS builds). TestFli Runs on a Linux runner with Java 17 and the Android SDK/NDK. -The mobile workflow is manual dispatch only. It is not triggered by tags. Run it from the Actions tab when you want to push a mobile build to testers. +The mobile workflow is manual dispatch only. It is not triggered by tags. Run it from the Actions tab when you want to push a mobile build to testers. **Required secrets (in addition to existing Apple secrets):** diff --git a/docs/core/sync-event-log.mdx b/docs/content/docs/core/sync-event-log.mdx similarity index 98% rename from docs/core/sync-event-log.mdx rename to docs/content/docs/core/sync-event-log.mdx index f976b3b2da49..d425533d2fca 100644 --- a/docs/core/sync-event-log.mdx +++ b/docs/content/docs/core/sync-event-log.mdx @@ -1,6 +1,5 @@ --- title: Sync Event Log -sidebarTitle: Sync Event Log --- Spacedrive maintains a persistent event log of high-level sync operations for debugging and observability. Unlike the in-memory metrics system, these events survive app restarts and provide a queryable timeline of what happened during sync. @@ -14,7 +13,7 @@ The sync event log captures critical lifecycle events, data flow operations, and - Batch aggregation (10-1000x write reduction) - Correlation IDs (track entire backfill sessions) - Flexible query API (filter by type, time, peer, severity) -- Minimal overhead (<100KB for 7 days) +- Minimal overhead (`<100KB` for 7 days) ## Event Types @@ -460,8 +459,8 @@ Expected storage: **Query Performance:** - 5 indexes for common query patterns -- Typical query: <50ms for 1000 events -- Complex filters: <200ms +- Typical query: `<50ms` for 1000 events +- Complex filters: `<200ms` **Memory Overhead:** - BatchAggregator: ~100KB for pending batches @@ -593,6 +592,6 @@ Human-readable table format: ## Related Documentation -- [Library Sync](/docs/core/library-sync) - Sync protocol and architecture -- [Testing](/docs/core/testing) - Sync integration tests -- [Events](/docs/core/events) - General event system +- [Library Sync](/core/library-sync) - Sync protocol and architecture +- [Testing](/core/testing) - Sync integration tests +- [Events](/core/events) - General event system diff --git a/docs/core/tagging.mdx b/docs/content/docs/core/tagging.mdx similarity index 99% rename from docs/core/tagging.mdx rename to docs/content/docs/core/tagging.mdx index e7fc6cf2d008..3b6af3e81fea 100644 --- a/docs/core/tagging.mdx +++ b/docs/content/docs/core/tagging.mdx @@ -1,6 +1,5 @@ --- title: Spacedrive Semantic Tagging System -sidebarTitle: Semantic Tagging --- ## Overview diff --git a/docs/core/task-tracking.mdx b/docs/content/docs/core/task-tracking.mdx similarity index 98% rename from docs/core/task-tracking.mdx rename to docs/content/docs/core/task-tracking.mdx index 7eef10036aab..a4af705c0b38 100644 --- a/docs/core/task-tracking.mdx +++ b/docs/content/docs/core/task-tracking.mdx @@ -1,6 +1,5 @@ --- title: Task Tracking -sidebarTitle: Task Tracking --- The `.tasks/` directory contains a structured task tracking system that manages the entire development lifecycle of Spacedrive. Tasks are defined as markdown files with YAML frontmatter and validated against a JSON schema. @@ -99,9 +98,9 @@ Task is complete. All acceptance criteria must be met: - Tests pass (if applicable) - Feature is production-ready - + Never mark a task as `Done` if implementation is partial, tests are failing, or the feature doesn't work as specified. - + ## Task Prefixes @@ -251,9 +250,9 @@ When code has been merged, tasks should be reviewed and updated: cargo run --bin task-validator -- validate ``` - + When unsure if a task is complete, leave it as `In Progress` rather than prematurely marking it `Done`. The tracker is only useful if it's accurate. - + ### Creating New Tasks @@ -270,7 +269,7 @@ When unsure if a task is complete, leave it as `In Progress` rather than prematu Example core task file (`.tasks/core/LSYNC-017-realtime-sync.md`): -```markdown +````markdown --- id: LSYNC-017 title: Real-time Sync Protocol @@ -299,11 +298,11 @@ immediately rather than relying on polling intervals. - [ ] Protocol handles network interruptions gracefully - [ ] Fallback to polling when real-time unavailable - [ ] Integration tests demonstrate cross-device sync -``` +```` Example interface task file (`.tasks/interface/EXPL-004-breadcrumbs.md`): -```markdown +````markdown --- id: EXPL-004 title: Breadcrumb Navigation @@ -333,7 +332,7 @@ current path with clickable segments. - [ ] Path updates when navigating - [ ] Works with virtual locations and SD paths - [ ] Responsive truncation for long paths -``` +```` ### Updating Task Status @@ -497,7 +496,7 @@ Will report broken parent references. Either remove the parent field or create t ### Complete Epic with Subtasks -```markdown +````markdown # .tasks/SEARCH-000-semantic-search.md --- id: SEARCH-000 @@ -518,9 +517,9 @@ Build a semantic search system using embeddings and vector similarity. - SEARCH-001: Async search job infrastructure - SEARCH-002: Two-stage FTS + semantic reranking - SEARCH-003: Unified vector repositories -``` +```` -```markdown +````markdown # .tasks/SEARCH-001-async-searchjob.md --- id: SEARCH-001 @@ -543,7 +542,7 @@ Implement background search job that processes queries asynchronously. - [x] Queries execute in background thread pool - [x] Results stream back via events - [x] Integration test demonstrates full workflow -``` +```` ## Integration with Development diff --git a/docs/core/testing.mdx b/docs/content/docs/core/testing.mdx similarity index 99% rename from docs/core/testing.mdx rename to docs/content/docs/core/testing.mdx index 7fb45398ac67..1cf27105fc64 100644 --- a/docs/core/testing.mdx +++ b/docs/content/docs/core/testing.mdx @@ -1,6 +1,5 @@ --- title: Testing -sidebarTitle: Testing --- Testing in Spacedrive Core ensures reliability across single-device operations and multi-device networking scenarios. This guide covers the available frameworks, patterns, and best practices. @@ -167,10 +166,10 @@ async fn alice_pairing() { } ``` - + Device scenario functions must be marked with `#[ignore]` to prevent direct execution. They only run when called by the subprocess framework. - + ### Process Coordination @@ -245,10 +244,10 @@ watcher.watch_ephemeral(dest_dir.clone()).await?; // Now filesystem events will be detected ``` - + The `IndexerJob` automatically calls `watch_ephemeral()` after successful indexing, so manual registration is only needed when bypassing the indexer. - + #### Persistent Location Watching @@ -708,10 +707,10 @@ The framework provides comprehensive test helpers in `core/tests/helpers/`: - `wait_for_indexing()` - Wait for indexing job completion - `register_device()` - Register a device in a library - + See `core/tests/helpers/README.md` for detailed documentation on all available helpers including usage examples and migration guides. - + ### Test Volumes @@ -882,10 +881,10 @@ The test will automatically: - Appear in `cargo xtask test-core` output - Show in progress tracking and summary - + Core integration tests use `--test-threads=1` to avoid conflicts when accessing the same locations or performing filesystem operations. - + ## Running Tests @@ -967,10 +966,10 @@ let desktop_path = std::env::var("HOME").unwrap() + "/Desktop"; ### Debugging - + When tests fail, check the logs in `test_data/{test_name}/library/logs/` for detailed information about what went wrong. - + Common debugging approaches: @@ -1098,10 +1097,10 @@ async fn test_typescript_cache_updates() -> anyhow::Result<()> { } ``` - + Use `.enable_daemon()` on `IndexingHarnessBuilder` to start the RPC server. The daemon listens on a random TCP port returned by `.daemon_socket_addr()`. - + #### TypeScript Side @@ -1255,10 +1254,10 @@ cd packages/ts-client BRIDGE_CONFIG_PATH=/path/to/config.json bun test tests/integration/mytest.test.ts ``` - + Use `--nocapture` to see TypeScript test output. The Rust test prints all stdout/stderr from the TypeScript test process. - + ### Common Scenarios diff --git a/docs/core/virtual-sidecars.mdx b/docs/content/docs/core/virtual-sidecars.mdx similarity index 99% rename from docs/core/virtual-sidecars.mdx rename to docs/content/docs/core/virtual-sidecars.mdx index 610ad8eefe5f..b5db84a5a3b6 100644 --- a/docs/core/virtual-sidecars.mdx +++ b/docs/content/docs/core/virtual-sidecars.mdx @@ -1,6 +1,5 @@ --- title: Virtual Sidecar System (VSS) -sidebarTitle: Virtual Sidecars --- The Virtual Sidecar System (VSS) manages derivative data associated with your files - thumbnails, OCR text, video transcripts, embeddings, and other metadata-rich artifacts. diff --git a/docs/core/volumes.mdx b/docs/content/docs/core/volumes.mdx similarity index 97% rename from docs/core/volumes.mdx rename to docs/content/docs/core/volumes.mdx index bbd8ae3f105d..e04d38bd0f13 100644 --- a/docs/core/volumes.mdx +++ b/docs/content/docs/core/volumes.mdx @@ -1,6 +1,5 @@ --- title: Volumes -sidebarTitle: Volumes --- Spacedrive detects and tracks storage volumes across all platforms, including local drives and cloud storage. The volume system enables intelligent file operations by understanding where data lives and how to move it efficiently. @@ -36,9 +35,9 @@ This fingerprint remains stable even when mount points change. Volumes serve as the ownership anchor for the sync system. Entries and locations reference a volume, inheriting ownership from the volume's device. This indirection enables portable storage: when you plug an external drive into a different machine, updating the volume's device reference transfers ownership of all associated files instantly. No bulk updates needed. - -See [Library Sync](/docs/core/library-sync) for details on how ownership flows through volumes and enables seamless device transfers. - + +See [Library Sync](/core/library-sync) for details on how ownership flows through volumes and enables seamless device transfers. + ### Unified Addressing @@ -52,7 +51,7 @@ azblob://container/data/export.csv ← Azure Blob gcs://bucket/logs/app.log ← Google Cloud Storage ``` -These URIs work identically to local volume paths and enable seamless operations across all storage backends. See [Unified Addressing](/docs/core/addressing) for complete details. +These URIs work identically to local volume paths and enable seamless operations across all storage backends. See [Unified Addressing](/core/addressing) for complete details. ## Volume Types @@ -206,11 +205,11 @@ if volume.supports_cow() { } ``` - + - COW copies on supported filesystems are nearly instant regardless of file size - Server-side cloud copies avoid downloading/uploading data through your machine - Cross-cloud operations automatically stream through local system - + ## Smart File Operations @@ -442,6 +441,6 @@ This enables true cross-storage deduplication: - Skip uploading files that already exist in cloud - Find files across all storage locations using content hash - + Large files (>100KB) use sample-based hashing, transferring only ~58KB for content identification regardless of file size. This makes cloud indexing efficient even on slow connections. - + diff --git a/docs/extensions/core-concepts.mdx b/docs/content/docs/extensions/core-concepts.mdx similarity index 99% rename from docs/extensions/core-concepts.mdx rename to docs/content/docs/extensions/core-concepts.mdx index 1168e891e6c1..c0b0ca632386 100644 --- a/docs/extensions/core-concepts.mdx +++ b/docs/content/docs/extensions/core-concepts.mdx @@ -1,6 +1,5 @@ --- title: Core Concepts -sidebarTitle: Core Concepts --- Extensions in Spacedrive are built with Rust macros that generate the integration code for you. Define your data models, jobs, and agents using attributes. The SDK handles sync, permissions, and sandboxing automatically. diff --git a/docs/extensions/data-storage.mdx b/docs/content/docs/extensions/data-storage.mdx similarity index 99% rename from docs/extensions/data-storage.mdx rename to docs/content/docs/extensions/data-storage.mdx index 09cd228fa45c..846ee2c02bc1 100644 --- a/docs/extensions/data-storage.mdx +++ b/docs/content/docs/extensions/data-storage.mdx @@ -1,6 +1,5 @@ --- title: Data Storage -sidebarTitle: Data Storage --- Extensions get their own database tables and file storage locations. You define data models in Rust, and Spacedrive creates the tables automatically. You declare storage locations, and users choose where files go during installation. diff --git a/docs/extensions/examples.mdx b/docs/content/docs/extensions/examples.mdx similarity index 99% rename from docs/extensions/examples.mdx rename to docs/content/docs/extensions/examples.mdx index 1f4ce9cbab80..b559d81987d1 100644 --- a/docs/extensions/examples.mdx +++ b/docs/content/docs/extensions/examples.mdx @@ -1,6 +1,5 @@ --- title: Examples -sidebarTitle: Examples --- ## Example: A Simple CRM Extension diff --git a/docs/extensions/getting-started.mdx b/docs/content/docs/extensions/getting-started.mdx similarity index 93% rename from docs/extensions/getting-started.mdx rename to docs/content/docs/extensions/getting-started.mdx index 3f649b5a2e32..bed987333a58 100644 --- a/docs/extensions/getting-started.mdx +++ b/docs/content/docs/extensions/getting-started.mdx @@ -1,6 +1,5 @@ --- title: Getting Started -sidebarTitle: Getting Started --- ## Setting Up Your Development Environment diff --git a/docs/extensions/introduction.mdx b/docs/content/docs/extensions/introduction.mdx similarity index 99% rename from docs/extensions/introduction.mdx rename to docs/content/docs/extensions/introduction.mdx index a46bdd16e0c2..36b167148daf 100644 --- a/docs/extensions/introduction.mdx +++ b/docs/content/docs/extensions/introduction.mdx @@ -1,6 +1,5 @@ --- title: Introduction to Extensions -sidebarTitle: Introduction --- Extensions turn Spacedrive into a platform. Build custom tools for any workflow using Rust, compile to WASM, and integrate seamlessly with Spacedrive's sync, search, and storage infrastructure. diff --git a/docs/content/docs/extensions/meta.json b/docs/content/docs/extensions/meta.json new file mode 100644 index 000000000000..a620ce7e60c6 --- /dev/null +++ b/docs/content/docs/extensions/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Extensions", + "defaultOpen": true, + "pages": [ + "introduction", + "getting-started", + "core-concepts", + "data-storage", + "security-and-sync", + "ui-integration", + "examples" + ] +} diff --git a/docs/extensions/security-and-sync.mdx b/docs/content/docs/extensions/security-and-sync.mdx similarity index 99% rename from docs/extensions/security-and-sync.mdx rename to docs/content/docs/extensions/security-and-sync.mdx index 60014c4314aa..4a99cf72620e 100644 --- a/docs/extensions/security-and-sync.mdx +++ b/docs/content/docs/extensions/security-and-sync.mdx @@ -1,6 +1,5 @@ --- title: Security and Sync -sidebarTitle: Security & Sync --- ## A Secure and Synchronized Ecosystem diff --git a/docs/extensions/ui-integration.mdx b/docs/content/docs/extensions/ui-integration.mdx similarity index 98% rename from docs/extensions/ui-integration.mdx rename to docs/content/docs/extensions/ui-integration.mdx index 4a7bcf11df48..8a36e539bc8b 100644 --- a/docs/extensions/ui-integration.mdx +++ b/docs/content/docs/extensions/ui-integration.mdx @@ -1,6 +1,5 @@ --- title: UI Integration -sidebarTitle: UI Integration --- ## Extending the Spacedrive Interface diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json new file mode 100644 index 000000000000..647a556d5485 --- /dev/null +++ b/docs/content/docs/meta.json @@ -0,0 +1,15 @@ +{ + "title": "Spacedrive", + "pages": [ + "---Overview---", + "...overview", + "---Developer---", + "...core", + "---SDK---", + "...extensions", + "---CLI---", + "...cli", + "---Interface---", + "...react" + ] +} diff --git a/docs/overview/add-index-locations.mdx b/docs/content/docs/overview/add-index-locations.mdx similarity index 58% rename from docs/overview/add-index-locations.mdx rename to docs/content/docs/overview/add-index-locations.mdx index 28e5c67987f0..7b07e003ebff 100644 --- a/docs/overview/add-index-locations.mdx +++ b/docs/content/docs/overview/add-index-locations.mdx @@ -1,71 +1,95 @@ --- title: Add and Index Locations -sidebarTitle: Add Locations +description: Connect folders to Spacedrive and tune indexing for different content types. --- Locations connect your existing folders to Spacedrive, making files searchable and organized without moving them. This guide covers adding locations, configuring indexing options, and optimizing performance for different use cases. You'll learn to index everything from small document folders to massive media libraries efficiently. -## Understand Locations +## Understand locations A location represents a folder on your device that Spacedrive monitors. Key concepts: -**Non-Destructive**: Spacedrive never moves or modifies your original files. It only reads and indexes. +**Non-destructive**: Spacedrive never moves or modifies your original files. It only reads and indexes. -**Device-Specific**: Each location belongs to one device. Other devices see the files but can't access the physical path. +**Device-specific**: each location belongs to one device. Other devices see the files but can't access the physical path. -**Real-Time Monitoring**: Spacedrive watches for changes and updates the index automatically. +**Real-time monitoring**: Spacedrive watches for changes and updates the index automatically. -**Flexible Depth**: Control how deeply Spacedrive analyzes files, from basic names to full content extraction. +**Flexible depth**: control how deeply Spacedrive analyzes files, from basic names to full content extraction. - -Locations can be local folders, external drives, or network mounts. Spacedrive treats them equally once indexed. - + + Locations can be local folders, external drives, or network mounts. + Spacedrive treats them equally once indexed. + -## Add Your First Location +## Add your first location - + + +### Open location manager + Click the + button next to "Locations" in sidebar or navigate to Settings → Locations. + - + + +### Select folder + Click "Add Location" and browse to the folder you want to index. Start with something manageable. + - + + +### Configure basic settings + Set initial options: -- **Display Name**: Friendly name shown in sidebar -- **Index Mode**: How deeply to analyze (see below) -- **Hidden Files**: Include or exclude dot files + +- **Display name**: friendly name shown in sidebar +- **Index mode**: how deeply to analyze (see below) +- **Hidden files**: include or exclude dot files + - + + +### Start indexing + Click "Add" to begin. Monitor progress in the job manager (bottom status bar). + - -For testing, start with a folder containing \<1,000 files. This indexes quickly and helps you understand the process. - + + For testing, start with a folder containing less than 1,000 files. This + indexes quickly and helps you understand the process. + -## Indexing Modes Explained +## Indexing modes explained Choose the right mode for your needs: -### Shallow Indexing +### Shallow indexing + Fastest option capturing basic metadata: + - File names and extensions -- Size and timestamps +- Size and timestamps - Directory structure - Basic file type detection Use for: + - Quick overviews - Temporary folders - Network drives - Initial exploration -### Deep Indexing (Recommended) +### Deep indexing (recommended) + Comprehensive analysis including: + - Everything from shallow mode - Content-based hashing - Thumbnail generation @@ -73,55 +97,78 @@ Comprehensive analysis including: - Text extraction (coming soon) Use for: + - Photo libraries -- Document archives +- Document archives - Media collections - Permanent storage -### Content Indexing +### Content indexing + Full text and analysis (future release): + - Everything from deep mode - OCR for images - Full text search - AI-powered tagging - Semantic relationships - -Indexing mode affects initial scan only. You can re-index with different settings later. - + + Indexing mode affects initial scan only. You can re-index with different + settings later. + -## Configure Location Settings +## Configure location settings Fine-tune each location after adding: - + + +### Access location settings + Right-click location in sidebar and select "Settings" or click gear icon. + - + + +### Indexing rules + Customize what gets indexed: -- **File Extensions**: Include/exclude specific types -- **Size Limits**: Skip files over certain size -- **Path Patterns**: Ignore folders matching patterns -- **Date Ranges**: Only index recent files + +- **File extensions**: include/exclude specific types +- **Size limits**: skip files over certain size +- **Path patterns**: ignore folders matching patterns +- **Date ranges**: only index recent files + - + + +### Watching options + Control real-time monitoring: -- **Watch for Changes**: Enable filesystem monitoring -- **Watch Frequency**: How often to check (default: instant) -- **Process Immediately**: Index new files right away + +- **Watch for changes**: enable filesystem monitoring +- **Watch frequency**: how often to check (default: instant) +- **Process immediately**: index new files right away + - + + +### Advanced settings + Performance and behavior: -- **Priority**: Processing order vs other locations -- **Parallelism**: Concurrent file processing -- **Thumbnail Size**: Quality vs storage tradeoff + +- **Priority**: processing order vs other locations +- **Parallelism**: concurrent file processing +- **Thumbnail size**: quality vs storage tradeoff + -## Index Different Content Types +## Index different content types -### Photo Libraries +### Photo libraries Optimize for image collections: @@ -134,23 +181,23 @@ Settings for Photo Location: - Watch: Enabled ``` - -Enable "Generate Previews" for instant photo browsing without opening files. - + + Enable "Generate Previews" for instant photo browsing without opening files. + -### Document Archives +### Document archives Best practices for documents: ``` Settings for Documents: -- Indexing: Deep +- Indexing: Deep - Include: *.pdf, *.docx, *.txt - OCR: Enabled (when available) - Versioning: Track changes ``` -### Media Collections +### Media collections Handle video and audio efficiently: @@ -162,7 +209,7 @@ Settings for Media: - Exclude: *.tmp, *.partial ``` -### Development Projects +### Development projects Index code intelligently: @@ -174,76 +221,117 @@ Settings for Code: - Watch: Enabled for hot reload ``` - -Avoid indexing package directories like `node_modules`. They contain thousands of files that change frequently. - + + Avoid indexing package directories like `node_modules`. They contain + thousands of files that change frequently. + -## Add External Drives +## Add external drives Index removable storage: - + + +### Connect drive + Attach external drive and ensure it mounts properly. + - + + +### Add as location + Select the drive root or specific folders. Name clearly like "Backup Drive - 4TB". + - + + +### Configure offline behavior + In settings, enable "Offline Mode": + - Preserves index when disconnected - Shows files as unavailable - Re-syncs on reconnection + - -Optional: Configure system to auto-mount and index when connected. + + +### Set auto-mount + +Optional: configure system to auto-mount and index when connected. + - -Spacedrive remembers external drives. Reconnecting automatically updates the index for changes made elsewhere. - + + Spacedrive remembers external drives. Reconnecting automatically updates the + index for changes made elsewhere. + -## Add Network Locations +## Add network locations Index NAS and network shares: - + + +### Mount network share + Ensure share mounted at OS level: + - macOS: Finder → Go → Connect to Server -- Windows: Map network drive -- Linux: Mount via fstab or GUI +- Windows: map network drive +- Linux: mount via fstab or GUI + - + + +### Add location + Browse to mounted path and add like any folder. + - + + +### Optimize for network + Adjust settings for network performance: + - Reduce parallelism - Enable caching - Increase timeouts - Shallow index initially + - + + +### Handle disconnections + Enable offline mode to prevent errors when network unavailable. + -## Monitor Indexing Progress +## Monitor indexing progress Track indexing status effectively: -### Job Manager +### Job manager + Access via status bar or `⌘J` / `Ctrl+J`: + - Current file being processed - Files completed vs total - Processing speed - Time remaining estimate - Pause/resume controls -### Location Statistics +### Location statistics + Right-click location → Statistics: + ``` Files indexed: 45,234 Total size: 127 GB @@ -252,43 +340,64 @@ Index health: 98% Thumbnails: 12,453 ``` -### System Resources +### System resources + Monitor impact during indexing: + - CPU usage (adjustable) - Disk I/O -- Memory consumption +- Memory consumption - Network bandwidth (for remote) - -Initial indexing runs at full speed. Subsequent updates throttle to avoid system impact. - + + Initial indexing runs at full speed. Subsequent updates throttle to avoid + system impact. + -## Optimize Performance +## Optimize performance -### Large Libraries (>100k files) +### Large libraries (>100k files) Handle massive collections: - + + +### Index in phases + Add subfolders separately rather than entire directory tree at once. + - + + +### Schedule indexing + Run initial index during off-hours: + ```bash sd location add /large/library --schedule "02:00" ``` + - + + +### Adjust parallelism + Reduce concurrent processing for stability: + Settings → Location → Advanced → Max Workers: 2 + - + + +### Disable previews initially + Generate thumbnails after initial index completes. + -### Slow Storage +### Slow storage Optimize for external/network drives: @@ -298,21 +407,24 @@ Optimize for external/network drives: - Use shallow indexing - Process sequentially -### System Impact +### System impact Minimize resource usage: -**CPU Management** +**CPU management** + - Set process priority to low - Limit worker threads - Enable thermal throttling **Disk I/O** + - Reduce concurrent reads -- Increase buffer sizes +- Increase buffer sizes - Schedule during idle -**Memory Usage** +**Memory usage** + - Lower thumbnail cache - Reduce preview quality - Clear cache regularly @@ -323,17 +435,20 @@ Minimize resource usage: Common causes and solutions: -**Corrupted Files** +**Corrupted files** + - Check logs for problem files - Exclude problematic paths - Run filesystem check -**Permission Issues** +**Permission issues** + - Verify read access - Run as appropriate user - Check security software -**Resource Constraints** +**Resource constraints** + - Free up disk space - Close other applications - Increase system limits @@ -353,23 +468,39 @@ Verify files indexed correctly: Fix index inconsistencies: - + + +### Stop watching + Disable location watching temporarily. + - + + +### Clear index + Right-click → Advanced → Clear Index (keeps settings). + - + + +### Re-index + Start fresh index with same settings. + - + + +### Verify results + Check file count matches filesystem. + -## Advanced Location Management +## Advanced location management -### Location Templates +### Location templates Save configurations for reuse: @@ -383,7 +514,7 @@ Photo Import Template: Apply templates when adding similar locations. -### Conditional Indexing +### Conditional indexing Index based on rules: @@ -395,39 +526,40 @@ Only index if: - Network connected ``` -### Location Hierarchies +### Location hierarchies Organize nested locations: ``` Media Library/ ├── Photos/ (Deep index) -├── RAW Files/ (Shallow index) +├── RAW Files/ (Shallow index) └── Exports/ (Watch only) ``` Each sublocation can have different settings. -### Cross-Device Locations +### Cross-device locations Share location references: - -While files stay on original device, location metadata syncs so other devices know what exists where. - + + While files stay on original device, location metadata syncs so other + devices know what exists where. + -## Best Practices +## Best practices -**Start Small**: Test with small folders before indexing everything. +**Start small**: test with small folders before indexing everything. -**Name Clearly**: Use descriptive names indicating content and device. +**Name clearly**: use descriptive names indicating content and device. -**Regular Maintenance**: Re-scan locations monthly to catch any missed changes. +**Regular maintenance**: re-scan locations monthly to catch any missed changes. -**Exclude Wisely**: Prevent indexing temporary files, caches, and system folders. +**Exclude wisely**: prevent indexing temporary files, caches, and system folders. -**Monitor Health**: Check location statistics regularly for issues. +**Monitor health**: check location statistics regularly for issues. -**Document Settings**: Note why specific settings chosen for future reference. +**Document settings**: note why specific settings chosen for future reference. -Your locations now integrate seamlessly with Spacedrive, making all your files searchable and organized while preserving your existing structure. Add locations strategically and tune settings for optimal performance. \ No newline at end of file +Your locations now integrate seamlessly with Spacedrive, making all your files searchable and organized while preserving your existing structure. Add locations strategically and tune settings for optimal performance. diff --git a/docs/overview/backup-photos-ios.mdx b/docs/content/docs/overview/backup-photos-ios.mdx similarity index 58% rename from docs/overview/backup-photos-ios.mdx rename to docs/content/docs/overview/backup-photos-ios.mdx index 982dcf66a608..885bcf0b59c6 100644 --- a/docs/overview/backup-photos-ios.mdx +++ b/docs/content/docs/overview/backup-photos-ios.mdx @@ -1,6 +1,6 @@ --- title: Back Up Photos from iPhone -sidebarTitle: Back Up Photos +description: Automatically back up your iPhone photos to any Mac, PC, or NAS you own. --- Spacedrive provides automatic photo backup from your iPhone to any Mac, PC, or NAS you own. Your photos transfer directly between devices using encrypted peer-to-peer connections, keeping them private and under your control. @@ -17,188 +17,283 @@ Before starting, ensure you have: - Spacedrive installed on both devices - Sufficient storage space on destination device - -Photo backup works completely offline after initial pairing. No internet connection required for transfers. - + + Photo backup works completely offline after initial pairing. No internet + connection required for transfers. + ## Install Spacedrive on iPhone - + + +### Download from App Store + Search for "Spacedrive" on the App Store or visit [spacedrive.com/ios](https://spacedrive.com/ios) for a direct link. + - + + +### Open the app + Launch Spacedrive and grant necessary permissions: + - **Photos access** to read your photo library - **Local network** to find and connect to your devices - **Notifications** for backup status updates (optional) + - + + +### Create mobile library + Follow the setup wizard to create your library. Choose "Mobile Library" when prompted for library type. + - -Without photo access permission, Spacedrive cannot back up your photos. iOS requires explicit permission for privacy protection. - + + Without photo access permission, Spacedrive cannot back up your photos. iOS + requires explicit permission for privacy protection. + -## Set Up Your Backup Destination +## Set up your backup destination Configure the computer where photos will be stored: - + + +### Open Spacedrive Desktop + Launch Spacedrive on your Mac, Windows, or Linux machine. Ensure the daemon is running (check status bar). + - + + +### Create backup location + Navigate to Settings → Locations and add a folder for photo backups. Name it clearly like "iPhone Photos" or "Mobile Backup". + - + + +### Enable network discovery + In Settings → Network, ensure "Device Discovery" is enabled. This allows your iPhone to find this computer. + - -Create a dedicated folder structure like `/Photos/iPhone/[Year]` for better organization. - + + Create a dedicated folder structure like `/Photos/iPhone/[Year]` for better + organization. + -## Pair Your Devices +## Pair your devices Connect your iPhone and computer for secure transfers: - + + +### Start pairing on iPhone + In Spacedrive iOS, tap the devices icon and select "Add Device". Your phone begins scanning for nearby Spacedrive instances. + - + + +### Select your computer + Choose your computer from the discovered devices list. If not visible, ensure both devices are on the same network. + - + + +### Verify security code + Confirm the 6-digit code matches on both devices. This ensures you're connecting to the right computer. + - + + +### Name the connection + Give this pairing a friendly name like "John's MacBook" for easy identification. + - -Pairing uses end-to-end encryption. Even on the same network, your photos remain private during transfer. - + + Pairing uses end-to-end encryption. Even on the same network, your photos + remain private during transfer. + -## Configure Backup Settings +## Configure backup settings Customize how and when photos back up: - + + +### Open backup settings + On iPhone, go to Settings → Photo Backup. Toggle "Enable Photo Backup" to on. + - + + +### Choose backup destination + Select your paired computer and choose the specific folder created earlier. + - + + +### Set transfer conditions + Configure when backups occur: -- **WiFi Only**: Prevents cellular data usage (recommended) -- **While Charging**: Runs backups only when plugged in -- **Background Backup**: Allows transfers when app is closed + +- **WiFi only**: prevents cellular data usage (recommended) +- **While charging**: runs backups only when plugged in +- **Background backup**: allows transfers when app is closed + - + + +### Select photo types + Choose what to back up: + - Photos (HEIC/JPEG) - Videos - Live Photos - RAW files (if shooting ProRAW) + -## Start Your First Backup +## Start your first backup - + + +### Initiate backup + Tap "Start Backup Now" for immediate transfer or wait for automatic backup based on your conditions. + - + + +### Monitor progress + View real-time progress showing: + - Photos queued - Transfer speed - Estimated time remaining - Individual file status + - + + +### Keep app active + For initial backup, keep Spacedrive open. Subsequent backups run in background. + - -First backup may take hours depending on library size. A 10,000 photo library typically transfers in 2-4 hours on WiFi. - + + First backup may take hours depending on library size. A 10,000 photo + library typically transfers in 2–4 hours on WiFi. + -## Manage Ongoing Backups +## Manage ongoing backups Spacedrive handles incremental backups intelligently: -### Automatic Detection +### Automatic detection + The app identifies new photos since last backup. Only new items transfer, saving time and bandwidth. -### Duplicate Prevention +### Duplicate prevention + Content-based deduplication ensures the same photo never backs up twice, even if edited or renamed. -### Conflict Resolution +### Conflict resolution + If you organize photos on computer, Spacedrive preserves both versions without overwriting. -### Storage Management +### Storage management + Monitor backup status and storage usage: ``` Settings → Photo Backup → Storage Backed up: 15,234 photos -Storage used: 45.2 GB +Storage used: 45.2 GB Last backup: 2 hours ago Next backup: When charging ``` -## Advanced Configuration +## Advanced configuration -### Multiple Backup Destinations +### Multiple backup destinations Back up to multiple computers for redundancy: - + + +### Pair additional devices + Repeat pairing process with other computers or NAS devices. + - + + +### Configure destinations + In Photo Backup settings, enable multiple destinations and set priorities. + - + + +### Choose sync strategy + Select between: -- **Mirror**: Same photos on all destinations -- **Distribute**: Split photos across devices -- **Archive**: Move older photos to specific devices + +- **Mirror**: same photos on all destinations +- **Distribute**: split photos across devices +- **Archive**: move older photos to specific devices + -### Network Optimization +### Network optimization Improve transfer speeds: -- **5GHz WiFi**: Use 5GHz networks when available for faster speeds -- **Direct Connection**: Create device-to-device network for maximum speed -- **Quality Settings**: Adjust compression for faster transfers (preserves originals) +- **5GHz WiFi**: use 5GHz networks when available for faster speeds +- **Direct connection**: create device-to-device network for maximum speed +- **Quality settings**: adjust compression for faster transfers (preserves originals) - -For fastest initial backup, connect devices to ethernet on same network switch. - + + For fastest initial backup, connect devices to ethernet on same network + switch. + -### Backup Rules +### Backup rules Create smart backup rules: ``` Only backup: - Photos from last 30 days -- Favorites marked in Photos app +- Favorites marked in Photos app - Photos with faces detected - Media larger than 1MB ``` @@ -206,51 +301,55 @@ Only backup: ## Troubleshooting ### iPhone can't find computer + - Verify both devices on same WiFi network - Disable VPNs temporarily - Check firewall allows Spacedrive - Restart both devices ### Backup seems stuck + - Check available storage on destination - Verify WiFi connection stable - Force quit and reopen app - Check job manager for errors ### Photos missing after backup + - Confirm backup completed (check status) - Verify correct destination folder - Check if filters excluded some photos - Look in Spacedrive's trash for accidents ### Battery draining quickly + - Enable "While Charging Only" option - Reduce backup frequency - Lower transfer quality setting - Disable background backup -## Best Practices +## Best practices -**Regular Backups**: Enable automatic daily backups while charging overnight. +**Regular backups**: enable automatic daily backups while charging overnight. -**Verify Backups**: Periodically check destination folder to confirm photos transferred correctly. +**Verify backups**: periodically check destination folder to confirm photos transferred correctly. -**Organize Early**: Set up folder structure before accumulating thousands of photos. +**Organize early**: set up folder structure before accumulating thousands of photos. -**Monitor Storage**: Keep 20% free space on destination for smooth operations. +**Monitor storage**: keep 20% free space on destination for smooth operations. -**Test Recovery**: Occasionally verify you can access backed-up photos from another device. +**Test recovery**: occasionally verify you can access backed-up photos from another device. - -Photo backup is not a sync service. Deleting photos from iPhone doesn't remove backups. Manage storage separately on each device. - + + Photo backup is not a sync service. Deleting photos from iPhone doesn't + remove backups. Manage storage separately on each device. + -## Next Steps +## Next steps With photo backup configured, explore these features: -- **[Manage Your Libraries](/guides/manage-libraries)** - Organize across devices -- **[Add and Index Locations](/guides/add-index-locations)** - Include existing photos -- **[Search and Tag Photos](/guides/search-tag)** - Organize your collection +- [Manage your libraries](/overview/manage-libraries) — organize across devices. +- [Add and index locations](/overview/add-index-locations) — include existing photos. -Your photos now back up automatically and securely to devices you control. No subscriptions, no privacy concerns, just your memories safely preserved. \ No newline at end of file +Your photos now back up automatically and securely to devices you control. No subscriptions, no privacy concerns — just your memories safely preserved. diff --git a/docs/overview/get-started.mdx b/docs/content/docs/overview/get-started.mdx similarity index 60% rename from docs/overview/get-started.mdx rename to docs/content/docs/overview/get-started.mdx index 086cfd7c50a9..57bfdfe51c00 100644 --- a/docs/overview/get-started.mdx +++ b/docs/content/docs/overview/get-started.mdx @@ -1,6 +1,6 @@ --- title: Get Started with Spacedrive -sidebarTitle: Get Started +description: Install Spacedrive, create your first library, and learn the core concepts. --- Spacedrive unifies your files across all devices into one seamless experience. This guide walks you through initial setup and core concepts to get you organizing files in minutes. @@ -12,190 +12,269 @@ You'll install Spacedrive, create your first library, and understand how the dis Spacedrive runs as a background service (daemon) with a separate user interface. This architecture enables features like offline sync and multi-device coordination. - + + +### Download the app + Visit [spacedrive.com/download](https://github.com/spacedriveapp/spacedrive/releases/tag/v2.0.0-alpha.1) and get the installer for your platform. Spacedrive (v2.0.0-alpha.1) supports macOS and Linux. Windows, iOS, and Android coming in v2.0.0-alpha.2. + - + + +### Run the installer + Follow your platform's standard installation process. On macOS, drag Spacedrive to Applications. On Windows, run the installer. Linux users can use AppImage or package managers. + - + + +### Launch Spacedrive + Open Spacedrive from your applications. The daemon starts automatically in the background. You'll see the setup wizard on first launch. + - -The daemon runs separately from the UI. Closing the app window doesn't stop file syncing or background operations. CLI users can set up auto-start with `sd daemon install` on macOS. - + + The daemon runs separately from the UI. Closing the app window doesn't stop + file syncing or background operations. CLI users can set up auto-start with + `sd daemon install` on macOS. + -## Create Your First Library +## Create your first library Libraries organize your files and settings. Think of a library as a unified view of all your data across devices. - + + +### Choose library name + Pick a descriptive name like "Personal" or "Work". You can create multiple libraries later for different purposes. + - + + +### Select storage location + Spacedrive stores library data in a local database. Choose the default location or pick a custom path with sufficient space. + - + + +### Complete setup + Click "Create Library" to initialize your database. Spacedrive creates the necessary structures for organizing your files. + Your library starts empty. Next, you'll add locations to begin organizing files. -## Understand Key Concepts +## Understand key concepts Spacedrive introduces concepts that differ from traditional file managers: ### Libraries + A library is your personal database of file information. It stores: + - File metadata and organization - Tags and custom attributes - Device relationships - Sync settings - -Libraries remain local to each device but can sync organizational data between devices you own. - + + Libraries remain local to each device but can sync organizational data + between devices you own. + ### Locations + Locations are folders on your device that Spacedrive monitors. Adding a location indexes its contents into your library without moving files. -### Virtual Filesystem (VDFS) +### Virtual filesystem (VDFS) + Spacedrive creates a unified view of files across all devices. Files keep their physical location but appear in one searchable interface. -### SD Paths +### SD paths + Universal file addresses that work across all storage types. Spacedrive uses three types of paths: -- **Physical**: Traditional filesystem paths on local devices (`/Users/jane/Documents/report.pdf`) -- **Cloud**: Cloud storage paths for services like S3, Google Drive, or Dropbox (`photos/vacation/beach.jpg`) -- **Content**: Content-addressed paths that find files by their content, regardless of location +- **Physical**: traditional filesystem paths on local devices (`/Users/jane/Documents/report.pdf`) +- **Cloud**: cloud storage paths for services like S3, Google Drive, or Dropbox (`photos/vacation/beach.jpg`) +- **Content**: content-addressed paths that find files by their content, regardless of location This unified system means you can copy files between your laptop, external drive, and cloud storage using the same commands. -## Navigate the Interface +## Navigate the interface The Spacedrive interface provides powerful tools for file management: -**Sidebar** - Switch between libraries, access locations, and view system status. Collapsible for more workspace. +**Sidebar** — switch between libraries, access locations, and view system status. Collapsible for more workspace. -**Explorer** - Browse files with list or grid views. Supports columns, sorting, and filtering like traditional file managers. +**Explorer** — browse files with list or grid views. Supports columns, sorting, and filtering like traditional file managers. -**Inspector** - View and edit file metadata. Add tags, see technical details, and manage file relationships. +**Inspector** — view and edit file metadata. Add tags, see technical details, and manage file relationships. -**Search Bar** - Find files instantly across all indexed locations. Supports filters and advanced queries. +**Search bar** — find files instantly across all indexed locations. Supports filters and advanced queries. -**Job Manager** - Monitor background operations like indexing and sync. Access via the status indicator. +**Job manager** — monitor background operations like indexing and sync. Access via the status indicator. - -Use keyboard shortcuts for efficiency. Press `⌘/` (Mac) or `Ctrl+/` (Windows/Linux) to see all available shortcuts. - + + Use keyboard shortcuts for efficiency. Press `⌘/` (Mac) or `Ctrl+/` + (Windows/Linux) to see all available shortcuts. + -## Add Your First Location +## Add your first location Connect a folder to start organizing files: - + + +### Open location settings + Click the + button next to "Locations" in the sidebar or go to Settings → Locations. + - + + +### Select a folder + Choose a folder to index. Start with something manageable like Documents or Pictures. + - + + +### Configure indexing + Select indexing depth: -- **Shallow**: File names and basic metadata only (fastest) -- **Deep**: Full content analysis including thumbnails (recommended) + +- **Shallow**: file names and basic metadata only (fastest) +- **Deep**: full content analysis including thumbnails (recommended) + - + + +### Start indexing + Click "Add Location" to begin. Monitor progress in the job manager (status bar). + - -Initial indexing time depends on folder size and depth selected. A folder with 10,000 files typically takes 2-5 minutes for deep indexing. - + + Initial indexing time depends on folder size and depth selected. A folder + with 10,000 files typically takes 2–5 minutes for deep indexing. + -## Organize with Tags +## Organize with tags Tags provide flexible organization without moving files: - + + +### Create a tag + Right-click any file and select "Add Tag" or use Settings → Tags to create tags first. + - + + +### Apply tags + Select multiple files and apply tags in bulk. Tags sync across devices. + - + + +### Filter by tags + Click tags in the sidebar to filter your view instantly. + Tags support colors and hierarchies for advanced organization. -## Search Across Everything +## Search across everything Spacedrive's search works across all indexed locations simultaneously: ``` photo # Find all photos -tag:vacation # Files tagged "vacation" -size:>10MB # Large files -modified:<7d # Changed in last week +tag:vacation # Files tagged "vacation" +size:>10MB # Large files +modified:<7d # Changed in last week ``` - -Search runs locally for privacy. No file data leaves your devices. - + + Search runs locally for privacy. No file data leaves your devices. + -## Connect Another Device +## Connect another device Expand Spacedrive to multiple devices: - + + +### Install on second device + Download and install Spacedrive on another computer or mobile device. + - + + +### Sign in + Use the same account to link devices (coming soon) or set up peer-to-peer pairing. + - + + +### Enable sync + Choose which organizational data to sync. File content remains on original devices. + Connected devices share tags, comments, and organization while keeping files in place. -## Next Steps +## Next steps You now have Spacedrive running with your first library and location. Explore these features: -- **[Back Up Photos from iPhone](/guides/backup-photos-ios)** - Automatic photo backup -- **[Manage Your Libraries](/guides/manage-libraries)** - Multiple library workflows -- **[Add and Index Locations](/guides/add-index-locations)** - Advanced indexing options +- [Back up photos from iPhone](/overview/backup-photos-ios) — automatic photo backup. +- [Manage your libraries](/overview/manage-libraries) — multiple library workflows. +- [Add and index locations](/overview/add-index-locations) — advanced indexing options. - -Join our [Discord community](https://discord.gg/spacedrive) for help, tips, and updates on new features. - + + Join our [Discord community](https://discord.gg/gTaF2Z44f5) for help, tips, + and updates on new features. + ## Troubleshooting ### Spacedrive won't start + - Check if the daemon is running in your system processes - Restart your computer to clear any stuck processes - Reinstall if issues persist ### Indexing seems slow + - Start with shallow indexing for large folders - Ensure your disk isn't full - Check job manager for detailed progress ### Can't see files + - Verify the location was added successfully - Check if indexing completed in job manager - Ensure you're viewing the correct library -Need more help? Visit our [troubleshooting guide](/troubleshooting) or ask in Discord. \ No newline at end of file +Need more help? Ask in [Discord](https://discord.gg/gTaF2Z44f5). diff --git a/docs/overview/history.mdx b/docs/content/docs/overview/history.mdx similarity index 51% rename from docs/overview/history.mdx rename to docs/content/docs/overview/history.mdx index be70ed926192..a85513fd1ceb 100644 --- a/docs/overview/history.mdx +++ b/docs/content/docs/overview/history.mdx @@ -1,27 +1,13 @@ --- -title: Spacedrive - A Historical Chronicle -sidebarTitle: History +title: Spacedrive — A Historical Chronicle +description: The project's evolution from personal prototype through V1 to the V2 rewrite. --- -## Table of Contents - -1. [Introduction](#introduction) -2. [Origins (2021-2022)](#origins-2021-2022) -3. [Open Source Launch (May 2022)](#open-source-launch-may-2022) -4. [Seed Funding (2022-2023)](#seed-funding-2022-2023) -5. [Technical Architecture V1](#technical-architecture-v1) -6. [Community Growth](#community-growth) -7. [Why V1 Failed](#why-v1-failed) -8. [V2 Rewrite (2025)](#v2-rewrite-2025) -9. [Development Approach](#development-approach) -10. [Lessons Learned](#lessons-learned) -11. [Future Direction](#future-direction) - ## Introduction Spacedrive is a cross-platform file manager built to unify file access across devices and cloud services. This document tracks the project's evolution from initial development through its V1 challenges and V2 rewrite. -## Origins (2021-2022) +## Origins (2021–2022) Started in early 2021 as a personal project to solve file fragmentation across devices and cloud services. Core concept: a Virtual Distributed File System (VDFS) that would make device boundaries transparent. @@ -32,19 +18,19 @@ Initial goals: - Cross-platform native apps - No vendor lock-in -## Open Source Launch (May 2022) +## Open source launch (May 2022) Released on GitHub in May 2022. Reached #1 on GitHub Trending for 3 days, accumulated 10,000+ stars in the first week. The reception indicated the problem resonated with developers managing files across multiple devices and cloud services. -## Seed Funding (2022-2023) +## Seed funding (2022–2023) Raised $2M seed round (June 2022) led by OSS Capital. Investors included Naval Ravikant, Guillermo Rauch, Tobias Lütke, Tom Preston-Werner, and others. Team grew to ~12 people working remotely. 100+ open source contributors participated. -## Technical Architecture V1 +## Technical architecture V1 Built on what we called the "PRRTT stack": @@ -63,7 +49,7 @@ Key features shipped: By early 2025: 35,000 GitHub stars, 600,000+ installations. -## Community Growth +## Community growth - 35,000+ GitHub stars by 2025 - 1,100+ forks @@ -71,108 +57,108 @@ By early 2025: 35,000 GitHub stars, 600,000+ installations. - Active Discord community - Translations in 11 languages -## Why V1 Failed +## Why V1 failed -By early 2025, V1 became unmaintainable. The decision to rewrite wasn't driven by perfectionism—the codebase had reached a state where shipping features or fixing bugs was no longer viable. +By early 2025, V1 became unmaintainable. The decision to rewrite wasn't driven by perfectionism — the codebase had reached a state where shipping features or fixing bugs was no longer viable. -### Infrastructure Collapse +### Infrastructure collapse -**Prisma deprecation**: The Rust client engine was deprecated, leaving the project on an unmaintained fork with no migration path. Every database interaction depended on infrastructure we could no longer update or fix. +**Prisma deprecation**: the Rust client engine was deprecated, leaving the project on an unmaintained fork with no migration path. Every database interaction depended on infrastructure we could no longer update or fix. -**libp2p reliability**: P2P networking was fundamentally broken. File transfers would hang, connections would drop, and debugging was nearly impossible. The networking layer—core to the product vision—couldn't be trusted. +**libp2p reliability**: P2P networking was fundamentally broken. File transfers would hang, connections would drop, and debugging was nearly impossible. The networking layer — core to the product vision — couldn't be trusted. -**Dependency maintenance**: The team built `prisma-client-rust` and `rspc` out of necessity, then couldn't maintain them. These weren't side projects, they were critical infrastructure that became technical debt. +**Dependency maintenance**: the team built `prisma-client-rust` and `rspc` out of necessity, then couldn't maintain them. These weren't side projects, they were critical infrastructure that became technical debt. -### Product Limitations +### Product limitations -**No SDK**: The community couldn't extend functionality. In a file manager where everyone has unique workflows, this was fatal. Users wanted custom integrations but had no way to build them. +**No SDK**: the community couldn't extend functionality. In a file manager where everyone has unique workflows, this was fatal. Users wanted custom integrations but had no way to build them. -**No CLI**: Automated workflows and scripting were impossible. Power users—the core audience—couldn't integrate Spacedrive into their existing tooling. +**No CLI**: automated workflows and scripting were impossible. Power users — the core audience — couldn't integrate Spacedrive into their existing tooling. -**Incomplete search**: Despite positioning as a search-focused tool, only basic SQL `LIKE` queries worked. No full-text indexing, no content search, no semantic search. The core value proposition was half-built. +**Incomplete search**: despite positioning as a search-focused tool, only basic SQL `LIKE` queries worked. No full-text indexing, no content search, no semantic search. The core value proposition was half-built. -### Development Velocity Collapse +### Development velocity collapse -**No integration tests**: Rapid prototyping without full integration testing meant every release broke something. Manual testing couldn't scale, and regressions were constant. +**No integration tests**: rapid prototyping without full integration testing meant every release broke something. Manual testing couldn't scale, and regressions were constant. -**Job system boilerplate**: Adding any file operation required 500-1000+ lines of boilerplate. Simple features took weeks to implement, making iteration impossible. +**Job system boilerplate**: adding any file operation required 500–1000+ lines of boilerplate. Simple features took weeks to implement, making iteration impossible. -**Poor modularity**: Refactors that developers knew were necessary couldn't be completed. The interconnected layers meant touching one thing broke three others. Technical debt compounded faster than it could be paid down. +**Poor modularity**: refactors that developers knew were necessary couldn't be completed. The interconnected layers meant touching one thing broke three others. Technical debt compounded faster than it could be paid down. -**Incomplete migrations**: The codebase had `old_*` files still in active use alongside new systems. Every migration stalled partway through, leaving the codebase in a perpetual transition state. +**Incomplete migrations**: the codebase had `old_*` files still in active use alongside new systems. Every migration stalled partway through, leaving the codebase in a perpetual transition state. -### Architecture Dead Ends +### Architecture dead ends -**Dual file systems**: Two incompatible systems (indexed database files vs. ephemeral filesystem access) made basic operations like copying between locations impossible. Every file operation needed two implementations, and they'd drift out of sync. +**Dual file systems**: two incompatible systems (indexed database files vs. ephemeral filesystem access) made basic operations like copying between locations impossible. Every file operation needed two implementations, and they'd drift out of sync. -**Sync confusion**: Complex CRDTs built on top of Prisma with unclear boundaries between local and shared data. The team couldn't decide what should sync and what shouldn't, so the implementation was never finished. +**Sync confusion**: complex CRDTs built on top of Prisma with unclear boundaries between local and shared data. The team couldn't decide what should sync and what shouldn't, so the implementation was never finished. -**Query invalidation**: Backend code contained hardcoded frontend cache keys (`invalidate_query!(library, "search.paths")`), creating tight coupling that made refactoring impossible. +**Query invalidation**: backend code contained hardcoded frontend cache keys (`invalidate_query!(library, "search.paths")`), creating tight coupling that made refactoring impossible. -### Business Model Misalignment +### Business model misalignment -**Cloud focus**: Development prioritized a cloud revenue model with high overhead, low margins, and questionable alignment with the core product. Resources went toward infrastructure that didn't serve users or validate product-market fit. (V2 pivots to premium extensions following the COSS model.) +**Cloud focus**: development prioritized a cloud revenue model with high overhead, low margins, and questionable alignment with the core product. Resources went toward infrastructure that didn't serve users or validate product-market fit. (V2 pivots to premium extensions following the COSS model.) -### Process Breakdown +### Process breakdown -**Slow release cycles**: Inefficient project management meant features took months to ship. Community momentum stalled. +**Slow release cycles**: inefficient project management meant features took months to ship. Community momentum stalled. -**Delayed refactors**: Developers knew what needed to be fixed, but priorities kept shifting. Technical debt accumulated while the team chased features. +**Delayed refactors**: developers knew what needed to be fixed, but priorities kept shifting. Technical debt accumulated while the team chased features. -**No AI tooling**: The complexity required to work across the stack didn't have the AI assistance available today. What would now take hours took weeks. +**No AI tooling**: the complexity required to work across the stack didn't have the AI assistance available today. What would now take hours took weeks. -### V1 as Necessary R&D +### V1 as necessary R&D -V1's apparent failure was actually essential research. The team pioneered a cross-platform file manager with P2P networking and content-addressable storage—problems with no existing solutions. Every architectural mistake taught us what not to do. Every incomplete migration revealed where boundaries should exist. Every performance bottleneck showed us where to optimize. +V1's apparent failure was actually essential research. The team pioneered a cross-platform file manager with P2P networking and content-addressable storage — problems with no existing solutions. Every architectural mistake taught us what not to do. Every incomplete migration revealed where boundaries should exist. Every performance bottleneck showed us where to optimize. -Without V1, we'd have no idea how to build V2. The technical achievements—SQLite-backed indexing, cross-platform support, content-addressable storage—proved the core concepts worked. The failures showed us how to structure them properly. +Without V1, we'd have no idea how to build V2. The technical achievements — SQLite-backed indexing, cross-platform support, content-addressable storage — proved the core concepts worked. The failures showed us how to structure them properly. The rewrite wasn't a failure of the team. It was the natural outcome of exploring genuinely hard problems without a map. -## V2 Rewrite (2025) +## V2 rewrite (2025) V2 has been rebuilt by the founder using AI-accelerated development, systematically addressing every failure from V1. Each architectural decision directly solves a specific problem that made V1 unmaintainable. -### Infrastructure: No More Deprecated Dependencies +### Infrastructure: no more deprecated dependencies **Problem**: Prisma Rust deprecated, libp2p unreliable, custom infrastructure becoming debt. **Solution**: -- Replaced Prisma with **SeaORM** - actively maintained ORM -- Replaced libp2p with **Iroh** - purpose-built for reliable P2P with QUIC transport and NAT traversal that actually works +- Replaced Prisma with **SeaORM** — actively maintained ORM +- Replaced libp2p with **Iroh** — purpose-built for reliable P2P with QUIC transport and NAT traversal that actually works - Moved away from custom infrastructure where possible -### Product: SDK, CLI, and Real Search +### Product: SDK, CLI, and real search -**Problem**: No SDK meant community couldn't extend. No CLI meant no automation. Search was SQL `LIKE` queries. +**Problem**: no SDK meant community couldn't extend. No CLI meant no automation. Search was SQL `LIKE` queries. **Solution**: -- **WASM Extension System** with complete Rust SDK - extensions are first-class, sandboxed, and can define their own data models +- **WASM extension system** with complete Rust SDK — extensions are first-class, sandboxed, and can define their own data models - **Production CLI** (`sd-cli`) for library management, indexing, and automation -- **FTS5 full-text search** with semantic re-ranking - sub-100ms queries on 1M+ files +- **FTS5 full-text search** with semantic re-ranking — sub-100ms queries on 1M+ files Launch extensions (Chronicle for research, Cipher for passwords, Atlas for CRM, Ledger for finances) prove the SDK works. -### Development Velocity: Eliminated Boilerplate +### Development velocity: eliminated boilerplate -**Problem**: 500-1000+ lines of boilerplate per file operation. No integration tests. Poor modularity made refactors impossible. +**Problem**: 500–1000+ lines of boilerplate per file operation. No integration tests. Poor modularity made refactors impossible. **Solution**: -- **Procedural macros** for job system - reduced to ~50 lines per job (95% reduction) -- **Entry-centric model** - files and directories unified, eliminating dual implementations -- **Integration test suite** - every major operation has tests that run against real SQLite -- **Clear domain boundaries** - each module (location, library, volume, sync) is independent +- **Procedural macros** for job system — reduced to ~50 lines per job (95% reduction) +- **Entry-centric model** — files and directories unified, eliminating dual implementations +- **Integration test suite** — every major operation has tests that run against real SQLite +- **Clear domain boundaries** — each module (location, library, volume, sync) is independent Development speed increased 10x. Adding new file operations went from weeks to hours. -### Architecture: Solved the Dual File System Problem +### Architecture: solved the dual file system problem -**Problem**: Indexed files vs. ephemeral files created two incompatible worlds. Copying between them was impossible. +**Problem**: indexed files vs. ephemeral files created two incompatible worlds. Copying between them was impossible. -**Solution**: **Universal `SdPath` addressing** +**Solution**: **universal `SdPath` addressing** ```rust pub enum SdPath { @@ -182,24 +168,24 @@ pub enum SdPath { } ``` -Every file in Spacedrive—indexed or not, local, cloud, or remote—has a single address. Operations work uniformly across all storage. Device and cloud boundaries disappeared. +Every file in Spacedrive — indexed or not, local, cloud, or remote — has a single address. Operations work uniformly across all storage. Device and cloud boundaries disappeared. -### Sync: Domain Separation That Works +### Sync: domain separation that works -**Problem**: Custom CRDT implementation never finished. Unclear boundaries between local and shared data. +**Problem**: custom CRDT implementation never finished. Unclear boundaries between local and shared data. -**Solution**: **Domain-separated sync with HLC timestamps** +**Solution**: **domain-separated sync with HLC timestamps** Instead of trying to sync everything, V2 defines clear boundaries: -- **Shared**: Library metadata, tags, device list -- **Local-only**: Volume mounts, locations, indexing state +- **Shared**: library metadata, tags, device list +- **Local-only**: volume mounts, locations, indexing state Leaderless P2P synchronization using Hybrid Logical Clocks. No coordination required, no consensus algorithms. It just works offline. -### Coupling: Event-Driven Architecture +### Coupling: event-driven architecture -**Problem**: Backend code had hardcoded frontend cache keys (`invalidate_query!`). Tight coupling made changes impossible. +**Problem**: backend code had hardcoded frontend cache keys (`invalidate_query!`). Tight coupling made changes impossible. **Solution**: **EventBus with domain events** @@ -207,11 +193,11 @@ Components publish events (`EntryCreated`, `LocationIndexed`, `SyncCompleted`). This eliminated cross-layer coupling entirely. -### Business Model: COSS Over Cloud +### Business model: COSS over cloud -**Problem**: Cloud revenue model with high overhead, low margins, misaligned with product. +**Problem**: cloud revenue model with high overhead, low margins, misaligned with product. -**Solution**: **Commercial Open Source Software (COSS) model** +**Solution**: **commercial open source software (COSS) model** - Core file manager: free forever - Premium extensions (Photos, Chronicle, Guardian, Studio): paid @@ -220,11 +206,11 @@ This eliminated cross-layer coupling entirely. Revenue aligns with value delivered. Resources go toward features users want, not cloud infrastructure. -### Actions: Preview Before Commit +### Actions: preview before commit -**Problem**: File operations were destructive and unpredictable. +**Problem**: file operations were destructive and unpredictable. -**Solution**: **Transactional action system** +**Solution**: **transactional action system** Every destructive operation follows preview-commit-verify flow: @@ -235,9 +221,9 @@ Every destructive operation follows preview-commit-verify flow: Conflicts are caught before execution. Users always know what will happen. -### Performance: Production-Ready Metrics +### Performance: production-ready metrics -V2 isn't just architecturally sound—it's fast: +V2 isn't just architecturally sound — it's fast: - **8,500 files/second** indexing (multi-phase resumable pipeline) - **~55ms search** on 1M entries (FTS5 with semantic re-ranking) @@ -246,30 +232,30 @@ V2 isn't just architecturally sound—it's fast: Enterprise capabilities running on consumer hardware. -## Development Approach +## Development approach -Spacedrive’s rewrite from scratch, despite limited resources, was made feasible by advancements in AI-driven code generation for Rust in 2025. Tools like Claude 3.5 Opus, GPT-5, and Gemini’s expansive context window enabled translating detailed specifications into robust implementations across the entire codebase. The workflow centered on iteratively refining specs and test cases, ensuring implementations adhered to strict code style guidelines and passed all tests. This AI-accelerated, spec-first, test-driven approach enabled rapid development with a single-person team. +Spacedrive's rewrite from scratch, despite limited resources, was made feasible by advancements in AI-driven code generation for Rust in 2025. Tools like Claude 3.5 Opus, GPT-5, and Gemini's expansive context window enabled translating detailed specifications into robust implementations across the entire codebase. The workflow centered on iteratively refining specs and test cases, ensuring implementations adhered to strict code style guidelines and passed all tests. This AI-accelerated, spec-first, test-driven approach enabled rapid development with a single-person team. **Why this worked:** -The codebase prioritizes simplicity and structure over complexity. Domains—library, location, volume, sync, and networking are organized into isolated modules with well-defined boundaries. Documentation supports every layer, inline comments detail execution, and integration tests validate behavior. +The codebase prioritizes simplicity and structure over complexity. Domains — library, location, volume, sync, and networking — are organized into isolated modules with well-defined boundaries. Documentation supports every layer, inline comments detail execution, and integration tests validate behavior. The original team established core concepts: file system abstractions, job system architecture, and sync protocol design. This rewrite leveraged these foundations but swapped much of the custom infrastructure for reliable, community-maintained tools. -## Lessons Learned +## Lessons learned -1. **User experience drives architecture** - Implementation details shouldn't leak into UX (dual file systems) -2. **Ship simple first** - Basic solutions are better than perfect ones that never ship -3. **Maintain what you create** - Don't create infrastructure you can't support long-term -4. **Reduce boilerplate** - Developer friction directly impacts iteration speed -5. **Deliver on core promises** - Search was a key value prop but remained weak -6. **One concept, one implementation** - Multiple representations of the same thing create confusion -7. **Complete migrations** - Don't start new ones until old systems are removed -8. **Loose coupling** - Event-driven beats direct dependencies -9. **Decide and move** - Imperfect decisions beat analysis paralysis -10. **Maintain momentum** - Regular releases keep community engaged +1. **User experience drives architecture** — implementation details shouldn't leak into UX (dual file systems). +2. **Ship simple first** — basic solutions are better than perfect ones that never ship. +3. **Maintain what you create** — don't create infrastructure you can't support long-term. +4. **Reduce boilerplate** — developer friction directly impacts iteration speed. +5. **Deliver on core promises** — search was a key value prop but remained weak. +6. **One concept, one implementation** — multiple representations of the same thing create confusion. +7. **Complete migrations** — don't start new ones until old systems are removed. +8. **Loose coupling** — event-driven beats direct dependencies. +9. **Decide and move** — imperfect decisions beat analysis paralysis. +10. **Maintain momentum** — regular releases keep community engaged. -## Future Direction +## Future direction Near-term: @@ -290,4 +276,4 @@ The core principle remains: files shouldn't be stuck in device ecosystems. Open --- -_Development continues at [github.com/spacedriveapp/spacedrive](https://github.com/spacedriveapp/spacedrive)_ +_Development continues at [github.com/spacedriveapp/spacedrive](https://github.com/spacedriveapp/spacedrive)._ diff --git a/docs/overview/introduction.mdx b/docs/content/docs/overview/introduction.mdx similarity index 63% rename from docs/overview/introduction.mdx rename to docs/content/docs/overview/introduction.mdx index b77530352fc3..c97d976a2ec9 100644 --- a/docs/overview/introduction.mdx +++ b/docs/content/docs/overview/introduction.mdx @@ -1,27 +1,22 @@ --- title: Welcome to Spacedrive -description: Infrastructure for multi-device computing -sidebarTitle: Introduction +description: Infrastructure for multi-device computing. --- - -**v2.0.0-alpha.1 Released: December 26, 2025** - -The complete ground-up rewrite is here. This is an alpha release for testing and feedback. macOS and Linux are supported now. Windows support coming in alpha.2. - -Read the [full release notes](https://github.com/spacedriveapp/spacedrive/releases) for more details on the rewrite. - + + **v2.0.0-alpha.1 released December 26, 2025.** The complete ground-up rewrite + is here. This is an alpha for testing and feedback. macOS and Linux are + supported now; Windows lands in alpha.2. See the [full release + notes](https://github.com/spacedriveapp/spacedrive/releases) for details. ## What is Spacedrive? Spacedrive is a local-first file manager that unifies data across devices without centralized cloud services. Files stay where they are. Spacedrive creates a content-aware index that makes them searchable, syncable, and manageable from anywhere. - - Spacedrive column view file explorer - +Spacedrive column view file explorer -## Why Spacedrive Exists +## Why Spacedrive exists Computing was designed for single devices. File managers like Finder and Explorer assume your data lives on the computer in front of you. The shift to multi-device forced us into cloud ecosystems. Convenience required giving up data ownership. @@ -29,18 +24,18 @@ AI is accelerating this trend. Cloud services offer semantic search and intellig This is infrastructure for the next era of computing. -## Current State +## Current state -### What Works Now (v2.0.0-alpha.1) +### What works now (v2.0.0-alpha.1) -**Core Infrastructure** +**Core infrastructure** - Daemon-client architecture with persistent background operations - Tauri desktop app with working interface - CLI with full library, location, job, and network management - Resumable job system with checkpointing and progress tracking -**Multi-Device Sync** +**Multi-device sync** - Peer-to-peer library sync via Iroh/QUIC with NAT traversal - Device pairing with BIP39 mnemonic codes @@ -49,7 +44,7 @@ This is infrastructure for the next era of computing. - Device-owned data (locations, files) and shared resources (tags, collections) - Incremental sync with watermarks for offline periods -**File Management** +**File management** - File indexing with shallow, content, and deep modes - Content identity and deduplication across devices @@ -57,14 +52,14 @@ This is infrastructure for the next era of computing. - Copy/move operations with atomic and streaming strategies - Checksum verification and progress tracking -**Cloud Storage** +**Cloud storage** - Integration via OpenDAL (40+ services supported) - S3, Google Drive, OneDrive, Dropbox, Azure Blob, GCS - Cloud files indexed and searchable like local files - Cross-storage operations (local to cloud, cloud to cloud) -**Media & Analysis** +**Media & analysis** - Thumbnail generation - OCR text extraction @@ -83,14 +78,14 @@ This is infrastructure for the next era of computing. - XChaCha20-Poly1305 credential encryption - OS keyring integration (Keychain, Credential Manager, Secret Service) -**Platform Support** +**Platform support** -- macOS: Fully supported -- Linux: Fully supported -- Windows: Coming in alpha.2 -- iOS & Android: Coming soon +- macOS: fully supported +- Linux: fully supported +- Windows: coming in alpha.2 +- iOS & Android: coming soon -### Coming Soon +### Coming soon - **alpha.2**: Windows support - **Future releases**: @@ -100,11 +95,11 @@ This is infrastructure for the next era of computing. - Extension SDK (stable API) - AI integration with local models -### Prototypes (Not Public) +### Prototypes (not public) Native Swift apps (iOS, macOS) and a GPUI media viewer exist as private prototypes. These demonstrate type-safe native integration and platform-specific optimizations. They may become production apps if performance demands it. -## How It Works +## How it works Spacedrive treats files as first-class objects with content identity. A photo on your laptop and the same photo on your NAS are recognized as one piece of content. @@ -118,7 +113,7 @@ Spacedrive treats files as first-class objects with content identity. A photo on Files stay in place. Spacedrive makes them universally addressable with rich metadata. -## Testing & Feedback +## Testing & feedback This is an alpha release. We need community help validating: @@ -127,38 +122,37 @@ This is an alpha release. We need community help validating: - Multi-device sync scenarios - Performance at scale - - macOS and Linux users: Test indexing, multi-device sync, and cloud + + macOS and Linux users: test indexing, multi-device sync, and cloud integration. Report bugs on GitHub or Discord. All feedback helps improve stability for the beta release. - + -## For Developers +## For developers - - **Most architecture docs are in the Developer section.** The core technical - documentation lives under [Architecture](/core/architecture), not in overview - guides. - + + Most architecture docs live under [Architecture](/core/architecture), not in + the overview guides. + Key technical docs: -- [Architecture Overview](/core/architecture) - System design and principles -- [Data Model](/core/data-model) - How data is structured -- [Content Addressing](/core/addressing) - Content identity system -- [Library Sync](/core/library-sync) - Multi-device synchronization -- [Testing Guide](/core/testing) - Test framework and patterns +- [Architecture overview](/core/architecture) — system design and principles +- [Data model](/core/data-model) — how data is structured +- [Content addressing](/core/addressing) — content identity system +- [Library sync](/core/library-sync) — multi-device synchronization +- [Testing guide](/core/testing) — test framework and patterns ## Roadmap The `.tasks` folder in the repo tracks core development work. Check the [releases page](https://github.com/spacedriveapp/spacedrive/releases) for the public roadmap. These docs provide detailed technical reference. - + Some guides document planned features that aren't in alpha.1 yet. When features aren't ready, you'll see callouts indicating their status. - + -## Project Timeline +## Project timeline Spacedrive v2 development began in June 2025. This is a ground-up rewrite addressing lessons from the v1 alpha. The architecture is designed for long-term sustainability, not quick feature delivery. @@ -166,42 +160,26 @@ v2.0.0-alpha.1 released December 26, 2025 after six months of development. This ## Screenshots -
- - - Column view - File explorer interface - +Column view — file explorer interface - - Video player - +Video player - - Grid view - Browse files in a grid layout - +Grid view — browse files in a grid layout - - Media view - Gallery and media browsing - +Media view — gallery and media browsing - - Size view - Visualize storage usage - +Size view — visualize storage usage - - 3D Gaussian Splat viewer - +3D Gaussian Splat viewer -
+## Get involved -## Get Involved +- **Test the code** — build from source, report bugs, validate cross-platform. +- **Join Discord** — [discord.gg/gTaF2Z44f5](https://discord.gg/gTaF2Z44f5). +- **Read the whitepaper** — [whitepaper/spacedrive.pdf](https://github.com/spacedriveapp/spacedrive/blob/main/whitepaper/spacedrive.pdf) (work in progress). +- **Explore architecture docs** — start with [core/architecture](/core/architecture). -- **Test the code** - Build from source, report bugs, validate cross-platform -- **Join Discord** - [discord.gg/gTaF2Z44f5](https://discord.gg/gTaF2Z44f5) -- **Read the whitepaper** - [whitepaper/spacedrive.pdf](https://github.com/spacedriveapp/spacedrive/blob/main/whitepaper/spacedrive.pdf) (work in progress) -- **Explore architecture docs** - Start with [core/architecture](/core/architecture) - - + This is a living document. As development progresses through alpha and beta releases, this page will be updated to reflect current status. - + diff --git a/docs/overview/manage-libraries.mdx b/docs/content/docs/overview/manage-libraries.mdx similarity index 55% rename from docs/overview/manage-libraries.mdx rename to docs/content/docs/overview/manage-libraries.mdx index 62b9a6e69ec3..29cd240c573d 100644 --- a/docs/overview/manage-libraries.mdx +++ b/docs/content/docs/overview/manage-libraries.mdx @@ -1,298 +1,418 @@ --- -title: Manage Your Libraries -sidebarTitle: Manage Libraries +title: Manage Your Libraries +description: Create, switch, sync, and back up libraries across devices. --- Libraries in Spacedrive organize your digital life into separate, manageable collections. Create libraries for different purposes, sync settings between devices, and maintain clean separation between personal and work files. This guide explains library management from basic operations to advanced multi-library workflows. -## Understand Libraries +## Understand libraries A library is your personal file database containing: + - Indexed file metadata and thumbnails -- Tags, comments, and custom organization +- Tags, comments, and custom organization - Location mappings and folder relationships - Device pairings and sync settings - Job history and statistics - -Libraries exist independently. Files in one library remain completely separate from others, ensuring privacy and organization. - + + Libraries exist independently. Files in one library remain completely + separate from others, ensuring privacy and organization. + -## Create Additional Libraries +## Create additional libraries Beyond your first library, create specialized collections: - + + +### Open library switcher + Click the library name in the top bar or press `⌘L` (Mac) / `Ctrl+L` (Windows/Linux). + - + + +### Select create library + Choose "Create New Library" from the dropdown menu. + - + + +### Configure library + Name your library and optionally: + - Set custom storage location - Choose encryption settings - Configure default indexing rules + - + + +### Initialize library + Click "Create" to build the database structure. Spacedrive switches to the new library automatically. + - -Each library maintains its own database file. Default location: `~/Library/Application Support/spacedrive/libraries/` on macOS. - + + Each library maintains its own database file. Default location: `~/Library/Application Support/spacedrive/libraries/` on macOS. + -## Switch Between Libraries +## Switch between libraries Navigate multiple libraries efficiently: -### Quick Switch +### Quick switch + Press `⌘L` / `Ctrl+L` to open the library switcher. Recent libraries appear at top. -### Keyboard Navigation -Use number keys `1-9` to switch to libraries by position in the list. +### Keyboard navigation + +Use number keys `1–9` to switch to libraries by position in the list. + +### Set default library -### Set Default Library Right-click a library in the switcher and select "Set as Default". This library opens on app launch. - -Pin frequently used libraries to the top of the switcher for faster access. - + + Pin frequently used libraries to the top of the switcher for faster access. + -## Library Settings +## Library settings Customize each library independently: - + + +### Open library settings + Navigate to Settings → Library or press `⌘,` / `Ctrl+,` then select Library tab. + - + + +### General settings + Configure basic options: -- **Name**: Update library display name -- **Description**: Add notes about library purpose -- **Storage Path**: View database location -- **Statistics**: See total files, size, and devices + +- **Name**: update library display name +- **Description**: add notes about library purpose +- **Storage path**: view database location +- **Statistics**: see total files, size, and devices + - + + +### Privacy settings + Control library privacy: -- **Encryption**: Enable database encryption -- **Network Visibility**: Hide from network discovery -- **Sync Permissions**: Limit which devices can sync + +- **Encryption**: enable database encryption +- **Network visibility**: hide from network discovery +- **Sync permissions**: limit which devices can sync + - + + +### Advanced options + Fine-tune behavior: -- **Indexing Rules**: Default settings for new locations -- **Thumbnail Quality**: Balance quality vs storage -- **Job Concurrency**: Parallel processing limits + +- **Indexing rules**: default settings for new locations +- **Thumbnail quality**: balance quality vs storage +- **Job concurrency**: parallel processing limits + -## Common Library Patterns +## Common library patterns -### Work/Personal Separation +### Work/personal separation Create distinct libraries for different aspects of life: -**Work Library** +**Work library** + - Project files and documents -- Client assets +- Client assets - Meeting recordings - Professional photos -**Personal Library** +**Personal library** + - Family photos and videos - Personal documents - Entertainment media - Hobby projects - -Libraries cannot share files directly. Use separate locations if you need files accessible from multiple libraries. - + + Libraries cannot share files directly. Use separate locations if you need + files accessible from multiple libraries. + -### Project-Based Libraries +### Project-based libraries Organize major projects independently: ``` Film Project Library ├── Raw Footage (Location) -├── Audio Files (Location) +├── Audio Files (Location) ├── Project Files (Location) └── Exports (Location) ``` Benefits: + - Archive entire project when complete - Share library with collaborators - Prevent accidental cross-contamination -### Device-Specific Libraries +### Device-specific libraries Optimize for different device types: -**Mobile Library** - Lightweight for phones/tablets +**Mobile library** — lightweight for phones/tablets + - Photos and screenshots only - Minimal processing rules - Reduced thumbnail sizes -**Desktop Library** - Full features for computers +**Desktop library** — full features for computers + - All file types indexed -- Maximum quality thumbnails +- Maximum quality thumbnails - Advanced processing enabled -## Sync Libraries Between Devices +## Sync libraries between devices Share organizational data across your devices: - + + +### Enable library sync + In Library Settings, toggle "Enable Sync" and note the Library ID displayed. + - + + +### Add library on second device + On another device, choose "Join Existing Library" and enter the Library ID. + - + + +### Verify connection + Confirm device pairing using the security code shown on both screens. + - + + +### Configure sync options + Choose what syncs: -- **Metadata Only**: Tags, comments (default) -- **Organization**: Folder structures -- **Settings**: Library preferences -- **Locations**: Folder mappings (advanced) + +- **Metadata only**: tags, comments (default) +- **Organization**: folder structures +- **Settings**: library preferences +- **Locations**: folder mappings (advanced) + - -Sync only shares organizational data. Actual files remain on their original devices unless explicitly transferred. - + + Sync only shares organizational data. Actual files remain on their original + devices unless explicitly transferred. + -### Sync Behavior +### Sync behavior Understanding sync helps avoid confusion: -**Immediate Sync** +**Immediate sync** + - Tags and labels - File relationships - Custom metadata -**Device-Specific** +**Device-specific** + - Location paths - Indexing progress - Local preferences -**Never Synced** +**Never synced** + - Actual file contents - Temporary data - Job history -## Back Up Libraries +## Back up libraries Protect your organizational work: - + + +### Locate library database + Find your library file: + - macOS: `~/Library/Application Support/spacedrive/libraries/[id]/library.db` - Windows: `%APPDATA%\spacedrive\libraries\[id]\library.db` - Linux: `~/.config/spacedrive/libraries/[id]/library.db` + - + + +### Create backup + Copy the entire library folder to backup location. Include all `.db` files and thumbnails directory. + - + + +### Automate backups + Set up automated backups using: + - Time Machine (macOS) -- File History (Windows) +- File History (Windows) - rsync scripts (Linux) - Cloud backup services + - -Name backup files with date stamps: `library-personal-2024-01-15.db` - + + Name backup files with date stamps: `library-personal-2024-01-15.db`. + -## Restore Libraries +## Restore libraries Recover from backups when needed: - + + +### Close Spacedrive + Quit the app completely. Ensure daemon stops. + - + + +### Replace library files + Copy backup files to the library directory, overwriting existing files. + - + + +### Restart Spacedrive + Launch app. The restored library appears in the switcher. + - + + +### Verify restoration + Check recent files and tags to confirm successful restore. + -## Delete Libraries +## Delete libraries Remove unwanted libraries cleanly: - + + +### Switch to different library + Ensure you're not currently using the library to delete. + - + + +### Open library manager + Access via Settings → Libraries → Manage. + - + + +### Select library + Choose the library and click "Delete Library". + - + + +### Confirm deletion + Type the library name to confirm. This permanently removes: + - Database files - Thumbnails - Organizational data - Sync relationships + - -Library deletion cannot be undone. Back up important libraries before deleting. - + + Library deletion cannot be undone. Back up important libraries before + deleting. + ## Troubleshooting ### Library won't open + - Check disk space for database - Verify file permissions - Try recovery mode (hold Shift on launch) - Restore from backup if corrupted ### Sync not working + - Confirm both devices online - Check Library IDs match - Verify network connectivity - Re-pair devices if needed ### Performance issues + - Reduce thumbnail quality - Limit concurrent jobs - Archive old data - Split large libraries ### Missing locations + - Locations are device-specific - Re-add locations after sync - Check paths still exist - Verify permissions -## Advanced Library Management +## Advanced library management -### Library Templates +### Library templates Create template libraries for repeated setups: @@ -306,19 +426,22 @@ Photo Workshop Template Export and import library configurations without data. -### Multi-User Libraries +### Multi-user libraries Share libraries safely: -**Read-Only Access** - Others can view but not modify -**Contributor Access** - Add files and tags -**Admin Access** - Full control including settings +**Read-only access** — others can view but not modify. + +**Contributor access** — add files and tags. + +**Admin access** — full control including settings. - -Multi-user features require Spacedrive Cloud (coming soon) or self-hosted server. - + + Multi-user features require Spacedrive Cloud (coming soon) or self-hosted + server. + -### Library Analytics +### Library analytics Monitor library usage: @@ -330,16 +453,16 @@ Monitor library usage: Access analytics in Settings → Library → Statistics. -## Best Practices +## Best practices -**Name Clearly**: Use descriptive names indicating purpose or content type. +**Name clearly**: use descriptive names indicating purpose or content type. -**Regular Backups**: Automate weekly backups of important libraries. +**Regular backups**: automate weekly backups of important libraries. -**Prune Regularly**: Remove unused locations and clean up old jobs. +**Prune regularly**: remove unused locations and clean up old jobs. -**Document Purpose**: Add descriptions explaining library organization. +**Document purpose**: add descriptions explaining library organization. -**Test Sync**: Verify sync works before relying on it. +**Test sync**: verify sync works before relying on it. -Your libraries now provide organized, separate spaces for different aspects of your digital life. Each library maintains its independence while enabling powerful organization features. \ No newline at end of file +Your libraries now provide organized, separate spaces for different aspects of your digital life. Each library maintains its independence while enabling powerful organization features. diff --git a/docs/content/docs/overview/meta.json b/docs/content/docs/overview/meta.json new file mode 100644 index 000000000000..a00c22bbfa12 --- /dev/null +++ b/docs/content/docs/overview/meta.json @@ -0,0 +1,16 @@ +{ + "title": "Overview", + "defaultOpen": true, + "pages": [ + "introduction", + "whitepaper", + "philosophy", + "history", + "---User Guides---", + "get-started", + "self-hosting", + "backup-photos-ios", + "manage-libraries", + "add-index-locations" + ] +} diff --git a/docs/overview/philosophy.mdx b/docs/content/docs/overview/philosophy.mdx similarity index 74% rename from docs/overview/philosophy.mdx rename to docs/content/docs/overview/philosophy.mdx index 7094af4327a5..50993f09ff16 100644 --- a/docs/overview/philosophy.mdx +++ b/docs/content/docs/overview/philosophy.mdx @@ -1,63 +1,63 @@ --- title: The Spacedrive Philosophy -sidebarTitle: Philosophy +description: Core tenets that guide every design decision in Spacedrive. --- -Spacedrive is more than a file manager; it is a paradigm shift in how humans interact with their digital assets. It was born from the universal frustration of "data fragmentation hell"—a state where our digital lives are scattered across countless devices and incompatible cloud services. +Spacedrive is more than a file manager; it is a paradigm shift in how humans interact with their digital assets. It was born from the universal frustration of "data fragmentation hell" — a state where our digital lives are scattered across countless devices and incompatible cloud services. Our mission is to create a unified, content-aware ecosystem that gives users complete control over their data. The V2 architecture is the realization of this vision, built on a set of core principles that guide every design decision, line of code, and future feature. --- -## The Core Tenets +## The core tenets -### 1. The User is Sovereign +### 1. The user is sovereign This is our most fundamental principle. In the Spacedrive ecosystem, you are in complete control. -- **Local-First by Default**: Spacedrive is designed to work entirely offline. All indexing, analysis, and processing happen on your devices, ensuring your data and your privacy remain yours alone. -- **You Own Your Data, Always**: We built Spacedrive as an intelligent layer that sits _on top_ of your existing storage. Your files stay where they are, in their original locations, with zero vendor lock-in. Backing up your entire organizational system is as simple as copying a single directory. -- **Human in the Loop**: The user is the final authority. AI agents and automation systems are designed to _propose_ actions, which are then presented in a clear, previewable format for you to approve. You are always in command. +- **Local-first by default**: Spacedrive is designed to work entirely offline. All indexing, analysis, and processing happen on your devices, ensuring your data and your privacy remain yours alone. +- **You own your data, always**: we built Spacedrive as an intelligent layer that sits _on top_ of your existing storage. Your files stay where they are, in their original locations, with zero vendor lock-in. Backing up your entire organizational system is as simple as copying a single directory. +- **Human in the loop**: the user is the final authority. AI agents and automation systems are designed to _propose_ actions, which are then presented in a clear, previewable format for you to approve. You are always in command. -### 2. From Chaos to Cohesion +### 2. From chaos to cohesion We aim to solve the universal problem of file chaos by creating a single, unified view of your entire digital world. -- **A Unified Virtual Layer**: The Virtual Distributed File System (VDFS) creates one interface to manage files across all devices and clouds. It makes scattered storage feel like a single, cohesive library. -- **Content is King**: We move beyond rigid, location-based folder hierarchies to a content-aware model. Through Content-Addressed Storage (CAS), Spacedrive understands what a file _is_, not just where it is, enabling powerful features like global deduplication and data integrity verification. -- **Location Transparency**: Our universal addressing system, `SdPath`, makes device boundaries disappear. A file on your offline laptop is as accessible as one on your local NAS, as the system can intelligently find and use any available copy. +- **A unified virtual layer**: the Virtual Distributed File System (VDFS) creates one interface to manage files across all devices and clouds. It makes scattered storage feel like a single, cohesive library. +- **Content is king**: we move beyond rigid, location-based folder hierarchies to a content-aware model. Through Content-Addressed Storage (CAS), Spacedrive understands what a file _is_, not just where it is, enabling powerful features like global deduplication and data integrity verification. +- **Location transparency**: our universal addressing system, `SdPath`, makes device boundaries disappear. A file on your offline laptop is as accessible as one on your local NAS, as the system can intelligently find and use any available copy. -### 3. Intelligence as Augmentation +### 3. Intelligence as augmentation AI is not a bolted-on feature; it is a foundational, **but optional** element of the architecture, designed to enhance your capabilities without compromising your control. -- **The AI Data Guardian**: With the official Guardian extension Spacedrive acts as a proactive protector of your data. By tracking file redundancy, it can identify irreplaceable memories that exist in only one location and suggest creating a backup before disaster strikes. -- **Natural Language as a Command Line**: You can manage your files by simply stating your intent. The system translates commands like "find my design assets from last fall" into safe, verifiable, and previewable actions. -- **Privacy-Preserving Intelligence**: We believe you shouldn't have to trade privacy for intelligence. Spacedrive is built to run powerful AI models locally on your hardware via tools like Ollama, ensuring your file contents are never sent to the cloud unless you explicitly choose to. +- **The AI Data Guardian**: with the official Guardian extension Spacedrive acts as a proactive protector of your data. By tracking file redundancy, it can identify irreplaceable memories that exist in only one location and suggest creating a backup before disaster strikes. +- **Natural language as a command line**: you can manage your files by simply stating your intent. The system translates commands like "find my design assets from last fall" into safe, verifiable, and previewable actions. +- **Privacy-preserving intelligence**: we believe you shouldn't have to trade privacy for intelligence. Spacedrive is built to run powerful AI models locally on your hardware via tools like Ollama, ensuring your file contents are never sent to the cloud unless you explicitly choose to. -### 4. Pragmatism in Engineering +### 4. Pragmatism in engineering The failure of Spacedrive V1 taught us a critical lesson: perfect is the enemy of good. The V2 architecture embodies a pragmatic approach focused on delivering value and reliability. -- **Simplicity over Complexity**: We replace over-engineered solutions with simpler, more robust patterns. V2's domain-separated sync avoids the "analysis paralysis" of a custom CRDT implementation, allowing us to ship a reliable sync system. -- **Developer Experience Matters**: We ruthlessly reduce boilerplate. The V2 job system, for example, cuts the code needed to add a new background operation by over 90%, enabling us to build and iterate faster. -- **Power for Everyone**: We engineer enterprise-grade capabilities to run efficiently on consumer hardware. Sophisticated features like semantic search, cross-device deduplication, and transactional operations are made accessible to everyone, not just large organizations. +- **Simplicity over complexity**: we replace over-engineered solutions with simpler, more robust patterns. V2's domain-separated sync avoids the "analysis paralysis" of a custom CRDT implementation, allowing us to ship a reliable sync system. +- **Developer experience matters**: we ruthlessly reduce boilerplate. The V2 job system, for example, cuts the code needed to add a new background operation by over 90%, enabling us to build and iterate faster. +- **Power for everyone**: we engineer enterprise-grade capabilities to run efficiently on consumer hardware. Sophisticated features like semantic search, cross-device deduplication, and transactional operations are made accessible to everyone, not just large organizations. -### 5. Open by Default +### 5. Open by default Trust is earned through transparency. Our commitment to open source is a core part of our identity. -- **Open Source for Control**: Spacedrive is open source to guarantee that you always retain absolute control over the software that manages your most important data. -- **Community as a Partner**: The project's success is tied to our community. The V2 whitepaper and architecture are a definitive technical blueprint, inviting developers to review, contribute, and help build the future of file management with us. -- **A Sustainable Vision**: We use a sustainable Open Core model. The core product is free for individuals, with paid features for teams and enterprises that ensure the project's long-term health and development. +- **Open source for control**: Spacedrive is open source to guarantee that you always retain absolute control over the software that manages your most important data. +- **Community as a partner**: the project's success is tied to our community. The V2 whitepaper and architecture are a definitive technical blueprint, inviting developers to review, contribute, and help build the future of file management with us. +- **A sustainable vision**: we use a sustainable Open Core model. The core product is free for individuals, with paid features for teams and enterprises that ensure the project's long-term health and development. -### 6. A New Way to Build +### 6. A new way to build The story of V2's creation is a meta-philosophy in itself. It demonstrates a revolutionary new paradigm for software development. -- **The AI-Augmented Team**: Spacedrive V2 was rebuilt from the ground up by a single developer orchestrating a suite of specialized AI assistants. This approach proved to be 100x faster and more effective than a traditional team. -- **Radical Capital Efficiency**: This new development model changes the economics of building software. It allows capital to be invested in growth, security, and infrastructure instead of being consumed by large team salaries. -- **Elite, Focused Teams**: Our hiring philosophy is to automate first and hire only the best humans for roles that require strategic impact. We believe small, focused, high-impact teams build better products. +- **The AI-augmented team**: Spacedrive V2 was rebuilt from the ground up by a single developer orchestrating a suite of specialized AI assistants. This approach proved to be 100x faster and more effective than a traditional team. +- **Radical capital efficiency**: this new development model changes the economics of building software. It allows capital to be invested in growth, security, and infrastructure instead of being consumed by large team salaries. +- **Elite, focused teams**: our hiring philosophy is to automate first and hire only the best humans for roles that require strategic impact. We believe small, focused, high-impact teams build better products. --- diff --git a/docs/overview/self-hosting.mdx b/docs/content/docs/overview/self-hosting.mdx similarity index 90% rename from docs/overview/self-hosting.mdx rename to docs/content/docs/overview/self-hosting.mdx index 00efac13c2fa..c479659283f3 100644 --- a/docs/overview/self-hosting.mdx +++ b/docs/content/docs/overview/self-hosting.mdx @@ -1,16 +1,15 @@ --- title: Self-Hosting Spacedrive -description: Deploy Spacedrive server for remote access and headless operation -sidebarTitle: Self-Hosting +description: Deploy the Spacedrive server for remote access and headless operation. --- Self-hosting Spacedrive means running the server component on hardware you control. The server provides HTTP access to your libraries, enabling remote connections from desktop and mobile apps. Unlike the desktop app which requires a GUI, the server runs headless on Linux systems, NAS devices, or cloud instances. The server embeds the full Spacedrive daemon with media processing capabilities. It exposes RPC endpoints over HTTP with optional basic authentication. Your data stays on your infrastructure while remaining accessible from any device with network access. -## Installation Methods +## Installation methods -### Static Binary +### Static binary Download the latest release binary for your platform. This method works on any Linux system and requires no container runtime. @@ -29,7 +28,7 @@ sudo chmod +x /usr/local/bin/sd-server For ARM systems like Raspberry Pi, use `sd-server-linux-aarch64.tar.gz` instead. -### Docker Deployment +### Docker deployment Docker images are available for both x86_64 and ARM64 architectures. The image includes all media processing dependencies and runs as a non-root user. @@ -55,21 +54,21 @@ services: spacedrive: image: ghcr.io/spacedriveapp/spacedrive/server:latest ports: - - "8080:8080" - - "7373:7373" + - '8080:8080' + - '7373:7373' volumes: - spacedrive-data:/data - /mnt/media:/media:ro environment: - SD_AUTH: "admin:your-password" - TZ: "America/New_York" + SD_AUTH: 'admin:your-password' + TZ: 'America/New_York' restart: unless-stopped volumes: spacedrive-data: ``` -## System Service Setup +## System service setup Running the server as a systemd service ensures it starts on boot and restarts on failure. @@ -115,15 +114,17 @@ The server accepts configuration via environment variables or command-line flags **Port configuration** defaults to 8080. Change with `--port` flag or `PORT` environment variable. The P2P port (7373) is fixed and required for device sync. - -Production deployments must set `SD_AUTH`. The server refuses to start without authentication unless explicitly disabled. This prevents accidental exposure of your libraries. - + + Production deployments must set `SD_AUTH`. The server refuses to start + without authentication unless explicitly disabled. This prevents accidental + exposure of your libraries. + -## Network Access +## Network access The server binds to all interfaces by default, making it accessible on your local network. For internet access, place it behind a reverse proxy with TLS termination. -### Nginx Example +### Nginx example ```nginx server { @@ -144,7 +145,7 @@ server { } ``` -### Caddy Example +### Caddy example Caddy handles TLS automatically with Let's Encrypt: @@ -154,7 +155,7 @@ spacedrive.example.com { } ``` -## NAS Deployment +## NAS deployment Network-attached storage devices are ideal for Spacedrive server. The server indexes existing media while providing remote access. @@ -179,23 +180,25 @@ Use Container Manager to deploy the Docker image. Configure port forwarding and Add a custom container template with the image `ghcr.io/spacedriveapp/spacedrive/server:latest`. Map your shares as additional paths in the container configuration. -## Storage Considerations +## Storage considerations The server creates three types of data in the data directory: **Libraries** contain SQLite databases with file indexes, tags, and metadata. Each library is a separate `.sdlibrary` directory. These are small, typically under 100MB even for millions of files. -**Sidecars** include thumbnails, previews, and extracted text. Size depends on media processing settings. Budget 1-5% of your total media size for thumbnails. +**Sidecars** include thumbnails, previews, and extracted text. Size depends on media processing settings. Budget 1–5% of your total media size for thumbnails. **Logs** rotate automatically but can grow large during initial indexing. The server keeps the last 10 log files, typically under 100MB total. -Plan for library growth over time. A library indexing 1 million files uses approximately 500MB for the database and 10-50GB for sidecars, depending on media density. +Plan for library growth over time. A library indexing 1 million files uses approximately 500MB for the database and 10–50GB for sidecars, depending on media density. - -The server never modifies your original files. Indexing is read-only. File operations like copy and move require explicit user action through connected clients. - + + The server never modifies your original files. Indexing is read-only. File + operations like copy and move require explicit user action through connected + clients. + -## Security Best Practices +## Security best practices Self-hosted servers face different threats than desktop apps. Follow these guidelines for secure deployment. @@ -209,7 +212,7 @@ Self-hosted servers face different threats than desktop apps. Follow these guide **Regular backups** of the data directory protect against corruption. The SQLite databases can be backed up while running using the `.backup` command or by copying the entire data directory when the server is stopped. -## Connecting Clients +## Connecting clients Desktop and mobile apps can connect to self-hosted servers by configuring the server URL in settings. @@ -227,7 +230,7 @@ Logs are written to stdout and the data directory under `logs/`. Set `RUST_LOG` Disk usage in the data directory grows with library size. Alert when free space drops below 20% to prevent database corruption. -Memory usage typically ranges from 100-500MB depending on active jobs. Spike during intensive operations like thumbnail generation. +Memory usage typically ranges from 100–500MB depending on active jobs. Spike during intensive operations like thumbnail generation. Network bandwidth peaks during initial sync with new devices. Ongoing sync uses minimal bandwidth after the first synchronization. @@ -253,11 +256,11 @@ The server requires FFmpeg and libheif for video thumbnails and HEIF images. Doc UDP port 7373 must be accessible for P2P connections. Check NAT and firewall rules. The server attempts hole-punching for NAT traversal but may fall back to relay servers if direct connection fails. -## Performance Tuning +## Performance tuning The server handles multiple concurrent operations through the job system. Performance depends on storage speed, available memory, and indexing load. -**Indexing performance** is limited by storage I/O. SSDs provide 10-50x faster indexing than spinning disks. Network storage over gigabit ethernet typically indexes at 50-100MB/s for metadata reads. +**Indexing performance** is limited by storage I/O. SSDs provide 10–50x faster indexing than spinning disks. Network storage over gigabit ethernet typically indexes at 50–100MB/s for metadata reads. **Thumbnail generation** is CPU-bound. Disable thumbnail generation for video files if CPU usage is a concern. Image thumbnails are fast but video processing can use significant resources. @@ -265,7 +268,7 @@ The server handles multiple concurrent operations through the job system. Perfor **Memory allocation** can be limited using systemd or Docker resource constraints. The server operates efficiently with 512MB minimum, though 2GB provides better performance for large libraries. -## Backup Strategy +## Backup strategy Back up the data directory regularly to prevent data loss. The recommended approach depends on your infrastructure. diff --git a/docs/overview/whitepaper.mdx b/docs/content/docs/overview/whitepaper.mdx similarity index 65% rename from docs/overview/whitepaper.mdx rename to docs/content/docs/overview/whitepaper.mdx index 6a2b8b7c4729..07b42133ebef 100644 --- a/docs/overview/whitepaper.mdx +++ b/docs/content/docs/overview/whitepaper.mdx @@ -1,59 +1,59 @@ --- title: Spacedrive V2 Whitepaper -sidebarTitle: Whitepaper +description: Why we wrote the V2 whitepaper, what it covers, and how to use it. --- This document explains the purpose of the Spacedrive V2 whitepaper, what it covers, and how our community can use it to understand, contribute to, and build upon the future of personal data management. ## [Read the paper →](https://github.com/spacedriveapp/spacedrive/blob/main/whitepaper/spacedrive.pdf) -## Why We Wrote This Whitepaper +## Why we wrote this whitepaper Spacedrive V2 represents a complete architectural reimagining of the project, designed to fulfill the original vision on a more robust and scalable foundation. After a period of reflection on the challenges faced by the first version, we recognized the need for a foundational rewrite to address critical issues like the "Dual File System Problem" and fragmented networking. This whitepaper was created to serve three primary purposes: -1. **To Provide a Definitive Technical Blueprint**: It is the single source of truth for the Spacedrive V2 architecture, detailing the core concepts, design decisions, and innovations that power the new system. -2. **To Re-engage Our Community**: We want to share our renewed vision and technical direction transparently, providing a clear path for developers, contributors, and users to rally behind. -3. **To Guide Future Development**: The document serves as a roadmap and a set of guiding principles, ensuring that all future contributions align with the core architectural tenets of performance, privacy, and user control. +1. **To provide a definitive technical blueprint**: it is the single source of truth for the Spacedrive V2 architecture, detailing the core concepts, design decisions, and innovations that power the new system. +2. **To re-engage our community**: we want to share our renewed vision and technical direction transparently, providing a clear path for developers, contributors, and users to rally behind. +3. **To guide future development**: the document serves as a roadmap and a set of guiding principles, ensuring that all future contributions align with the core architectural tenets of performance, privacy, and user control. Ultimately, this paper is our commitment to building a paradigm shift in how humans interact with their digital assets in the AI era. --- -## What This Whitepaper Covers +## What this whitepaper covers The whitepaper presents the complete architecture of Spacedrive V2, a local-first, AI-native Virtual Distributed File System (VDFS). It details the five foundational innovations that solve traditionally hard problems in distributed systems for a consumer-grade product. Key architectural pillars covered in detail include: -- **A Virtual Distributed File System (VDFS)**: A unified, virtual layer that provides a single view of all your data across every device and cloud, while the files themselves stay in their original locations. This is made possible by a universal addressing system called **`SdPath`**. -- **An AI-Native Architecture**: The system is designed from the ground up for intelligent management, enabling natural language commands ("find my tax documents from last year") and proactive assistance from a data guardian that respects user privacy. -- **A Transactional Action System**: All file operations are treated as transactions that can be previewed before they are committed, preventing conflicts and guaranteeing completion even across offline devices. -- **Domain-Separated Library Sync**: A novel synchronization method that maintains consistency across devices without the complexity of distributed consensus algorithms like CRDTs. -- **The Content Identity System**: A content-addressable foundation that provides intelligent, cross-device deduplication while also powering a "Data Guardian" feature that monitors data redundancy to protect against loss. -- **A Modern, Performant Implementation**: The entire core is implemented in **Rust** on a modern, asynchronous technology stack designed for enterprise-grade capabilities on consumer hardware. +- **A Virtual Distributed File System (VDFS)**: a unified, virtual layer that provides a single view of all your data across every device and cloud, while the files themselves stay in their original locations. This is made possible by a universal addressing system called **`SdPath`**. +- **An AI-native architecture**: the system is designed from the ground up for intelligent management, enabling natural language commands ("find my tax documents from last year") and proactive assistance from a data guardian that respects user privacy. +- **A transactional action system**: all file operations are treated as transactions that can be previewed before they are committed, preventing conflicts and guaranteeing completion even across offline devices. +- **Domain-separated library sync**: a novel synchronization method that maintains consistency across devices without the complexity of distributed consensus algorithms like CRDTs. +- **The content identity system**: a content-addressable foundation that provides intelligent, cross-device deduplication while also powering a "Data Guardian" feature that monitors data redundancy to protect against loss. +- **A modern, performant implementation**: the entire core is implemented in **Rust** on a modern, asynchronous technology stack designed for enterprise-grade capabilities on consumer hardware. --- -## How to Use This Document +## How to use this document This whitepaper is more than a technical document; it's an invitation to our community to help build the future of file management. -#### **For Developers & Contributors:** +### For developers and contributors -- **Understand the Vision**: Before diving into the code, read the whitepaper to understand the "why" behind the architecture. It provides the context for our design choices and the problems we are solving. -- **Guide Your Contributions**: Use this document alongside the **Roadmap** as a guide. The architecture detailed here is the blueprint for all new features. Whether you're fixing a bug or building a new operation, it should align with these core principles. -- **Build with Confidence**: The paper explains the core abstractions like `SdPath`, the `ActionManager`, and the `JobManager`. Understanding these will help you build new features that integrate seamlessly and reliably with the rest of the system. +- **Understand the vision**: before diving into the code, read the whitepaper to understand the "why" behind the architecture. It provides the context for our design choices and the problems we are solving. +- **Guide your contributions**: use this document alongside the roadmap as a guide. The architecture detailed here is the blueprint for all new features. Whether you're fixing a bug or building a new operation, it should align with these core principles. +- **Build with confidence**: the paper explains the core abstractions like `SdPath`, the `ActionManager`, and the `JobManager`. Understanding these will help you build new features that integrate seamlessly and reliably with the rest of the system. -#### **For System Architects & Designers:** +### For system architects and designers -- **Review and Feedback**: We welcome feedback on our architectural decisions. The whitepaper details our solutions to complex problems like synchronization and distributed file operations. If you have insights or see potential improvements, we encourage you to start a discussion. -- **Propose Enhancements**: The document provides the context for proposing new, large-scale features or optimizations, such as the planned migration to a Closure Table for hierarchical queries. +- **Review and feedback**: we welcome feedback on our architectural decisions. The whitepaper details our solutions to complex problems like synchronization and distributed file operations. If you have insights or see potential improvements, we encourage you to start a discussion. +- **Propose enhancements**: the document provides the context for proposing new, large-scale features or optimizations, such as the planned migration to a Closure Table for hierarchical queries. -#### **For the Entire Community:** +### For the entire community -- **The Source of Truth**: When you have questions about how Spacedrive works under the hood, this document is the canonical source. It explains how we handle your data, ensure privacy, and deliver powerful features in a local-first environment. -- **A Foundation for Discussion**: Use this paper as a common ground for discussions about Spacedrive's future. It ensures everyone is working from the same set of assumptions about the technology. +- **The source of truth**: when you have questions about how Spacedrive works under the hood, this document is the canonical source. It explains how we handle your data, ensure privacy, and deliver powerful features in a local-first environment. +- **A foundation for discussion**: use this paper as a common ground for discussions about Spacedrive's future. It ensures everyone is working from the same set of assumptions about the technology. We are incredibly excited about the foundation we've built with Spacedrive V2 and believe it positions us to solve the fundamental problems of data fragmentation and privacy for the modern era. We invite you to read, discuss, and build with us. diff --git a/docs/content/docs/react/meta.json b/docs/content/docs/react/meta.json new file mode 100644 index 000000000000..ab7b9ed2503b --- /dev/null +++ b/docs/content/docs/react/meta.json @@ -0,0 +1,5 @@ +{ + "title": "React UI", + "defaultOpen": true, + "pages": ["ui"] +} diff --git a/docs/react/ui/colors.mdx b/docs/content/docs/react/ui/colors.mdx similarity index 99% rename from docs/react/ui/colors.mdx rename to docs/content/docs/react/ui/colors.mdx index 5ca3c2905244..e96e3f955972 100644 --- a/docs/react/ui/colors.mdx +++ b/docs/content/docs/react/ui/colors.mdx @@ -1,6 +1,5 @@ --- title: Colors -sidebarTitle: Colors --- Spacedrive uses a semantic color system built on CSS variables and HSL values. The system provides context-aware colors that automatically support theming and opacity modifiers. diff --git a/docs/react/ui/explorer.mdx b/docs/content/docs/react/ui/explorer.mdx similarity index 94% rename from docs/react/ui/explorer.mdx rename to docs/content/docs/react/ui/explorer.mdx index 102ea2a94a8b..fc68748d4884 100644 --- a/docs/react/ui/explorer.mdx +++ b/docs/content/docs/react/ui/explorer.mdx @@ -1,6 +1,5 @@ --- title: Explorer Architecture -sidebarTitle: Explorer --- The Explorer is Spacedrive's primary file browsing interface. It displays files, locations, volumes, and devices using a unified system that treats all entities as navigable directories. The architecture centers on a single source of truth for navigation: URL query parameters. @@ -19,7 +18,7 @@ The Explorer uses URL query parameters instead of route paths to manage navigati When you navigate to a location from the sidebar, the URL updates with the new path. The Explorer reads this URL parameter and displays the corresponding directory. The sidebar highlights the active item by comparing its own path to the URL parameter. -The URL is the single source of truth. All navigation actions update the URL first, then components react to URL changes. +The URL is the single source of truth. All navigation actions update the URL first, then components react to URL changes. ### History Management @@ -111,7 +110,7 @@ if (isVirtualView && virtualFiles) { The first column displays virtual files. When you select one, the second column queries the backend for real directory contents using the virtual file's `sd_path`. -Virtual files must never be passed to file operations like copy, move, or delete. Always check `isVirtualFile()` before backend mutations. +Virtual files must never be passed to file operations like copy, move, or delete. Always check `isVirtualFile()` before backend mutations. ## Safety Guards @@ -156,7 +155,7 @@ const volumeTypeStr = typeof volume.volume_type === "string" This pattern extracts the `Other` variant value when present, falls back to JSON stringification, or uses the string directly if it's already a string. -Apply this pattern consistently to `file_system`, `disk_type`, `volume_type`, `form_factor`, and any other backend enum fields. +Apply this pattern consistently to `file_system`, `disk_type`, `volume_type`, `form_factor`, and any other backend enum fields. ## Component Communication @@ -207,4 +206,3 @@ if (isVolume) { ``` This adds visual information without requiring volume-specific components or branching logic in the core Explorer. - diff --git a/docs/react/ui/hooks.mdx b/docs/content/docs/react/ui/hooks.mdx similarity index 99% rename from docs/react/ui/hooks.mdx rename to docs/content/docs/react/ui/hooks.mdx index 30df3de8f6d5..a0a383dbe512 100644 --- a/docs/react/ui/hooks.mdx +++ b/docs/content/docs/react/ui/hooks.mdx @@ -1,6 +1,5 @@ --- title: React Hooks -sidebarTitle: Hooks --- Spacedrive provides type-safe React hooks for data fetching, mutations, and event subscriptions. All types are auto-generated from Rust definitions. @@ -336,7 +335,7 @@ function EventDebugger() { } ``` -**Warning:** This can be noisy. Use `useEvent` for specific events in production. +This can be noisy. Use `useEvent` for specific events in production. ## Custom Hooks diff --git a/docs/content/docs/react/ui/meta.json b/docs/content/docs/react/ui/meta.json new file mode 100644 index 000000000000..33f1953e8a81 --- /dev/null +++ b/docs/content/docs/react/ui/meta.json @@ -0,0 +1,12 @@ +{ + "title": "UI", + "pages": [ + "colors", + "primitives", + "hooks", + "normalized-cache", + "platform", + "explorer", + "popout-windows" + ] +} diff --git a/docs/react/ui/normalized-cache.mdx b/docs/content/docs/react/ui/normalized-cache.mdx similarity index 96% rename from docs/react/ui/normalized-cache.mdx rename to docs/content/docs/react/ui/normalized-cache.mdx index 71534bfe8632..19c64abff765 100644 --- a/docs/react/ui/normalized-cache.mdx +++ b/docs/content/docs/react/ui/normalized-cache.mdx @@ -1,6 +1,6 @@ --- title: Normalized Query -sidebarTitle: Normalized Query +description: Real-time normalized cache with TanStack Query, event-driven updates, and server-side filtering. --- # Real-Time Normalized Cache with TanStack Query @@ -20,8 +20,6 @@ The `useNormalizedQuery` hook provides instant, event-driven cache updates with ## Architecture -import { FlowDiagram } from '/snippets/FlowDiagram.mdx'; - + Popout windows only work on Tauri desktop builds. The web version shows components inline since browsers don't support multiple windows with shared state. - + ## Adding New Popout Windows diff --git a/docs/react/ui/primitives.mdx b/docs/content/docs/react/ui/primitives.mdx similarity index 99% rename from docs/react/ui/primitives.mdx rename to docs/content/docs/react/ui/primitives.mdx index 90a9b03e9a32..587975ad501d 100644 --- a/docs/react/ui/primitives.mdx +++ b/docs/content/docs/react/ui/primitives.mdx @@ -1,6 +1,5 @@ --- title: UI Primitives -sidebarTitle: Primitives --- Spacedrive's UI is built on a set of reusable primitives from `@sd/ui`. These components provide consistent styling and behavior across the application. diff --git a/docs/custom.css b/docs/custom.css deleted file mode 100644 index 445ea69c06b3..000000000000 --- a/docs/custom.css +++ /dev/null @@ -1,15 +0,0 @@ -/* Anchor hover styles */ -.nav-anchor:hover { - @apply text-[#36a3ff]; -} - -/* Icon wrapper on hover */ -.nav-anchor:hover div { - background: #36A3FF !important; - filter: brightness(1) !important; -} - -/* Icon SVG on hover */ -.nav-anchor:hover svg { - @apply bg-white !important; -} diff --git a/docs/archive/README.md b/docs/internal/ARCHIVE_README.md similarity index 100% rename from docs/archive/README.md rename to docs/internal/ARCHIVE_README.md diff --git a/docs/design/MIGRATE-TO-SPACEUI.md b/docs/internal/MIGRATE-TO-SPACEUI.md similarity index 100% rename from docs/design/MIGRATE-TO-SPACEUI.md rename to docs/internal/MIGRATE-TO-SPACEUI.md diff --git a/docs/design/POPOVER-REFACTOR.md b/docs/internal/POPOVER-REFACTOR.md similarity index 100% rename from docs/design/POPOVER-REFACTOR.md rename to docs/internal/POPOVER-REFACTOR.md diff --git a/docs/design/SERVER_RELEASE_SETUP.md b/docs/internal/SERVER_RELEASE_SETUP.md similarity index 100% rename from docs/design/SERVER_RELEASE_SETUP.md rename to docs/internal/SERVER_RELEASE_SETUP.md diff --git a/docs/core/design/archive.md b/docs/internal/core-design/archive.md similarity index 100% rename from docs/core/design/archive.md rename to docs/internal/core-design/archive.md diff --git a/docs/core/design/file-system-intelligence.md b/docs/internal/core-design/file-system-intelligence.md similarity index 100% rename from docs/core/design/file-system-intelligence.md rename to docs/internal/core-design/file-system-intelligence.md diff --git a/docs/core/design/spacebot-integration.md b/docs/internal/core-design/spacebot-integration.md similarity index 100% rename from docs/core/design/spacebot-integration.md rename to docs/internal/core-design/spacebot-integration.md diff --git a/docs/core/design/spacebot-remote-execution.md b/docs/internal/core-design/spacebot-remote-execution.md similarity index 100% rename from docs/core/design/spacebot-remote-execution.md rename to docs/internal/core-design/spacebot-remote-execution.md diff --git a/docs/core/design/spacebot-spacedrive-contract.md b/docs/internal/core-design/spacebot-spacedrive-contract.md similarity index 100% rename from docs/core/design/spacebot-spacedrive-contract.md rename to docs/internal/core-design/spacebot-spacedrive-contract.md diff --git a/docs/lib/cn.ts b/docs/lib/cn.ts new file mode 100644 index 000000000000..ba66fd250be5 --- /dev/null +++ b/docs/lib/cn.ts @@ -0,0 +1 @@ +export { twMerge as cn } from 'tailwind-merge'; diff --git a/docs/lib/layout.shared.tsx b/docs/lib/layout.shared.tsx new file mode 100644 index 000000000000..115b39070e7f --- /dev/null +++ b/docs/lib/layout.shared.tsx @@ -0,0 +1,17 @@ +import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; + +export const gitConfig = { + user: 'spacedriveapp', + repo: 'spacedrive', + branch: 'main', + docsPath: 'docs/content/docs', +}; + +export function baseOptions(): BaseLayoutProps { + return { + nav: { + title: 'Spacedrive', + }, + githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`, + }; +} diff --git a/docs/lib/source.ts b/docs/lib/source.ts new file mode 100644 index 000000000000..92df6bb54226 --- /dev/null +++ b/docs/lib/source.ts @@ -0,0 +1,22 @@ +import { type InferPageType, loader } from 'fumadocs-core/source'; +import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; +import { docs } from 'fumadocs-mdx:collections/server'; + +export const source = loader({ + baseUrl: '/', + source: docs.toFumadocsSource(), + plugins: [lucideIconsPlugin()], +}); + +export function getPageImage(page: InferPageType) { + const segments = [...page.slugs, 'image.png']; + return { + segments, + url: `/og/docs/${segments.join('/')}`, + }; +} + +export async function getLLMText(page: InferPageType) { + const processed = await page.data.getText('processed'); + return `# ${page.data.title}\n\n${processed}`; +} diff --git a/docs/mdx-components.tsx b/docs/mdx-components.tsx new file mode 100644 index 000000000000..526d20931f61 --- /dev/null +++ b/docs/mdx-components.tsx @@ -0,0 +1,29 @@ +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { File, Files, Folder } from 'fumadocs-ui/components/files'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import type { MDXComponents } from 'mdx/types'; +import { FlowDiagram } from '@/components/flow-diagram'; + +export function getMDXComponents(components?: MDXComponents): MDXComponents { + return { + ...defaultMdxComponents, + Accordion, + Accordions, + Callout, + Card, + Cards, + File, + Files, + Folder, + FlowDiagram, + Step, + Steps, + Tab, + Tabs, + ...components, + }; +} diff --git a/docs/mint.json b/docs/mint.json deleted file mode 100644 index eecb28634e20..000000000000 --- a/docs/mint.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "$schema": "https://mintlify.com/schema.json", - "name": "Spacedrive", - "logo": { - "light": "/logo/spacedrive-logo.png", - "dark": "/logo/spacedrive-logo.png" - }, - "favicon": "/public/favicon.png", - "colors": { - "primary": "#36A3FF", - "light": "#36A3FF", - "dark": "#36A3FF" - }, - "styles": { - "css": ["/custom.css"] - }, - "anchors": [ - { - "name": "Overview", - "icon": "book-open", - "url": "overview" - }, - { - "name": "Developer", - "icon": "code", - "url": "core" - }, - { - "name": "SDK", - "icon": "puzzle-piece", - "url": "extensions" - }, - { - "name": "CLI", - "icon": "terminal", - "url": "cli" - }, - { - "name": "Interface", - "icon": "palette", - "url": "react" - } - ], - "navigation": [ - { - "group": "Getting Started", - "icon": "book-open", - "pages": [ - "overview/introduction", - "overview/whitepaper", - "overview/philosophy", - "overview/history" - ] - }, - { - "group": "User Guides", - "icon": "compass", - "pages": [ - "overview/get-started", - "overview/self-hosting", - "overview/backup-photos-ios", - "overview/manage-libraries", - "overview/add-index-locations" - ] - }, - { - "group": "Architecture", - "icon": "cube", - "pages": [ - "core/architecture", - "core/library", - "core/data-model", - "core/key-manager", - "core/addressing", - "core/jobs", - "core/ops", - "core/api", - "core/events" - ] - }, - { - "group": "File Management", - "icon": "folder", - "pages": [ - "core/indexing", - "core/locations", - "core/devices", - "core/volumes", - "core/file-copy-operations", - "core/tagging", - "core/virtual-sidecars" - ] - }, - { - "group": "Sync & Network", - "icon": "network-wired", - "pages": [ - "core/networking", - "core/pairing", - "core/proxy-pairing", - "core/library-sync", - "core/file-sync", - "core/cloud-integration" - ] - }, - { - "group": "Development", - "icon": "flask", - "pages": ["core/database", "core/testing", "core/releases", "core/sync-event-log", "core/task-tracking", "core/cli"] - }, - { - "group": "Extension SDK", - "icon": "code", - "pages": [ - "extensions/introduction", - "extensions/getting-started", - "extensions/core-concepts", - "extensions/data-storage", - "extensions/security-and-sync", - "extensions/ui-integration", - "extensions/examples" - ] - }, - { - "group": "CLI Reference", - "icon": "terminal", - "pages": [ - "cli/overview", - "cli/linux-deployment", - "cli/library-sync-setup", - "cli/multi-instance", - "cli/index-verify" - ] - }, - { - "group": "React UI", - "icon": "palette", - "pages": [ - "react/ui/colors", - "react/ui/primitives", - "react/ui/hooks", - "react/ui/normalized-cache", - "react/ui/platform", - "react/ui/explorer" - ] - } - ] -} diff --git a/docs/next.config.mjs b/docs/next.config.mjs new file mode 100644 index 000000000000..97b2eab54a98 --- /dev/null +++ b/docs/next.config.mjs @@ -0,0 +1,30 @@ +import { createMDX } from 'fumadocs-mdx/next'; + +const withMDX = createMDX(); + +/** @type {import('next').NextConfig} */ +const config = { + reactStrictMode: true, + turbopack: { + root: import.meta.dirname, + }, + async redirects() { + return [ + { + source: '/', + destination: '/overview/introduction', + permanent: false, + }, + ]; + }, + async rewrites() { + return [ + { + source: '/:path*.mdx', + destination: '/llms.mdx/docs/:path*', + }, + ]; + }, +}; + +export default withMDX(config); diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000000..bd45270b3e27 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,37 @@ +{ + "name": "@sd/docs", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev -p 19831", + "start": "next start -p 19831", + "types:check": "fumadocs-mdx && next typegen && tsc --noEmit", + "postinstall": "fumadocs-mdx", + "lint": "biome check", + "format": "biome format --write" + }, + "dependencies": { + "@radix-ui/react-popover": "^1.1.15", + "class-variance-authority": "^0.7.1", + "fumadocs-core": "^16.6.1", + "fumadocs-mdx": "^14.2.7", + "fumadocs-ui": "^16.6.1", + "lucide-react": "^0.563.0", + "next": "^16.1.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.15", + "@tailwindcss/postcss": "^4.1.18", + "@types/mdx": "^2.0.13", + "@types/node": "^25.2.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3" + } +} diff --git a/docs/postcss.config.mjs b/docs/postcss.config.mjs new file mode 100644 index 000000000000..2c59869459ff --- /dev/null +++ b/docs/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/docs/logo/spacedrive-logo.png b/docs/public/logo/spacedrive-logo.png similarity index 100% rename from docs/logo/spacedrive-logo.png rename to docs/public/logo/spacedrive-logo.png diff --git a/docs/public/SDColumnView.webp b/docs/public/screenshots/SDColumnView.webp similarity index 100% rename from docs/public/SDColumnView.webp rename to docs/public/screenshots/SDColumnView.webp diff --git a/docs/public/SDGridView.webp b/docs/public/screenshots/SDGridView.webp similarity index 100% rename from docs/public/SDGridView.webp rename to docs/public/screenshots/SDGridView.webp diff --git a/docs/public/SDMediaView.webp b/docs/public/screenshots/SDMediaView.webp similarity index 100% rename from docs/public/SDMediaView.webp rename to docs/public/screenshots/SDMediaView.webp diff --git a/docs/public/SDSizeView.webp b/docs/public/screenshots/SDSizeView.webp similarity index 100% rename from docs/public/SDSizeView.webp rename to docs/public/screenshots/SDSizeView.webp diff --git a/docs/public/SDSplatView.webp b/docs/public/screenshots/SDSplatView.webp similarity index 100% rename from docs/public/SDSplatView.webp rename to docs/public/screenshots/SDSplatView.webp diff --git a/docs/public/SDVideoPlayer.webp b/docs/public/screenshots/SDVideoPlayer.webp similarity index 100% rename from docs/public/SDVideoPlayer.webp rename to docs/public/screenshots/SDVideoPlayer.webp diff --git a/docs/snippets/FlowDiagram.mdx b/docs/snippets/FlowDiagram.mdx deleted file mode 100644 index 0b8c3dafd19c..000000000000 --- a/docs/snippets/FlowDiagram.mdx +++ /dev/null @@ -1,48 +0,0 @@ -export const FlowDiagram = ({ steps = [] }) => { - return ( -
- {steps.map((step, index) => ( -
-
-
-
- {index + 1} -
-
-

{step.title}

- {step.description && ( -

{step.description}

- )} - {step.items && Array.isArray(step.items) && ( -
- {step.items.map((item, i) => ( - - {item} - - ))} -
- )} - {step.metrics && ( -
- {Object.entries(step.metrics).map(([key, value]) => ( -
- {key}:{' '} - {value} -
- ))} -
- )} -
-
-
- {index < steps.length - 1 && ( -
-
-
-
- )} -
- ))} -
- ); -}; diff --git a/docs/snippets/README.md b/docs/snippets/README.md deleted file mode 100644 index 89cabadf6a3d..000000000000 --- a/docs/snippets/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Custom Mintlify Components - -This directory contains reusable React components for Spacedrive documentation. - -## Available Components - -### FlowDiagram - -A flow diagram component for visualizing multi-step processes with Spacedrive styling. - -**Usage:** - -```mdx -import { FlowDiagram } from '/snippets/FlowDiagram.mdx'; - - -``` - -**Props:** - -- `steps` (array, required): Array of step objects - - `title` (string, required): Step title - - `description` (string, optional): Step description - - `details` (string[], optional): Bullet points for additional details - - `metrics` (object, optional): Key-value pairs displayed as metric badges - -**Example:** - -See `docs/react/ui/normalized-cache.mdx` for a real-world example. - -## Adding New Components - -1. Create a new `.mdx` file in this directory -2. Export your React component -3. Import and use it in any documentation page with `import { Component } from '/snippets/Component.mdx'` - -## Styling - -Components use Tailwind CSS classes and Spacedrive's accent color (`#36A3FF`). diff --git a/docs/source.config.ts b/docs/source.config.ts new file mode 100644 index 000000000000..7dcaa2b4ecba --- /dev/null +++ b/docs/source.config.ts @@ -0,0 +1,19 @@ +import { metaSchema, pageSchema } from 'fumadocs-core/source/schema'; +import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; + +export const docs = defineDocs({ + dir: 'content/docs', + docs: { + schema: pageSchema, + postprocess: { + includeProcessedMarkdown: true, + }, + }, + meta: { + schema: metaSchema, + }, +}); + +export default defineConfig({ + mdxOptions: {}, +}); diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 000000000000..a411add52ffc --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "paths": { + "@/*": ["./*"], + "fumadocs-mdx:collections/*": [".source/*"] + }, + "plugins": [{ "name": "next" }] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/packages/interface/src/ShellLayout.tsx b/packages/interface/src/ShellLayout.tsx index 4a0fc6432be6..702a47620aad 100644 --- a/packages/interface/src/ShellLayout.tsx +++ b/packages/interface/src/ShellLayout.tsx @@ -18,6 +18,7 @@ import { } from './components/TabManager'; import {usePlatform} from './contexts/PlatformContext'; import {useNormalizedQuery} from './contexts/SpacedriveContext'; +import {WebContextMenuProvider} from './contexts/WebContextMenuContext'; import {ExplorerProvider, useExplorer} from './routes/explorer'; import {KeyboardHandler} from './routes/explorer/KeyboardHandler'; import {SelectionProvider} from './routes/explorer/SelectionContext'; @@ -133,7 +134,12 @@ function ShellLayoutContent() { const isSizeViewActive = viewMode === 'size'; return ( -
+
{/* Preview layer - portal target for fullscreen preview, sits between content and sidebar/inspector */}
- {/* Sync tab navigation and defaults with router */} - - - + + {/* Sync tab navigation and defaults with router */} + + + + diff --git a/packages/interface/src/components/SpacesSidebar/SpaceCustomizationPanel.tsx b/packages/interface/src/components/SpacesSidebar/SpaceCustomizationPanel.tsx index f75ec47553f0..2ee7b28c7831 100644 --- a/packages/interface/src/components/SpacesSidebar/SpaceCustomizationPanel.tsx +++ b/packages/interface/src/components/SpacesSidebar/SpaceCustomizationPanel.tsx @@ -35,6 +35,10 @@ const PALETTE_ITEMS: PaletteItem[] = [ type: "Sources", label: "Sources", }, + { + type: "Redundancy", + label: "Redundancy", + }, ]; function DraggablePaletteItem({ item }: { item: PaletteItem }) { diff --git a/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx b/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx index 9c3f6efec04e..7b0e8387b557 100644 --- a/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx +++ b/packages/interface/src/components/SpacesSidebar/SpaceSwitcher.tsx @@ -20,7 +20,7 @@ export function SpaceSwitcher({ return ( - +
void; + close: () => void; +} + +const ControllerContext = createContext(null); + +export function useWebContextMenuController(): WebContextMenuController | null { + return useContext(ControllerContext); +} + +export function WebContextMenuProvider({ children }: PropsWithChildren) { + const [state, setState] = useState(null); + + const show = useCallback( + (items: ContextMenuItem[], x: number, y: number) => { + // If a menu is already open, close it first so Radix re-anchors + // at the new cursor position on the next tick. + setState(null); + requestAnimationFrame(() => setState({ items, x, y })); + }, + [], + ); + + const close = useCallback(() => setState(null), []); + + return ( + + {children} + + + ); +} + +function WebContextMenu({ + state, + onClose, +}: { + state: MenuState | null; + onClose: () => void; +}) { + return ( + { + if (!open) onClose(); + }} + > + + + + + + {state && renderItems(state.items)} + + + + ); +} + +function renderItems(items: ContextMenuItem[]) { + return items.map((item, index) => { + const key = `${index}-${item.label ?? item.type ?? "item"}`; + + if (item.type === "separator") { + return ( + + ); + } + + if (item.submenu && item.submenu.length > 0) { + return ( + + + + + + + + {renderItems(item.submenu)} + + + + ); + } + + return ( + item.onClick?.()} + className={menuItemClasses(item)} + > + + + ); + }); +} + +function menuItemClasses(item: ContextMenuItem) { + const variant = item.variant ?? "default"; + return clsx( + "mx-1 flex items-center gap-2 rounded-md px-2 py-1 text-sm outline-none", + variant === "danger" && "text-status-error", + variant === "dull" && "text-menu-faint", + variant === "default" && "text-menu-ink", + item.disabled + ? "cursor-not-allowed opacity-50" + : "data-[highlighted]:bg-menu-hover cursor-pointer", + ); +} + +function MenuItemInner({ item }: { item: ContextMenuItem }) { + const Icon = item.icon; + return ( + <> + {Icon ? : null} + {item.label} + {item.keybind ? ( + + {item.keybind} + + ) : null} + + ); +} diff --git a/packages/interface/src/hooks/useContextMenu.ts b/packages/interface/src/hooks/useContextMenu.ts index 096cf0badbe4..532f85d2e630 100644 --- a/packages/interface/src/hooks/useContextMenu.ts +++ b/packages/interface/src/hooks/useContextMenu.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react'; import type { Icon } from '@phosphor-icons/react'; import { usePlatform } from '../contexts/PlatformContext'; +import { useWebContextMenuController } from '../contexts/WebContextMenuContext'; import type { KeybindId } from '../util/keybinds/registry'; import { getKeybind } from '../util/keybinds/registry'; import { getComboForPlatform, getCurrentPlatform, toDisplayString } from '../util/keybinds/platform'; @@ -50,6 +51,30 @@ function resolveKeybindDisplay(item: ContextMenuItem): string | undefined { return undefined; } +/** + * Drop leading/trailing separators and merge runs of adjacent separators into + * one. Condition-based filtering can leave orphaned separators behind; this + * keeps the rendered menu from looking broken. Recurses into submenus. + */ +function collapseSeparators(items: ContextMenuItem[]): ContextMenuItem[] { + const result: ContextMenuItem[] = []; + for (const item of items) { + if (item.type === 'separator') { + if (result.length === 0) continue; + if (result[result.length - 1].type === 'separator') continue; + result.push(item); + } else if (item.submenu) { + result.push({ ...item, submenu: collapseSeparators(item.submenu) }); + } else { + result.push(item); + } + } + while (result.length > 0 && result[result.length - 1].type === 'separator') { + result.pop(); + } + return result; +} + /** * Process menu items to resolve keybindId to display strings */ @@ -106,58 +131,43 @@ function processMenuItems(items: ContextMenuItem[]): ContextMenuItem[] { export function useContextMenu(config: ContextMenuConfig): ContextMenuResult { const [menuData, setMenuData] = useState(null); const platform = usePlatform(); + const webController = useWebContextMenuController(); const show = useCallback( async (e: React.MouseEvent) => { - console.log('[useContextMenu] show called', { x: e.clientX, y: e.clientY }); e.preventDefault(); e.stopPropagation(); - // Filter items by condition and process keybindIds const filteredItems = config.items.filter( (item) => !item.condition || item.condition() ); - const visibleItems = processMenuItems(filteredItems); - - console.log('[useContextMenu] visible items:', visibleItems.length); + const visibleItems = collapseSeparators(processMenuItems(filteredItems)); - // Check if running in Tauri const isTauri = platform.platform === 'tauri'; - console.log('[useContextMenu] isTauri:', isTauri); - - if (isTauri) { - // Native mode: Use Tauri's native menu API - console.log('[useContextMenu] Using Tauri native menu'); + const nativeShow = (window as any).__SPACEDRIVE__?.showContextMenu; + if (isTauri && nativeShow) { try { - // Call the platform-specific context menu handler - // This will be provided by the Tauri app wrapper - if ((window as any).__SPACEDRIVE__?.showContextMenu) { - await (window as any).__SPACEDRIVE__.showContextMenu(visibleItems, { - x: e.clientX, - y: e.clientY, - }); - } else { - console.warn('[useContextMenu] Tauri context menu handler not found, falling back to web mode'); - setMenuData(visibleItems); - } + await nativeShow(visibleItems, { x: e.clientX, y: e.clientY }); + return; } catch (err) { - console.error('[useContextMenu] Failed to show native context menu:', err); - // Fallback to web mode - setMenuData(visibleItems); + console.error('[useContextMenu] native menu failed, falling back to web', err); } + } + + if (webController) { + webController.show(visibleItems, e.clientX, e.clientY); } else { - // Web mode: Use Radix ContextMenu (trigger via state) - console.log('[useContextMenu] Using web mode (Radix)'); setMenuData(visibleItems); } }, - [config.items, platform] + [config.items, platform, webController] ); const closeMenu = useCallback(() => { setMenuData(null); - }, []); + webController?.close(); + }, [webController]); return { show, menuData, closeMenu }; } \ No newline at end of file diff --git a/packages/interface/src/router.tsx b/packages/interface/src/router.tsx index 938b9dfb6e1d..e8d328f4a116 100644 --- a/packages/interface/src/router.tsx +++ b/packages/interface/src/router.tsx @@ -1,6 +1,9 @@ import { createBrowserRouter, Navigate, Outlet } from "react-router-dom"; import { Overview } from "./routes/overview"; import { ExplorerView } from "./routes/explorer"; +import { RedundancyDashboard } from "./routes/redundancy"; +import { AtRiskFiles } from "./routes/redundancy/at-risk"; +import { CompareVolumes } from "./routes/redundancy/compare"; import { ShellLayout } from "./ShellLayout"; import { JobsScreen } from "./components/JobManager"; import { DaemonManager } from "./routes/daemon"; @@ -78,6 +81,23 @@ export const explorerRoutes = [ path: "sources/:sourceId", element: , }, + { + path: "redundancy", + children: [ + { + index: true, + element: , + }, + { + path: "at-risk", + element: , + }, + { + path: "compare", + element: , + }, + ], + }, { path: "search", element: ( diff --git a/packages/interface/src/routes/explorer/ExplorerView.tsx b/packages/interface/src/routes/explorer/ExplorerView.tsx index f44a1c2d6378..9176ba2f1d25 100644 --- a/packages/interface/src/routes/explorer/ExplorerView.tsx +++ b/packages/interface/src/routes/explorer/ExplorerView.tsx @@ -148,8 +148,9 @@ export function ExplorerView() { [sortBy, setSortBy, viewMode] ); - // Allow rendering if either we have a currentPath or we're in a virtual view - if (!currentPath && !isVirtualView) { + // Allow rendering if we have a currentPath, a virtual view, + // or we're in filtered mode (e.g. redundancy views). + if (!currentPath && !isVirtualView && mode.type !== 'filtered') { return ; } diff --git a/packages/interface/src/routes/explorer/context.tsx b/packages/interface/src/routes/explorer/context.tsx index 473ed7819b47..82428bb182e9 100644 --- a/packages/interface/src/routes/explorer/context.tsx +++ b/packages/interface/src/routes/explorer/context.tsx @@ -22,6 +22,7 @@ import type { ListLibraryDevicesInput, DirectorySortBy, MediaSortBy, + SearchFilters as ApiSearchFilters, } from "@sd/ts-client"; import { useViewPreferencesStore, @@ -61,7 +62,8 @@ export interface SearchFilters { export type ExplorerMode = | { type: "browse" } | { type: "search"; query: string; scope: SearchScope } - | { type: "recents" }; + | { type: "recents" } + | { type: "filtered"; filters: ApiSearchFilters; label: string }; export type NavigationTarget = | { type: "path"; path: SdPath } @@ -193,6 +195,8 @@ type UIAction = | { type: "EXIT_SEARCH_MODE" } | { type: "ENTER_RECENTS_MODE" } | { type: "EXIT_RECENTS_MODE" } + | { type: "ENTER_FILTERED_MODE"; filters: ApiSearchFilters; label: string } + | { type: "EXIT_FILTERED_MODE" } | { type: "SET_SEARCH_FILTERS"; filters: SearchFilters } | { type: "LOAD_PREFERENCES"; @@ -260,6 +264,22 @@ function uiReducer(state: UIState, action: UIAction): UIState { mode: { type: "browse" }, }; + case "ENTER_FILTERED_MODE": + return { + ...state, + mode: { + type: "filtered", + filters: action.filters, + label: action.label, + }, + }; + + case "EXIT_FILTERED_MODE": + return { + ...state, + mode: { type: "browse" }, + }; + case "SET_SEARCH_FILTERS": return { ...state, @@ -412,6 +432,8 @@ interface ExplorerContextValue { exitSearchMode: () => void; enterRecentsMode: () => void; exitRecentsMode: () => void; + enterFilteredMode: (filters: ApiSearchFilters, label: string) => void; + exitFilteredMode: () => void; searchFilters: SearchFilters; setSearchFilters: (filters: SearchFilters) => void; @@ -737,6 +759,17 @@ export function ExplorerProvider({ uiDispatch({ type: "EXIT_RECENTS_MODE" }); }, []); + const enterFilteredMode = useCallback( + (filters: ApiSearchFilters, label: string) => { + uiDispatch({ type: "ENTER_FILTERED_MODE", filters, label }); + }, + [], + ); + + const exitFilteredMode = useCallback(() => { + uiDispatch({ type: "EXIT_FILTERED_MODE" }); + }, []); + const setSearchFilters = useCallback((filters: SearchFilters) => { uiDispatch({ type: "SET_SEARCH_FILTERS", filters }); }, []); @@ -794,6 +827,8 @@ export function ExplorerProvider({ exitSearchMode, enterRecentsMode, exitRecentsMode, + enterFilteredMode, + exitFilteredMode, searchFilters: uiState.searchFilters, setSearchFilters, devices, @@ -837,6 +872,8 @@ export function ExplorerProvider({ exitSearchMode, enterRecentsMode, exitRecentsMode, + enterFilteredMode, + exitFilteredMode, uiState.searchFilters, setSearchFilters, devices, diff --git a/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts b/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts index 97da398b4405..3a83e9ef71ed 100644 --- a/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts +++ b/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts @@ -4,7 +4,12 @@ import { useNormalizedQuery } from "../../../contexts/SpacedriveContext"; import { useExplorer } from "../context"; import { useVirtualListing } from "./useVirtualListing"; -export type FileSource = "search" | "virtual" | "directory" | "recents"; +export type FileSource = + | "search" + | "virtual" + | "directory" + | "recents" + | "filtered"; export interface ExplorerFilesResult { files: File[]; @@ -30,6 +35,7 @@ export function useExplorerFiles(): ExplorerFilesResult { // Check for search mode const isSearchMode = mode.type === "search"; const isRecentsMode = mode.type === "recents"; + const isFilteredMode = mode.type === "filtered"; // Build search query input const searchQueryInput = useMemo(() => { @@ -67,6 +73,11 @@ export function useExplorerFiles(): ExplorerFilesResult { content_types: null, include_hidden: null, include_archived: null, + at_risk: null, + on_volumes: null, + not_on_volumes: null, + min_volume_count: null, + max_volume_count: null, }, mode: "Normal", sort: { @@ -80,6 +91,40 @@ export function useExplorerFiles(): ExplorerFilesResult { }; }, [isSearchMode, mode, currentPath, sortBy]); + // Build filtered query input (pre-applied SearchFilters, e.g. redundancy views) + const filteredQueryInput = useMemo(() => { + if (!isFilteredMode || mode.type !== "filtered") return null; + + const searchSortField = (() => { + if (!sortBy) return "Size" as const; + const sortMap: Record< + string, + "Relevance" | "Name" | "Size" | "ModifiedAt" | "CreatedAt" + > = { + name: "Name", + size: "Size", + modified: "ModifiedAt", + type: "Size", + }; + return sortMap[sortBy] || "Size"; + })(); + + return { + query: "", + scope: "Library", + filters: mode.filters, + mode: "Fast", + sort: { + field: searchSortField, + direction: "Desc", + }, + pagination: { + limit: 1000, + offset: 0, + }, + }; + }, [isFilteredMode, mode, sortBy]); + // Build recents query input const recentsQueryInput = useMemo(() => { if (!isRecentsMode) return null; @@ -96,6 +141,11 @@ export function useExplorerFiles(): ExplorerFilesResult { content_types: null, include_hidden: null, include_archived: null, + at_risk: null, + on_volumes: null, + not_on_volumes: null, + min_volume_count: null, + max_volume_count: null, }, mode: "Fast", // Fast mode since we're just sorting by indexed_at sort: { @@ -129,6 +179,14 @@ export function useExplorerFiles(): ExplorerFilesResult { enabled: isRecentsMode && !!recentsQueryInput, }); + // Filtered query (pre-applied SearchFilters) + const filteredQuery = useNormalizedQuery({ + query: "search.files", + input: filteredQueryInput!, + resourceType: "file", + enabled: isFilteredMode && !!filteredQueryInput, + }); + // Directory query const directoryQuery = useNormalizedQuery({ query: "files.directory_listing", @@ -142,20 +200,32 @@ export function useExplorerFiles(): ExplorerFilesResult { } : null!, resourceType: "file", - enabled: !!currentPath && !isVirtualView && !isSearchMode && !isRecentsMode, + enabled: + !!currentPath && + !isVirtualView && + !isSearchMode && + !isRecentsMode && + !isFilteredMode, pathScope: currentPath ?? undefined, }); - // Determine source and files with priority: recents > search > virtual > directory - const source: FileSource = isRecentsMode - ? "recents" - : isSearchMode - ? "search" - : isVirtualView - ? "virtual" - : "directory"; + // Priority: filtered > recents > search > virtual > directory + const source: FileSource = isFilteredMode + ? "filtered" + : isRecentsMode + ? "recents" + : isSearchMode + ? "search" + : isVirtualView + ? "virtual" + : "directory"; const files = useMemo(() => { + if (isFilteredMode) { + return ( + (filteredQuery.data as FileSearchOutput | undefined)?.files || [] + ); + } if (isRecentsMode) { return (recentsQuery.data as FileSearchOutput | undefined)?.files || []; } @@ -166,15 +236,27 @@ export function useExplorerFiles(): ExplorerFilesResult { return virtualFiles || []; } return (directoryQuery.data as any)?.files || []; - }, [isRecentsMode, isSearchMode, isVirtualView, recentsQuery.data, searchQuery.data, virtualFiles, directoryQuery.data]); - - const isLoading = isRecentsMode - ? recentsQuery.isLoading - : isSearchMode - ? searchQuery.isLoading - : isVirtualView - ? false - : directoryQuery.isLoading; + }, [ + isFilteredMode, + isRecentsMode, + isSearchMode, + isVirtualView, + filteredQuery.data, + recentsQuery.data, + searchQuery.data, + virtualFiles, + directoryQuery.data, + ]); + + const isLoading = isFilteredMode + ? filteredQuery.isLoading + : isRecentsMode + ? recentsQuery.isLoading + : isSearchMode + ? searchQuery.isLoading + : isVirtualView + ? false + : directoryQuery.isLoading; return { files, isLoading, source }; } diff --git a/packages/interface/src/routes/redundancy/at-risk.tsx b/packages/interface/src/routes/redundancy/at-risk.tsx new file mode 100644 index 000000000000..f8ab364ed075 --- /dev/null +++ b/packages/interface/src/routes/redundancy/at-risk.tsx @@ -0,0 +1,109 @@ +/** + * At-Risk / Redundant Files View + * + * Enters the Explorer into "filtered" mode with a redundancy SearchFilters + * payload, then delegates rendering to the real ExplorerView so users get + * the full file browser experience (view modes, selection, QuickPreview, + * context menus, drag-drop, sort, etc.). + */ + +import { useEffect, useMemo, useRef } from "react"; +import { useSearchParams, useNavigate } from "react-router-dom"; +import { ArrowLeft, ShieldWarning } from "@phosphor-icons/react"; +import { CircleButton } from "@spacedrive/primitives"; +import type { SearchFilters } from "@sd/ts-client"; +import { TopBarPortal, TopBarItem } from "../../TopBar"; +import { ExplorerView, useExplorer } from "../explorer"; + +const EMPTY_FILTERS: SearchFilters = { + file_types: null, + tags: null, + date_range: null, + size_range: null, + locations: null, + content_types: null, + include_hidden: null, + include_archived: null, + at_risk: null, + on_volumes: null, + not_on_volumes: null, + min_volume_count: null, + max_volume_count: null, +}; + +export function AtRiskFiles() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const volumeFilter = searchParams.get("volume"); + const atRiskParam = searchParams.get("at_risk"); + const isAtRisk = atRiskParam !== "false"; // default true + + const { enterFilteredMode, exitFilteredMode, setSortBy } = useExplorer(); + + const label = isAtRisk ? "At-Risk Files" : "Redundant Files"; + + const filters = useMemo( + () => ({ + ...EMPTY_FILTERS, + at_risk: isAtRisk, + on_volumes: volumeFilter ? [volumeFilter] : null, + }), + [isAtRisk, volumeFilter], + ); + + // Keep filtered mode in sync with current params + useEffect(() => { + enterFilteredMode(filters, label); + }, [enterFilteredMode, filters, label]); + + // Default sort to size (largest first) on mount — biggest risk/dupe first. + // Guarded with a ref because setSortBy's identity churns whenever the + // explorer context's derived deps update, which would otherwise loop. + const didSetSort = useRef(false); + useEffect(() => { + if (didSetSort.current) return; + didSetSort.current = true; + setSortBy("size"); + }, [setSortBy]); + + // Exit filtered mode only when the route unmounts + useEffect(() => { + return () => exitFilteredMode(); + }, [exitFilteredMode]); + + const titleItem = useMemo( + () => ( +
+ navigate("/redundancy")} + /> + + {label} +
+ ), + [navigate, label], + ); + + return ( + <> + + {titleItem} + + } + /> + + + ); +} diff --git a/packages/interface/src/routes/redundancy/compare.tsx b/packages/interface/src/routes/redundancy/compare.tsx new file mode 100644 index 000000000000..f1a508bbf9ba --- /dev/null +++ b/packages/interface/src/routes/redundancy/compare.tsx @@ -0,0 +1,260 @@ +/** + * Volume Comparison View + * + * Pick two volumes and see which files are unique to each or shared. + * Once both volumes are selected, switches the Explorer into filtered mode + * with the appropriate SearchFilters — the real ExplorerView renders the + * results so users keep selection, QuickPreview, context menus, drag-drop, etc. + */ + +import { useEffect, useMemo, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { ArrowLeft, ArrowsLeftRight, ShieldCheck } from "@phosphor-icons/react"; +import { CircleButton } from "@spacedrive/primitives"; +import type { SearchFilters } from "@sd/ts-client"; +import { TopBarPortal, TopBarItem } from "../../TopBar"; +import { useLibraryQuery } from "../../contexts/SpacedriveContext"; +import { ExplorerView, useExplorer } from "../explorer"; + +type CompareMode = "unique_a" | "shared" | "unique_b"; + +const EMPTY_FILTERS: SearchFilters = { + file_types: null, + tags: null, + date_range: null, + size_range: null, + locations: null, + content_types: null, + include_hidden: null, + include_archived: null, + at_risk: null, + on_volumes: null, + not_on_volumes: null, + min_volume_count: null, + max_volume_count: null, +}; + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; +} + +export function CompareVolumes() { + const navigate = useNavigate(); + const [volumeA, setVolumeA] = useState(null); + const [volumeB, setVolumeB] = useState(null); + const [mode, setMode] = useState("unique_a"); + + const { enterFilteredMode, exitFilteredMode, setSortBy } = useExplorer(); + + const { data: summaryData } = useLibraryQuery({ + type: "redundancy.summary", + input: {}, + }); + + const volumes = summaryData?.volumes ?? []; + + const volumeAName = + volumes.find((v) => v.volume_uuid === volumeA)?.display_name ?? + "Volume A"; + const volumeBName = + volumes.find((v) => v.volume_uuid === volumeB)?.display_name ?? + "Volume B"; + + const hasBoth = !!volumeA && !!volumeB; + + const filters = useMemo(() => { + if (!hasBoth) return null; + switch (mode) { + case "unique_a": + return { + ...EMPTY_FILTERS, + on_volumes: [volumeA!], + not_on_volumes: [volumeB!], + }; + case "unique_b": + return { + ...EMPTY_FILTERS, + on_volumes: [volumeB!], + not_on_volumes: [volumeA!], + }; + case "shared": + return { + ...EMPTY_FILTERS, + on_volumes: [volumeA!, volumeB!], + min_volume_count: 2, + }; + } + }, [hasBoth, mode, volumeA, volumeB]); + + const label = useMemo(() => { + if (!hasBoth) return "Compare Volumes"; + switch (mode) { + case "unique_a": + return `Unique to ${volumeAName}`; + case "unique_b": + return `Unique to ${volumeBName}`; + case "shared": + return `Shared between ${volumeAName} & ${volumeBName}`; + } + }, [hasBoth, mode, volumeAName, volumeBName]); + + // Sync filtered mode whenever selection changes (only when both selected) + useEffect(() => { + if (filters) { + enterFilteredMode(filters, label); + } else { + // Ensure we're not stuck in an old filtered state if user clears a picker + exitFilteredMode(); + } + }, [enterFilteredMode, exitFilteredMode, filters, label]); + + // Default sort to size (largest first) on mount. Guarded with a ref + // because setSortBy's identity churns when context deps update. + const didSetSort = useRef(false); + useEffect(() => { + if (didSetSort.current) return; + didSetSort.current = true; + setSortBy("size"); + }, [setSortBy]); + + // Exit filtered mode when leaving the route + useEffect(() => { + return () => exitFilteredMode(); + }, [exitFilteredMode]); + + const titleItem = useMemo( + () => ( +
+ navigate("/redundancy")} + /> + + {label} +
+ ), + [navigate, label], + ); + + return ( + <> + + {titleItem} + + } + /> + +
+ {/* Picker + mode toggle */} +
+
+ + + +
+ + {hasBoth && ( +
+ setMode("unique_a")} + label={`Unique to ${volumeAName}`} + /> + setMode("shared")} + label="Shared" + /> + setMode("unique_b")} + label={`Unique to ${volumeBName}`} + /> +
+ )} +
+ + {/* Results */} +
+ {!hasBoth ? ( +
+ + + Select two volumes to compare their contents + +
+ ) : ( + + )} +
+
+ + ); +} + +function ModeButton({ + active, + onClick, + label, +}: { + active: boolean; + onClick: () => void; + label: string; +}) { + return ( + + ); +} diff --git a/packages/interface/src/routes/redundancy/components/RedundancyVolumeBar.tsx b/packages/interface/src/routes/redundancy/components/RedundancyVolumeBar.tsx new file mode 100644 index 000000000000..80b3832d3720 --- /dev/null +++ b/packages/interface/src/routes/redundancy/components/RedundancyVolumeBar.tsx @@ -0,0 +1,128 @@ +import { motion } from "framer-motion"; +import { useNavigate } from "react-router-dom"; + +interface RedundancyVolumeBarProps { + volumeUuid: string; + displayName: string | null; + totalBytes: number; + atRiskBytes: number; + atRiskFileCount: number; + redundantBytes: number; + redundantFileCount: number; + totalFileCount: number; +} + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; +} + +export function RedundancyVolumeBar({ + volumeUuid, + displayName, + totalBytes, + atRiskBytes, + atRiskFileCount, + redundantBytes, + redundantFileCount, + totalFileCount, +}: RedundancyVolumeBarProps) { + const navigate = useNavigate(); + + const redundantPercent = + totalBytes > 0 ? (redundantBytes / totalBytes) * 100 : 0; + const atRiskPercent = + totalBytes > 0 ? (atRiskBytes / totalBytes) * 100 : 0; + // Remaining is unindexed content (no content_id) + const unindexedPercent = Math.max( + 0, + 100 - redundantPercent - atRiskPercent, + ); + + return ( +
+ {/* Header */} +
+ + {displayName || "Unknown Volume"} + + + {totalFileCount.toLocaleString()} files + +
+ + {/* Bar */} +
+ {/* Redundant segment (safe) */} + {redundantPercent > 0 && ( + + navigate( + `/redundancy/at-risk?volume=${volumeUuid}&at_risk=false`, + ) + } + /> + )} + {/* At-risk segment */} + {atRiskPercent > 0 && ( + + navigate( + `/redundancy/at-risk?volume=${volumeUuid}&at_risk=true`, + ) + } + /> + )} + {/* Unindexed segment */} + {unindexedPercent > 0 && ( + + )} +
+ + {/* Legend */} +
+
+
+ + Redundant ({formatBytes(redundantBytes)}) + +
+
+
+ + At Risk ({formatBytes(atRiskBytes)}) + +
+
+
+ ); +} diff --git a/packages/interface/src/routes/redundancy/index.tsx b/packages/interface/src/routes/redundancy/index.tsx new file mode 100644 index 000000000000..d041f51594b6 --- /dev/null +++ b/packages/interface/src/routes/redundancy/index.tsx @@ -0,0 +1,223 @@ +/** + * Redundancy Dashboard + * + * Shows library-wide replication score, per-volume redundancy bars, + * and an at-risk data callout. + */ + +import { useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { ShieldCheck, Warning, ArrowRight } from "@phosphor-icons/react"; +import { motion } from "framer-motion"; +import { TopBarPortal, TopBarItem } from "../../TopBar"; +import { useLibraryQuery } from "../../contexts/SpacedriveContext"; +import { RedundancyVolumeBar } from "./components/RedundancyVolumeBar"; + +function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; +} + +export function RedundancyDashboard() { + const navigate = useNavigate(); + + const { data, isLoading } = useLibraryQuery({ + type: "redundancy.summary", + input: {}, + }); + + const scorePercent = useMemo(() => { + if (!data) return 0; + return Math.round(data.library_totals.replication_score * 100); + }, [data]); + + const scoreColor = + scorePercent >= 75 + ? "text-status-success" + : scorePercent >= 40 + ? "text-status-warning" + : "text-status-error"; + + const topBarTitle = useMemo( + () => ( +
+ +

Redundancy

+
+ ), + [], + ); + + if (isLoading || !data) { + return ( + <> + + {topBarTitle} + + } + /> +
+ Loading redundancy data... +
+ + ); + } + + const { library_totals, volumes } = data; + const totalAtRiskFiles = volumes.reduce( + (sum, v) => sum + v.at_risk_file_count, + 0, + ); + + return ( + <> + + {topBarTitle} + + } + /> + +
+
+ {/* Replication Score + At-Risk Summary */} +
+ {/* Replication Score Card */} +
+
+ + {scorePercent}% + + + Replication Score + +
+
+
+ + {formatBytes( + library_totals.total_redundant_bytes, + )} + {" "} + safely replicated +
+
+ + {formatBytes( + library_totals.total_at_risk_bytes, + )} + {" "} + at risk (single copy) +
+
+ + {formatBytes( + library_totals.total_unique_content_bytes, + )} + {" "} + unique content total +
+
+
+ + {/* At-Risk Callout */} + +
+ + {/* Per-Volume Redundancy Bars */} +
+

+ Per-Volume Breakdown +

+
+ {volumes.length === 0 ? ( +
+ No volumes with indexed content found. + Index a volume to see redundancy data. +
+ ) : ( + volumes.map((vol) => ( + + )) + )} +
+
+ + {/* Quick Actions */} +
+ + +
+
+
+ + ); +} diff --git a/packages/ts-client/src/generated/types.ts b/packages/ts-client/src/generated/types.ts index cddc78b86f45..da0f3c92dc16 100644 --- a/packages/ts-client/src/generated/types.ts +++ b/packages/ts-client/src/generated/types.ts @@ -1468,7 +1468,7 @@ export type FileSystem = /** * Indicates which filters are available for a given search type */ -export type FilterKind = "FileTypes" | "DateRange" | "SizeRange" | "ContentTypes" | "Tags" | "Locations" | "Hidden" | "Archived"; +export type FilterKind = "FileTypes" | "DateRange" | "SizeRange" | "ContentTypes" | "Tags" | "Locations" | "Hidden" | "Archived" | "AtRisk" | "OnVolumes" | "NotOnVolumes" | "VolumeCount"; /** * Raw filesystem event kinds emitted by the watcher without DB resolution @@ -2091,7 +2091,11 @@ export type ItemType = /** * Specific archive data source */ -{ Source: { source_id: string } }; +{ Source: { source_id: string } } | +/** + * Redundancy awareness dashboard + */ +"Redundancy"; export type JobCancelInput = { job_id: string }; @@ -2420,6 +2424,27 @@ name: string; */ path: string }; +/** + * Library-wide redundancy totals + */ +export type LibraryRedundancyTotals = { +/** + * Total unique content bytes across the entire library (deduplicated) + */ +total_unique_content_bytes: number; +/** + * Content bytes that exist on only one volume + */ +total_at_risk_bytes: number; +/** + * Content bytes that exist on two or more volumes + */ +total_redundant_bytes: number; +/** + * Ratio of redundant to total content (0.0 = nothing replicated, 1.0 = everything replicated) + */ +replication_score: number }; + export type LibraryRenameInput = { library_id: string; new_name: string }; export type LibraryRenameOutput = { library_id: string; old_name: string; new_name: string }; @@ -3318,6 +3343,28 @@ enabled: boolean; */ regenerate: boolean }; +/** + * Input for the redundancy summary query + */ +export type RedundancySummaryInput = { +/** + * Optional: restrict summary to specific volumes. None = all volumes. + */ +volume_uuids?: string[] | null }; + +/** + * Complete redundancy summary for the library + */ +export type RedundancySummaryOutput = { +/** + * Per-volume redundancy breakdown + */ +volumes: VolumeRedundancySummary[]; +/** + * Library-wide totals + */ +library_totals: LibraryRedundancyTotals }; + export type RegenerateThumbnailInput = { /** * UUID of the entry to regenerate thumbnails for @@ -3560,7 +3607,28 @@ export type SearchFacets = { file_types: { [key in string]: number }; tags: { [k /** * Container for all structured filters */ -export type SearchFilters = { file_types: string[] | null; tags: TagFilter | null; date_range: DateRangeFilter | null; size_range: SizeRangeFilter | null; locations: string[] | null; content_types: ContentKind[] | null; include_hidden: boolean | null; include_archived: boolean | null }; +export type SearchFilters = { file_types: string[] | null; tags: TagFilter | null; date_range: DateRangeFilter | null; size_range: SizeRangeFilter | null; locations: string[] | null; content_types: ContentKind[] | null; include_hidden: boolean | null; include_archived: boolean | null; +/** + * Only return files that are at risk (true) or redundant (false). + * At risk = content exists on exactly one volume. + */ +at_risk: boolean | null; +/** + * Only return files whose content is present on these volumes + */ +on_volumes: string[] | null; +/** + * Only return files whose content is NOT present on these volumes + */ +not_on_volumes: string[] | null; +/** + * Minimum number of distinct volumes the content must exist on + */ +min_volume_count: number | null; +/** + * Maximum number of distinct volumes the content can exist on + */ +max_volume_count: number | null }; /** * Defines the search mode and performance characteristics @@ -4746,6 +4814,43 @@ export type VolumeListQueryInput = { */ filter?: VolumeFilter }; +/** + * Redundancy breakdown for a single volume + */ +export type VolumeRedundancySummary = { +/** + * Volume UUID + */ +volume_uuid: string; +/** + * Display name of the volume + */ +display_name: string | null; +/** + * Total bytes of file content on this volume (deduplicated within volume) + */ +total_bytes: number; +/** + * Bytes of content unique to this volume (at risk if volume is lost) + */ +at_risk_bytes: number; +/** + * Number of files whose content only exists on this volume + */ +at_risk_file_count: number; +/** + * Bytes of content that also exists on at least one other volume + */ +redundant_bytes: number; +/** + * Number of files whose content exists on other volumes too + */ +redundant_file_count: number; +/** + * Total number of files on this volume + */ +total_file_count: number }; + export type VolumeRefreshInput = { /** * Optional: Set to true to force recalculation even if recently calculated @@ -4995,6 +5100,7 @@ export type LibraryQuery = | { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } | { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } | { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput } + | { type: 'redundancy.summary'; input: RedundancySummaryInput; output: RedundancySummaryOutput } | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } | { type: 'sources.get'; input: GetSourceInput; output: SourceInfo } | { type: 'sources.list'; input: ListSourcesInput; output: [SourceInfo] } @@ -5126,6 +5232,7 @@ export const WIRE_METHODS = { 'locations.list': 'query:locations.list', 'locations.suggested': 'query:locations.suggested', 'locations.validate_path': 'query:locations.validate_path', + 'redundancy.summary': 'query:redundancy.summary', 'search.files': 'query:search.files', 'sources.get': 'query:sources.get', 'sources.list': 'query:sources.list',