From ccf421bc494c605a7e4616f99e52c873fa748689 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Mon, 12 Jan 2026 19:37:40 -0800 Subject: [PATCH] feat(types): generate TypeScript types for core and tauri applications This commit introduces auto-generated TypeScript types for both the core and Tauri applications, enhancing type safety and consistency across the codebase. The generated types include various data structures and interfaces used in the applications, facilitating better integration and development experience. The types are generated using Specta and are intended to be maintained automatically, ensuring they remain up-to-date with the underlying data models. --- .../packages/ts-client/src/generated/types.ts | 4599 +++++++++++++++++ .../packages/ts-client/src/generated/types.ts | 4599 +++++++++++++++++ core/src/device/manager.rs | 1 + core/src/domain/device.rs | 33 + core/src/domain/mod.rs | 2 +- core/src/lib.rs | 4 + core/src/ops/devices/list/query.rs | 127 +- core/src/ops/files/copy/metadata.rs | 12 +- core/src/service/network/core/event_loop.rs | 88 + core/src/service/network/core/mod.rs | 99 +- core/src/service/network/device/registry.rs | 134 +- .../JobManager/components/SpeedGraph.tsx | 20 +- .../JobManager/renderers/FileCopyRenderer.tsx | 40 +- .../src/routes/overview/DevicePanel.tsx | 457 +- 14 files changed, 9942 insertions(+), 273 deletions(-) create mode 100644 apps/tauri/packages/ts-client/src/generated/types.ts create mode 100644 core/packages/ts-client/src/generated/types.ts diff --git a/apps/tauri/packages/ts-client/src/generated/types.ts b/apps/tauri/packages/ts-client/src/generated/types.ts new file mode 100644 index 000000000..8b77de44c --- /dev/null +++ b/apps/tauri/packages/ts-client/src/generated/types.ts @@ -0,0 +1,4599 @@ +// Generated by Spacedrive using Specta + rspc-inspired type extraction - DO NOT EDIT +// This file is auto-generated. See core/src/bin/generate_typescript_types.rs + +// Empty type for operations with no input +export type Empty = Record; + +// This file has been generated by Specta. DO NOT EDIT. + +export type ActionContextInfo = { action_type: string; initiated_at: string; initiated_by: string | null; action_input: JsonValue; context: JsonValue }; + +export type ActiveJobItem = { id: string; name: string; status: JobStatus; progress: number; action_type: string | null; action_context: ActionContextInfo | null }; + +export type ActiveJobsInput = Record; + +export type ActiveJobsOutput = { jobs: ActiveJobItem[]; running_count: number; paused_count: number }; + +export type AddGroupInput = { space_id: string; name: string; group_type: GroupType }; + +export type AddGroupOutput = { group: SpaceGroup }; + +export type AddItemInput = { space_id: string; group_id: string | null; item_type: ItemType }; + +export type AddItemOutput = { item: SpaceItem }; + +/** + * Input for alternate instances query + */ +export type AlternateInstancesInput = { +/** + * The entry UUID to find alternates for + */ +entry_uuid: string }; + +/** + * Output containing alternate instances + */ +export type AlternateInstancesOutput = { +/** + * All instances of this file (including the original) + */ +instances: File[]; +/** + * Total number of instances found + */ +total_count: number }; + +/** + * Represents an APFS container (physical storage with multiple volumes) + */ +export type ApfsContainer = { container_id: string; uuid: string; physical_store: string; total_capacity: number; capacity_in_use: number; capacity_free: number; volumes: ApfsVolumeInfo[] }; + +/** + * APFS volume information within a container + */ +export type ApfsVolumeInfo = { disk_id: string; uuid: string; role: ApfsVolumeRole; name: string; mount_point: string | null; snapshot_mount_point: string | null; capacity_consumed: number; sealed: boolean; filevault: boolean }; + +/** + * APFS volume roles in the container + */ +export type ApfsVolumeRole = "System" | "Data" | "Preboot" | "Recovery" | "VM" | { Other: string }; + +export type ApplyTagsInput = { +/** + * What to tag: content identities or specific entries + */ +targets: TagTargets; +/** + * Tag IDs to apply + */ +tag_ids: string[]; +/** + * Source of the tag application + */ +source: TagSource | null; +/** + * Confidence score (for AI-applied tags) + */ +confidence: number | null; +/** + * Context when applying (e.g., "image_analysis", "user_input") + */ +applied_context: string | null; +/** + * Instance-specific attributes for this application + */ +instance_attributes: { [key in string]: JsonValue } | null }; + +export type ApplyTagsOutput = { +/** + * Number of entries that had tags applied + */ +entries_affected: number; +/** + * Number of tags that were applied + */ +tags_applied: number; +/** + * Tag IDs that were successfully applied + */ +applied_tag_ids: string[]; +/** + * Entry IDs that were successfully tagged + */ +tagged_entry_ids: number[]; +/** + * Any warnings or notes about the operation + */ +warnings: string[]; +/** + * Success message + */ +message: string }; + +/** + * Targets for immediately applying a newly created tag + */ +export type ApplyToTargets = +/** + * Apply to content identities (all instances) + */ +{ type: "Content"; ids: string[] } | +/** + * Apply to specific entries (single instance) + */ +{ type: "Entry"; ids: number[] }; + +/** + * Audio metadata extracted from FFmpeg + */ +export type AudioMediaData = { uuid: string; duration_seconds: number | null; bit_rate: number | null; sample_rate: number | null; channels: string | null; codec: string | null; title: string | null; artist: string | null; album: string | null; album_artist: string | null; genre: string | null; year: number | null; track_number: number | null; disc_number: number | null; composer: string | null; publisher: string | null; copyright: string | null }; + +/** + * Cloud service type identifier + */ +export type CloudServiceType = "s3" | "gdrive" | "dropbox" | "onedrive" | "gcs" | "azblob" | "b2" | "wasabi" | "spaces" | "cloud"; + +export type CloudStorageConfig = { type: "S3"; bucket: string; region: string; access_key_id: string; secret_access_key: string; endpoint: string | null } | +/** + * Google Drive with OAuth 2.0 credentials. + * Requires both access_token and refresh_token for automatic token renewal. + */ +{ type: "GoogleDrive"; root: string | null; access_token: string; refresh_token: string; client_id: string; client_secret: string } | +/** + * OneDrive with OAuth 2.0 credentials. + * Requires both access_token and refresh_token for automatic token renewal. + */ +{ type: "OneDrive"; root: string | null; access_token: string; refresh_token: string; client_id: string; client_secret: string } | +/** + * Dropbox with OAuth 2.0 refresh token for long-term access. + * OpenDAL automatically obtains and refreshes access tokens as needed. + * Only refresh_token is required (not access_token). + */ +{ type: "Dropbox"; root: string | null; refresh_token: string; client_id: string; client_secret: string } | { type: "AzureBlob"; container: string; endpoint: string | null; account_name: string; account_key: string } | { type: "GoogleCloudStorage"; bucket: string; root: string | null; endpoint: string | null; credential: string }; + +/** + * Operators for combining tag attributes + */ +export type CompositionOperator = +/** + * All conditions must be true + */ +"And" | +/** + * Any condition must be true + */ +"Or" | +/** + * Must have this property + */ +"With" | +/** + * Must not have this property + */ +"Without"; + +/** + * Rules for composing attributes from multiple tags + */ +export type CompositionRule = { operator: CompositionOperator; operands: string[]; result_attribute: string }; + +/** + * Network connection method for a device + */ +export type ConnectionMethod = +/** + * Direct peer-to-peer connection (mDNS/local network) + */ +"Direct" | +/** + * Connection via relay server + */ +"Relay" | +/** + * Mixed connection (both direct and relay) + */ +"Mixed"; + +/** + * Domain representation of content identity + */ +export type ContentIdentity = { uuid: string; kind: ContentKind; content_hash: string; integrity_hash: string | null; mime_type_id: number | null; text_content: string | null; total_size: number; entry_count: number; first_seen_at: string; last_verified_at: string }; + +/** + * Type of content + */ +export type ContentKind = "unknown" | "image" | "video" | "audio" | "document" | "archive" | "code" | "text" | "database" | "book" | "font" | "mesh" | "config" | "encrypted" | "key" | "executable" | "binary" | "spreadsheet" | "presentation" | "email" | "calendar" | "contact" | "web" | "shortcut" | "package" | "model_entry" | "memory"; + +/** + * A single content kind with its file count + */ +export type ContentKindStat = { +/** + * The content kind (image, video, audio, etc.) + */ +kind: ContentKind; +/** + * The name of the content kind + */ +name: string; +/** + * The number of files with this content kind + */ +file_count: number }; + +/** + * Input for content kind statistics query + */ +export type ContentKindStatsInput = Record; + +/** + * Output containing content kind statistics + */ +export type ContentKindStatsOutput = { +/** + * Statistics for each content kind + */ +stats: ContentKindStat[]; +/** + * Total number of files across all content kinds + */ +total_files: number }; + +/** + * Metadata for a single file or directory in the copy operation. + * For directories, this represents the entire directory (not flattened). + */ +export type CopyFileEntry = { +/** + * Source path + */ +source_path: SdPath; +/** + * Destination path + */ +dest_path: SdPath; +/** + * Total size in bytes (for directories, this is the recursive total) + */ +size_bytes: number; +/** + * Whether this entry is a directory + */ +is_directory: boolean; +/** + * Current status of this file/directory + */ +status: CopyFileStatus; +/** + * Error message if status is Failed + */ +error: string | null; +/** + * Entry UUID if source is in database (for building File objects) + */ +entry_id: string | null }; + +/** + * Status of a file in the copy operation. + */ +export type CopyFileStatus = +/** + * File is waiting to be copied + */ +"pending" | +/** + * File is currently being copied + */ +"copying" | +/** + * File has been successfully copied + */ +"completed" | +/** + * File copy failed + */ +"failed" | +/** + * File was skipped (already exists or user choice) + */ +"skipped"; + +/** + * Full metadata for a copy job, queryable via jobs.get_copy_metadata. + */ +export type CopyJobMetadata = { +/** + * Strategy metadata (name, description, flags) + */ +strategy: CopyStrategyMetadata | null; +/** + * List of files/directories being copied + */ +files: CopyFileEntry[]; +/** + * Total bytes across all files + */ +total_bytes: number; +/** + * Total file count (actual files, not directories) + */ +total_file_count: number; +/** + * Whether this is a move operation + */ +is_move_operation: boolean; +/** + * Full File domain objects (populated by query, not stored in job) + */ +file_objects?: File[] }; + +/** + * Output from the copy metadata query. + */ +export type CopyMetadataOutput = { +/** + * The copy job metadata, if the job exists and is a copy job + */ +metadata: CopyJobMetadata | null; +/** + * Error message if the job is not a copy job or doesn't have metadata + */ +error: string | null }; + +/** + * Input for the copy metadata query. + */ +export type CopyMetadataQueryInput = { +/** + * The job ID to query metadata for + */ +job_id: string }; + +/** + * Copy method preference for file operations + */ +export type CopyMethod = +/** + * Automatically select the best method based on source and destination + */ +"Auto" | +/** + * Use atomic operations (rename for moves, APFS clone for copies, etc.) + */ +"Atomic" | +/** + * Use streaming copy/move (works across all scenarios) + */ +"Streaming"; + +/** + * Metadata about the selected copy strategy for UI display. + */ +export type CopyStrategyMetadata = { +/** + * Internal strategy name (e.g., "LocalMove", "FastCopy", "LocalStream", "RemoteTransfer") + */ +strategy_name: string; +/** + * Human-readable description (e.g., "Atomic move (same storage)") + */ +strategy_description: string; +/** + * Whether operation crosses device boundaries + */ +is_cross_device: boolean; +/** + * Whether operation crosses volume/partition boundaries on same device + */ +is_cross_volume: boolean; +/** + * Whether this is expected to be a fast operation (instant or near-instant) + */ +is_fast_operation: boolean; +/** + * The copy method used (Auto, Atomic, Streaming) + */ +copy_method: CopyMethod }; + +export type CoreStatus = { version: string; built_at: string; library_count: number; device_info: DeviceInfo; libraries: LibraryInfo[]; services: ServiceStatus; network: NetworkStatus; system: SystemInfo }; + +/** + * Input for creating a new folder + */ +export type CreateFolderInput = { +/** + * Parent directory where the folder will be created + */ +parent: SdPath; +/** + * Name for the new folder + */ +name: string; +/** + * Optional items to move into the new folder after creation + */ +items?: SdPath[] }; + +/** + * Output from creating a folder + */ +export type CreateFolderOutput = { +/** + * Path to the created folder + */ +folder_path: SdPath; +/** + * Job receipt if items were moved into the folder + */ +job_receipt?: JobReceipt | null }; + +export type CreateTagInput = { +/** + * The canonical name for this tag + */ +canonical_name: string; +/** + * Optional display name (if different from canonical) + */ +display_name: string | null; +/** + * Semantic variants + */ +formal_name: string | null; abbreviation: string | null; aliases: string[]; +/** + * Context and categorization + */ +namespace: string | null; tag_type: TagType | null; +/** + * Visual properties + */ +color: string | null; icon: string | null; description: string | null; +/** + * Advanced capabilities + */ +is_organizational_anchor: boolean | null; privacy_level: PrivacyLevel | null; search_weight: number | null; +/** + * Initial attributes + */ +attributes: { [key in string]: JsonValue } | null; +/** + * Optional: Targets to immediately apply this tag to after creation + */ +apply_to: ApplyToTargets | null }; + +export type CreateTagOutput = { +/** + * The created tag's UUID + */ +tag_id: string; +/** + * The canonical name of the created tag + */ +canonical_name: string; +/** + * The namespace if specified + */ +namespace: string | null; +/** + * Success message + */ +message: string }; + +/** + * Data volume metrics snapshot + */ +export type DataVolumeSnapshot = { entries_synced: { [key in string]: number }; entries_by_device: { [key in string]: DeviceMetricsSnapshot }; bytes_sent: number; bytes_received: number; last_sync_per_peer: { [key in string]: string }; last_sync_per_model: { [key in string]: string } }; + +/** + * Time-based fields that can be filtered + */ +export type DateField = "CreatedAt" | "ModifiedAt" | "AccessedAt"; + +/** + * Filter for a time-based field + */ +export type DateRangeFilter = { field: DateField; start: string | null; end: string | null }; + +export type DeleteGroupInput = { group_id: string }; + +export type DeleteGroupOutput = { success: boolean }; + +export type DeleteItemInput = { item_id: string }; + +export type DeleteItemOutput = { success: boolean }; + +export type DeleteWhisperModelInput = { model: string }; + +export type DeleteWhisperModelOutput = { deleted: boolean }; + +/** + * A device running Spacedrive + * + * This is the canonical device type used throughout the application. + * It represents both database-registered devices and network-paired devices. + */ +export type Device = { +/** + * Unique identifier for this device + */ +id: string; +/** + * Human-readable name + */ +name: string; +/** + * Unique slug for URI addressing (e.g., "jamies-macbook") + */ +slug: string; +/** + * Operating system + */ +os: OperatingSystem; +/** + * Operating system version + */ +os_version: string | null; +/** + * Hardware model (e.g., "MacBook Pro", "iPhone 15") + */ +hardware_model: string | null; +/** + * CPU model name (e.g., "Apple M3 Max", "Intel Core i9-13900K") + */ +cpu_model: string | null; +/** + * CPU architecture (e.g., "arm64", "x86_64") + */ +cpu_architecture: string | null; +/** + * Number of physical CPU cores + */ +cpu_cores_physical: number | null; +/** + * Number of logical CPU cores (with hyperthreading) + */ +cpu_cores_logical: number | null; +/** + * CPU base frequency in MHz + */ +cpu_frequency_mhz: number | null; +/** + * Total system memory in bytes + */ +memory_total_bytes: number | null; +/** + * Device form factor + */ +form_factor: DeviceFormFactor | null; +/** + * Device manufacturer (e.g., "Apple", "Dell", "Lenovo") + */ +manufacturer: string | null; +/** + * GPU model names (can have multiple GPUs) + */ +gpu_models: string[] | null; +/** + * Boot disk type (e.g., "SSD", "HDD", "NVMe") + */ +boot_disk_type: string | null; +/** + * Boot disk capacity in bytes + */ +boot_disk_capacity_bytes: number | null; +/** + * Total swap space in bytes + */ +swap_total_bytes: number | null; +/** + * Network addresses for P2P connections + */ +network_addresses: string[]; +/** + * Device capabilities (indexing, P2P, volume detection, etc.) + */ +capabilities: JsonValue; +/** + * Whether this device is currently online + */ +is_online: boolean; +/** + * Last time this device was seen + */ +last_seen_at: string; +/** + * Whether sync is enabled for this device + */ +sync_enabled: boolean; +/** + * Last time this device synced + */ +last_sync_at: string | null; +/** + * When this device was first added + */ +created_at: string; +/** + * When this device info was last updated + */ +updated_at: string; +/** + * Whether this is the current device (computed) + */ +is_current?: boolean; +/** + * Whether this device is paired via network but not in library DB + */ +is_paired?: boolean; +/** + * Whether this device is currently connected via network + */ +is_connected?: boolean; +/** + * Connection method when connected (Direct, Relay, or Mixed) + */ +connection_method?: ConnectionMethod | null }; + +/** + * Device form factor types + */ +export type DeviceFormFactor = "Desktop" | "Laptop" | "Mobile" | "Tablet" | "Server" | "Other"; + +export type DeviceInfo = { id: string; name: string; os: string; hardware_model: string | null; created_at: string }; + +/** + * Device metrics snapshot + */ +export type DeviceMetricsSnapshot = { device_id: string; device_name: string; entries_received: number; last_seen: string; is_online: boolean }; + +export type DeviceRevokeInput = { device_id: string; +/** + * Whether to also remove the device from all library databases + * + * If false (default), only unpairs from network but keeps device history in libraries. + * If true, completely removes device from libraries (deletes all records). + */ +remove_from_library?: boolean }; + +export type DeviceRevokeOutput = { revoked: boolean }; + +/** + * Device sync state for state machine + */ +export type DeviceSyncState = +/** + * Not yet synced, no backfill started + */ +"Uninitialized" | +/** + * Currently backfilling from peer(s) + * Buffers all live updates during this phase + */ +{ Backfilling: { peer: string; progress: number } } | +/** + * Backfill complete, processing buffered updates + * Still buffers new updates while catching up + */ +{ CatchingUp: { buffered_count: number } } | +/** + * Fully synced, applying live updates immediately + */ +"Ready" | +/** + * Sync paused (offline or user disabled) + */ +"Paused"; + +/** + * Input for directory listing + */ +export type DirectoryListingInput = { +/** + * The directory path to list contents for + */ +path: SdPath; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Whether to include hidden files (default: false) + */ +include_hidden: boolean | null; +/** + * Sort order for results + */ +sort_by: DirectorySortBy; +/** + * Whether to show folders before files (default: false) + */ +folders_first: boolean | null }; + +/** + * Output containing directory contents + */ +export type DirectoryListingOutput = { +/** + * Direct children of the directory as File objects + */ +files: File[]; +/** + * Total count of direct children + */ +total_count: number; +/** + * Whether this directory has more children than returned + */ +has_more: boolean }; + +/** + * Sort options for directory listing + */ +export type DirectorySortBy = +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by size (largest first) + */ +"size" | +/** + * Sort by type (directories first, then files) + */ +"type"; + +export type DiscoverRemoteLibrariesInput = { +/** + * Device ID to query for libraries + */ +deviceId: string }; + +/** + * Output from discovering remote libraries + */ +export type DiscoverRemoteLibrariesOutput = { +/** + * Remote device ID that was queried + */ +deviceId: string; +/** + * Remote device name + */ +deviceName: string; +/** + * List of libraries available on the remote device + */ +libraries: RemoteLibraryInfo[]; +/** + * Whether the device is currently online + */ +isOnline: boolean }; + +/** + * Disk type classification + */ +export type DiskType = +/** + * Solid State Drive + */ +"SSD" | +/** + * Hard Disk Drive + */ +"HDD" | +/** + * Network storage + */ +"Network" | +/** + * Virtual/RAM disk + */ +"Virtual" | +/** + * Unknown type + */ +"Unknown"; + +export type DownloadWhisperModelInput = { +/** + * Model size: "tiny", "base", "small", "medium", "large" + */ +model: string }; + +export type DownloadWhisperModelOutput = { +/** + * Job ID for tracking download progress + */ +job_id: string }; + +export type EnableIndexingInput = { +/** + * UUID of the location to enable indexing for + */ +id: string; +/** + * Index mode to use (defaults to Deep if not specified) + */ +index_mode?: string }; + +export type EnableIndexingOutput = { +/** + * UUID of the location that had indexing enabled + */ +location_id: string; +/** + * Job ID of the indexing job that was started + */ +job_id: string }; + +/** + * Type of filesystem entry + */ +export type EntryKind = +/** + * Regular file + */ +"File" | +/** + * Directory + */ +"Directory" | +/** + * Symbolic link + */ +"Symlink"; + +/** + * Input for resetting the ephemeral cache + */ +export type EphemeralCacheResetInput = { +/** + * Confirmation flag to prevent accidental cache clearing + */ +confirm: boolean }; + +/** + * Output from resetting the ephemeral cache + */ +export type EphemeralCacheResetOutput = { +/** + * Number of paths that were cleared from the cache + */ +cleared_paths: number; +/** + * Message describing the result + */ +message: string }; + +/** + * Status of the unified ephemeral index cache + */ +export type EphemeralCacheStatus = { +/** + * Number of paths that have been indexed + */ +indexed_paths_count: number; +/** + * Number of paths currently being indexed + */ +indexing_in_progress_count: number; +/** + * Unified index statistics (shared arena and string interning) + */ +index_stats: UnifiedIndexStats; +/** + * List of indexed paths (directories whose contents are ready) + */ +indexed_paths: IndexedPathInfo[]; +/** + * List of paths currently being indexed + */ +paths_in_progress: string[]; total_indexes?: number | null; indexing_in_progress?: number | null; indexes?: EphemeralIndexInfo[] }; + +/** + * Input for the ephemeral cache status query + */ +export type EphemeralCacheStatusInput = { +/** + * Optional: only include indexed paths containing this substring + */ +path_filter?: string | null }; + +/** + * Legacy: Information about a single ephemeral index (for backward compatibility) + */ +export type EphemeralIndexInfo = { +/** + * Root path this index covers + */ +root_path: string; +/** + * Whether indexing is currently in progress + */ +indexing_in_progress: boolean; +/** + * Total entries in the arena + */ +total_entries: number; +/** + * Number of entries indexed by path + */ +path_index_count: number; +/** + * Number of unique interned names + */ +unique_names: number; +/** + * Number of interned strings in cache + */ +interned_strings: number; +/** + * Number of content kinds stored + */ +content_kinds: number; +/** + * Estimated memory usage in bytes + */ +memory_bytes: number; +/** + * Age of the index in seconds + */ +age_seconds: number; +/** + * Seconds since last access + */ +idle_seconds: number; +/** + * Indexer job statistics (files/dirs/bytes counted) + */ +job_stats: JobStats }; + +/** + * Error event for tracking recent errors + */ +export type ErrorEvent = { timestamp: string; error_type: string; message: string; model_type: string | null; device_id: string | null }; + +/** + * Error metrics snapshot + */ +export type ErrorSnapshot = { total_errors: number; network_errors: number; database_errors: number; apply_errors: number; validation_errors: number; recent_errors: ErrorEvent[]; conflicts_detected: number; conflicts_resolved_by_hlc: number }; + +/** + * A central event type that represents all events that can be emitted throughout the system + */ +export type Event = "CoreStarted" | "CoreShutdown" | { LibraryCreated: { id: string; name: string; path: string; +/** + * How the library was created (manual, sync, cloud import) + */ +source?: LibraryCreationSource } } | { LibraryOpened: { id: string; name: string; path: string } } | { LibraryClosed: { id: string; name: string } } | { LibraryDeleted: { id: string; name: string; deleted_data: boolean } } | { LibraryLoadFailed: { +/** + * Library ID if config was readable, None otherwise + */ +id: string | null; +/** + * Path to the library directory + */ +path: string; +/** + * Human-readable error message + */ +error: string; +/** + * Error type for frontend categorization (e.g., "DatabaseError", "ConfigError") + */ +error_type: string } } | { LibraryStatisticsUpdated: { library_id: string; statistics: LibraryStatistics } } | +/** + * Refresh event - signals that all frontend caches should be invalidated + * Emitted after major data recalculations (e.g., volume unique_bytes refresh) + */ +"Refresh" | { EntryCreated: { library_id: string; entry_id: string } } | { EntryModified: { library_id: string; entry_id: string } } | { EntryDeleted: { library_id: string; entry_id: string } } | { EntryMoved: { library_id: string; entry_id: string; old_path: string; new_path: string } } | { FsRawChange: { library_id: string; kind: FsRawEventKind } } | { VolumeAdded: Volume } | { VolumeRemoved: { fingerprint: VolumeFingerprint } } | { VolumeUpdated: { fingerprint: VolumeFingerprint; old_info: VolumeInfo; new_info: VolumeInfo } } | { VolumeSpeedTested: { fingerprint: VolumeFingerprint; read_speed_mbps: number; write_speed_mbps: number } } | { VolumeMountChanged: { fingerprint: VolumeFingerprint; is_mounted: boolean } } | { VolumeError: { fingerprint: VolumeFingerprint; error: string } } | { JobQueued: { job_id: string; job_type: string; device_id: string } } | { JobStarted: { job_id: string; job_type: string; device_id: string } } | { JobProgress: { job_id: string; job_type: string; device_id: string; progress: number; message: string | null; generic_progress: GenericProgress | null } } | { JobCompleted: { job_id: string; job_type: string; device_id: string; output: JobOutput } } | { JobFailed: { job_id: string; job_type: string; device_id: string; error: string } } | { JobCancelled: { job_id: string; job_type: string; device_id: string } } | { JobPaused: { job_id: string; device_id: string } } | { JobResumed: { job_id: string; device_id: string } } | { IndexingStarted: { location_id: string } } | { IndexingProgress: { location_id: string; processed: number; total: number | null } } | { IndexingCompleted: { location_id: string; total_files: number; total_dirs: number } } | { IndexingFailed: { location_id: string; error: string } } | { DeviceConnected: { device_id: string; device_name: string } } | { DeviceDisconnected: { device_id: string } } | { SyncStateChanged: { library_id: string; previous_state: string; new_state: string; timestamp: string } } | { SyncActivity: { library_id: string; peer_device_id: string; activity_type: SyncActivityType; model_type: string | null; count: number; timestamp: string } } | { SyncConnectionChanged: { library_id: string; peer_device_id: string; peer_name: string; connected: boolean; timestamp: string } } | { SyncError: { library_id: string; peer_device_id: string | null; error_type: string; message: string; timestamp: string } } | { ResourceChanged: { +/** + * Resource type identifier (e.g., "location", "tag", "album") + */ +resource_type: string; +/** + * The full resource data as JSON + */ +resource: JsonValue; +/** + * Metadata for proper cache updates + */ +metadata?: ResourceMetadata | null } } | { ResourceChangedBatch: { +/** + * Resource type identifier (e.g., "file") + */ +resource_type: string; +/** + * Array of full resource data as JSON + * Used for batch updates during indexing to reduce event overhead + */ +resources: JsonValue; +/** + * Metadata for proper cache updates + */ +metadata?: ResourceMetadata | null } } | { ResourceDeleted: { +/** + * Resource type identifier + */ +resource_type: string; +/** + * The deleted resource's ID + */ +resource_id: string } } | { LocationAdded: { library_id: string; location_id: string; path: string } } | { LocationRemoved: { library_id: string; location_id: string } } | { FilesIndexed: { library_id: string; location_id: string; count: number } } | { ThumbnailsGenerated: { library_id: string; count: number } } | { FileOperationCompleted: { library_id: string; operation: FileOperation; affected_files: number } } | { FilesModified: { library_id: string; paths: string[] } } | { Custom: { event_type: string } }; + +/** + * Event category for grouping related events + */ +export type EventCategory = +/** + * State machine lifecycle events + */ +"lifecycle" | +/** + * Data synchronization flow + */ +"data_flow" | +/** + * Network communication + */ +"network" | +/** + * Errors and failures + */ +"error"; + +export type EventInfo = { +/** + * The event variant name (e.g., "JobProgress", "LibraryCreated") + */ +variant: string; +/** + * Whether this event is considered "noisy" (high frequency, should be excluded by default) + */ +is_noisy: boolean; +/** + * Human-readable description + */ +description: string }; + +/** + * Event severity level + */ +export type EventSeverity = +/** + * Debug-level information + */ +"debug" | +/** + * Informational event + */ +"info" | +/** + * Warning condition + */ +"warning" | +/** + * Error condition + */ +"error"; + +/** + * Statistics about what was exported + */ +export type ExportStats = { entries: number; content_identities: number; user_metadata: number; tags: number; media_data: number }; + +export type ExtractTextInput = { +/** + * UUID of the entry to extract text from + */ +entry_uuid: string; +/** + * Languages to use for OCR (e.g., ["eng", "spa"]) + */ +languages: string[] | null; +/** + * Force re-extraction even if text exists + */ +force: boolean }; + +export type ExtractTextOutput = { +/** + * Job ID for tracking OCR progress + */ +job_id: string }; + +/** + * Represents a file within the Spacedrive VDFS. + * + * This is a computed domain model that aggregates data from Entry, ContentIdentity, + * Tags, and Sidecars. It provides a rich, developer-friendly interface without + * duplicating data in the database. + */ +export type File = { +/** + * The unique identifier of the file entry + */ +id: string; +/** + * The universal path to the file in Spacedrive's VDFS + */ +sd_path: SdPath; +/** + * The file kind (file, directory, symlink) + */ +kind: EntryKind; +/** + * The name of the file, including the extension + */ +name: string; +/** + * The file extension (without dot) + */ +extension: string | null; +/** + * The size of the file in bytes + */ +size: number; +/** + * Information about the file's content, including its content hash + */ +content_identity: ContentIdentity | null; +/** + * A list of other paths that share the same content identity + */ +alternate_paths: SdPath[]; +/** + * The semantic tags associated with this file + */ +tags: Tag[]; +/** + * A list of sidecars associated with this file + */ +sidecars: Sidecar[]; +/** + * Media-specific metadata (extracted from EXIF/FFmpeg) + */ +image_media_data: ImageMediaData | null; video_media_data: VideoMediaData | null; audio_media_data: AudioMediaData | null; +/** + * Timestamps for creation, modification, and access + */ +created_at: string; modified_at: string; accessed_at: string | null; +/** + * Additional computed fields + */ +content_kind: ContentKind; is_local: boolean; +/** + * Video duration (for grid display optimization) + */ +duration_seconds: number | null }; + +/** + * Query to get a file by its ID with all related data + */ +export type FileByIdQuery = { file_id: string }; + +/** + * Query to get a file by its local path with all related data + */ +export type FileByPathQuery = { path: string }; + +/** + * Internal enum for file conflict resolution strategies + */ +export type FileConflictResolution = "Overwrite" | "AutoModifyName" | "Skip" | "Abort"; + +/** + * Core input structure for file copy operations + * This is the canonical interface that all external APIs (CLI, REST) convert to + */ +export type FileCopyInput = { +/** + * Source files or directories to copy (domain addressing) + */ +sources: SdPathBatch; +/** + * Destination path (domain addressing) + */ +destination: SdPath; +/** + * Whether to overwrite existing files + */ +overwrite: boolean; +/** + * Whether to verify checksums during copy + */ +verify_checksum: boolean; +/** + * Whether to preserve file timestamps + */ +preserve_timestamps: boolean; +/** + * Whether to delete source files after copying (move operation) + */ +move_files: boolean; +/** + * Preferred copy method to use + */ +copy_method: CopyMethod; +/** + * How to handle file conflicts (set by CLI confirmation) + */ +on_conflict: FileConflictResolution | null }; + +/** + * Input for deleting files + */ +export type FileDeleteInput = { +/** + * Files or directories to delete + */ +targets: SdPathBatch; +/** + * Whether to permanently delete (true) or move to trash (false) + */ +permanent: boolean; +/** + * Whether to delete directories recursively + */ +recursive: boolean }; + +/** + * Types of file operations + */ +export type FileOperation = "Copy" | "Move" | "Delete" | "Rename"; + +/** + * Input for renaming a file or directory + */ +export type FileRenameInput = { +/** + * The file or directory to rename + */ +target: SdPath; +/** + * The new name (filename only, no path separators) + */ +new_name: string }; + +/** + * Main input structure for file search operations + */ +export type FileSearchInput = { +/** + * Primary search query (filename, content, or natural language) + */ +query: string; +/** + * Search scope (library, location, or specific path) + */ +scope: SearchScope; +/** + * Search mode (fast, normal, full) + */ +mode: SearchMode; +/** + * Filters to narrow results + */ +filters: SearchFilters; +/** + * Sorting options + */ +sort: SortOptions; +/** + * Pagination + */ +pagination: PaginationOptions }; + +/** + * Main output structure for file search operations + */ +export type FileSearchOutput = { +/** + * Flat file array matching DirectoryListingOutput - primary field for explorer + */ +files: File[]; +/** + * Search results with scoring metadata - use for search-specific UI (scores, highlights) + */ +results: FileSearchResult[]; total_found: number; search_id: string; facets: SearchFacets; suggestions: string[]; pagination: PaginationInfo; execution_time_ms: number; +/** + * Which index type was used for this search + */ +index_type: IndexType; +/** + * Which filters are available for this search type + */ +available_filters: FilterKind[] }; + +/** + * Individual search result + */ +export type FileSearchResult = { file: File; score: number; score_breakdown: ScoreBreakdown; highlights: TextHighlight[]; matched_content: string | null }; + +/** + * Filesystem type + */ +export type FileSystem = +/** + * Apple File System + */ +"APFS" | +/** + * NT File System (Windows) + */ +"NTFS" | +/** + * Fourth Extended Filesystem (Linux) + */ +"Ext4" | +/** + * B-tree Filesystem (Linux) + */ +"Btrfs" | +/** + * ZFS + */ +"ZFS" | +/** + * Resilient File System (Windows) + */ +"ReFS" | +/** + * File Allocation Table 32 + */ +"FAT32" | +/** + * Extended File Allocation Table + */ +"ExFAT" | +/** + * Hierarchical File System Plus (macOS legacy) + */ +"HFSPlus" | +/** + * Network File System + */ +"NFS" | +/** + * Server Message Block + */ +"SMB" | +/** + * Other filesystem + */ +{ Other: string }; + +/** + * Indicates which filters are available for a given search type + */ +export type FilterKind = "FileTypes" | "DateRange" | "SizeRange" | "ContentTypes" | "Tags" | "Locations" | "Hidden" | "Archived"; + +/** + * Raw filesystem event kinds emitted by the watcher without DB resolution + */ +export type FsRawEventKind = { Create: { path: string } } | { Modify: { path: string } } | { Remove: { path: string } } | { Rename: { from: string; to: string } }; + +/** + * Generate proxy for a single video file + */ +export type GenerateProxyInput = { +/** + * UUID of the entry to generate proxy for + */ +entry_uuid: string; +/** + * Proxy resolution (scrubbing, ultra_low, quick, editing) + */ +resolution: string | null; +/** + * Force regeneration even if proxy exists + */ +force: boolean; +/** + * Use hardware acceleration if available + */ +use_hardware_accel: boolean | null }; + +export type GenerateProxyOutput = { +/** + * Number of proxies generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[]; +/** + * Total encoding time in seconds + */ +encoding_time_secs: number }; + +export type GenerateSplatInput = { entry_uuid: string; model_path: string | null }; + +export type GenerateSplatOutput = { +/** + * Job ID for tracking splat generation progress + */ +job_id: string }; + +/** + * Generate thumbstrip for a single video file + */ +export type GenerateThumbstripInput = { +/** + * UUID of the entry to generate thumbstrip for + */ +entry_uuid: string; +/** + * Optional variant names (defaults to thumbstrip_preview) + */ +variants: string[] | null; +/** + * Force regeneration even if thumbstrip exists + */ +force: boolean }; + +export type GenerateThumbstripOutput = { +/** + * Number of thumbstrips generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[] }; + +/** + * Generic progress information that all job types can convert into + */ +export type GenericProgress = { +/** + * Current progress as a percentage (0.0 to 1.0) + */ +percentage: number; +/** + * Current phase or stage name (e.g., "Discovery", "Processing", "Finalizing") + */ +phase: string; +/** + * Current path being processed (if applicable) + */ +current_path: SdPath | null; +/** + * Human-readable message describing current activity + */ +message: string; +/** + * Completion metrics + */ +completion: ProgressCompletion; +/** + * Performance metrics + */ +performance: PerformanceMetrics }; + +/** + * Input for getting sync activity summary + */ +export type GetSyncActivityInput = Record; + +/** + * Sync activity summary for the UI + */ +export type GetSyncActivityOutput = { currentState: DeviceSyncState; peers: PeerActivity[]; errorCount: number }; + +export type GetSyncEventLogInput = { +/** + * Time range filter (start) + */ +start_time?: string | null; +/** + * Time range filter (end) + */ +end_time?: string | null; +/** + * Filter by event types + */ +event_types?: SyncEventType[] | null; +/** + * Filter by categories + */ +categories?: EventCategory[] | null; +/** + * Filter by severity levels + */ +severities?: EventSeverity[] | null; +/** + * Filter by peer device + */ +peer_id?: string | null; +/** + * Filter by model type + */ +model_type?: string | null; +/** + * Filter by correlation ID + */ +correlation_id?: string | null; +/** + * Maximum number of results + */ +limit?: number | null; +/** + * Offset for pagination + */ +offset?: number | null; +/** + * Include events from remote peers + */ +include_remote_peers?: boolean | null }; + +export type GetSyncEventLogOutput = { events: SyncEventLog[] }; + +export type GetSyncMetricsInput = { +/** + * Filter metrics since this time + */ +since: string | null; +/** + * Filter metrics for specific peer device + */ +peer_id: string | null; +/** + * Filter metrics for specific model type + */ +model_type: string | null; +/** + * Show only state metrics + */ +state_only: boolean | null; +/** + * Show only operation metrics + */ +operations_only: boolean | null; +/** + * Show only error metrics + */ +errors_only: boolean | null }; + +export type GetSyncMetricsOutput = { +/** + * The metrics snapshot + */ +metrics: SyncMetricsSnapshot }; + +/** + * Types of groups that can appear in a space + */ +export type GroupType = +/** + * Fixed quick navigation (Overview, Recents, Favorites) + */ +"QuickAccess" | +/** + * Device with its volumes and locations as children + */ +{ Device: { device_id: string } } | +/** + * All devices (library and paired) across the system + */ +"Devices" | +/** + * All locations across all devices + */ +"Locations" | +/** + * All volumes across all devices + */ +"Volumes" | +/** + * Tag collection + */ +"Tags" | +/** + * Cloud storage providers + */ +"Cloud" | +/** + * User-defined custom group + */ +"Custom"; + +/** + * Image metadata extracted from EXIF + */ +export type ImageMediaData = { uuid: string; width: number; height: number; blurhash: string | null; date_taken: string | null; latitude: number | null; longitude: number | null; camera_make: string | null; camera_model: string | null; lens_model: string | null; focal_length: string | null; aperture: string | null; shutter_speed: string | null; iso: number | null; orientation: number | null; color_space: string | null; color_profile: string | null; bit_depth: string | null; artist: string | null; copyright: string | null; description: string | null }; + +/** + * Statistics about what was imported + */ +export type ImportStats = { entries_imported: number; entries_skipped: number; content_identities: number; user_metadata: number; tags: number; media_data: number }; + +/** + * Canonical input for indexing requests from any interface (CLI, API, etc.) + */ +export type IndexInput = { +/** + * The library within which the operation runs + */ +library_id: string; +/** + * One or more filesystem paths to index + */ +paths: string[]; +/** + * Indexing scope (current directory only vs recursive) + */ +scope: IndexScope; +/** + * Indexing mode (shallow/content/deep) + */ +mode: IndexMode; +/** + * Whether to include hidden files/directories + */ +include_hidden: boolean; +/** + * Where results are stored (ephemeral vs persistent) + */ +persistence: IndexPersistence }; + +/** + * How deeply to index files in this location + */ +export type IndexMode = +/** + * Location exists but is not indexed + */ +"None" | +/** + * Just filesystem metadata (name, size, dates) + */ +"Shallow" | +/** + * Generate content IDs for deduplication + */ +"Content" | +/** + * Full indexing - content IDs, text extraction, thumbnails + */ +"Deep"; + +/** + * Whether to write indexing results to the database or keep them in memory. + * + * Ephemeral persistence allows users to browse external drives and network shares + * without adding them as managed locations. The in-memory index survives for the + * session duration and provides the same API surface as persistent entries, enabling + * features like search and navigation to work identically for both modes. If an + * ephemeral path is later promoted to a managed location, UUIDs are preserved to + * maintain continuity for user metadata. + */ +export type IndexPersistence = +/** + * Write all results to database (normal operation) + */ +"Persistent" | +/** + * Keep results in memory only (for unmanaged paths) + */ +"Ephemeral"; + +/** + * Whether to index just one directory level or recurse through subdirectories. + * + * Current scope is used for UI navigation where users expand folders on-demand, + * while Recursive scope is used for full location indexing. Current scope with + * persistent storage enables progressive indexing where the UI drives which + * directories get indexed based on user interaction. + */ +export type IndexScope = +/** + * Index only the current directory (single level) + */ +"Current" | +/** + * Index recursively through all subdirectories + */ +"Recursive"; + +/** + * Indicates which index type was used for a search query + */ +export type IndexType = +/** + * Database FTS5 search (persistent index) + */ +"Persistent" | +/** + * In-memory ephemeral search + */ +"Ephemeral" | +/** + * Mix of both (future: hybrid searches) + */ +"Hybrid"; + +export type IndexVerifyInput = { +/** + * Path to verify (can be a location root or subdirectory) + */ +path: string; +/** + * Whether to check content hashes (slower but more thorough) + */ +verify_content?: boolean; +/** + * Whether to include detailed file-by-file comparison + */ +detailed_report?: boolean; +/** + * Whether to fix issues automatically (future feature) + */ +auto_fix?: boolean }; + +/** + * Result of index integrity verification + */ +export type IndexVerifyOutput = { +/** + * Overall integrity status + */ +is_valid: boolean; +/** + * Integrity report with detailed findings + */ +report: IntegrityReport; +/** + * Path that was verified + */ +path: string; +/** + * Time taken to verify (seconds) + */ +duration_secs: number }; + +/** + * Input for volume indexing action + */ +export type IndexVolumeInput = { +/** + * Volume fingerprint to index + */ +fingerprint: string; +/** + * Indexing scope (defaults to Recursive for full volume) + */ +scope?: IndexScope }; + +/** + * Output from volume indexing action + */ +export type IndexVolumeOutput = { +/** + * UUID of the indexed volume + */ +volume_id: string; +/** + * Job ID for tracking progress + */ +job_id: string; +/** + * Total files found (if job completed) + */ +total_files: number | null; +/** + * Total directories found (if job completed) + */ +total_directories: number | null; +/** + * Success message + */ +message: string }; + +/** + * Information about an indexed path + */ +export type IndexedPathInfo = { +/** + * The directory path that was indexed + */ +path: string; +/** + * Number of direct children in this directory + */ +child_count: number }; + +/** + * Complete snapshot of indexer performance after job completion. + */ +export type IndexerMetrics = { total_duration: { secs: number; nanos: number }; discovery_duration: { secs: number; nanos: number }; processing_duration: { secs: number; nanos: number }; content_duration: { secs: number; nanos: number }; files_per_second: number; bytes_per_second: number; dirs_per_second: number; db_writes: number; db_reads: number; batch_count: number; avg_batch_size: number; total_errors: number; critical_errors: number; non_critical_errors: number; skipped_paths: number; peak_memory_bytes: number | null; avg_memory_bytes: number | null }; + +/** + * Indexer settings controlling rule toggles + */ +export type IndexerSettings = { no_system_files?: boolean; no_git?: boolean; no_dev_dirs?: boolean; no_hidden?: boolean; gitignore?: boolean; only_images?: boolean }; + +/** + * Cumulative statistics tracked throughout the indexing process. + */ +export type IndexerStats = { files: number; dirs: number; bytes: number; symlinks: number; skipped: number; errors: number }; + +/** + * Represents a single integrity difference + */ +export type IntegrityDifference = { +/** + * Path relative to verification root + */ +path: string; +/** + * Type of issue + */ +issue_type: IssueType; +/** + * Expected value (from filesystem or correct state) + */ +expected: string | null; +/** + * Actual value (from database) + */ +actual: string | null; +/** + * Human-readable description + */ +description: string; +/** + * Debug: database entry ID for investigation + */ +db_entry_id?: number | null; +/** + * Debug: database entry name + */ +db_entry_name?: string | null }; + +/** + * Detailed integrity report + */ +export type IntegrityReport = { +/** + * Total files found on filesystem + */ +filesystem_file_count: number; +/** + * Total files in database index + */ +database_file_count: number; +/** + * Total directories found on filesystem + */ +filesystem_dir_count: number; +/** + * Total directories in database index + */ +database_dir_count: number; +/** + * Files missing from index (on filesystem but not in DB) + */ +missing_from_index: IntegrityDifference[]; +/** + * Stale entries in index (in DB but not on filesystem) + */ +stale_in_index: IntegrityDifference[]; +/** + * Entries with incorrect metadata + */ +metadata_mismatches: IntegrityDifference[]; +/** + * Entries with incorrect parent relationships + */ +hierarchy_errors: IntegrityDifference[]; +/** + * Summary statistics + */ +summary: string }; + +export type IssueType = { type: "MissingFromIndex" } | { type: "StaleInIndex" } | { type: "SizeMismatch" } | { type: "ModifiedTimeMismatch" } | { type: "InodeMismatch" } | { type: "ExtensionMismatch" } | { type: "ParentMismatch" } | { type: "KindMismatch" }; + +/** + * Types of items that can appear in a group + */ +export type ItemType = +/** + * Overview screen (fixed) + */ +"Overview" | +/** + * Recent files (fixed) + */ +"Recents" | +/** + * Favorited files (fixed) + */ +"Favorites" | +/** + * File kinds (images, videos, audio, etc.) + */ +"FileKinds" | +/** + * Indexed location + */ +{ Location: { location_id: string } } | +/** + * Storage volume (with locations as children) + */ +{ Volume: { volume_id: string } } | +/** + * Tag filter + */ +{ Tag: { tag_id: string } } | +/** + * Any arbitrary path (dragged from explorer) + */ +{ Path: { sd_path: SdPath } }; + +export type JobCancelInput = { job_id: string }; + +export type JobCancelOutput = { job_id: string; success: boolean }; + +/** + * Unique identifier for a job + */ +export type JobId = string; + +export type JobInfoOutput = { id: string; name: string; status: JobStatus; progress: number; created_at: string; started_at: string | null; completed_at: string | null; error_message: string | null }; + +export type JobInfoQueryInput = { job_id: string }; + +export type JobListInput = { status: JobStatus | null }; + +export type JobListItem = { id: string; name: string; device_id: string; status: JobStatus; progress: number; action_type: string | null; action_context: ActionContextInfo | null; created_at: string; started_at: string | null; completed_at: string | null }; + +export type JobListOutput = { jobs: JobListItem[] }; + +/** + * Output from a completed job + */ +export type JobOutput = +/** + * Job completed successfully with no specific output + */ +{ type: "Success" } | +/** + * File copy job output + */ +{ type: "FileCopy"; data: { copied_count: number; total_bytes: number } } | +/** + * Indexer job output + */ +{ type: "Indexed"; data: { stats: IndexerStats; metrics: IndexerMetrics } } | +/** + * Thumbnail generation output + */ +{ type: "ThumbnailsGenerated"; data: { generated_count: number; failed_count: number } } | +/** + * Thumbnail generation output (detailed) + */ +{ type: "ThumbnailGeneration"; data: { generated_count: number; skipped_count: number; error_count: number; total_size_bytes: number } } | +/** + * File move/rename operation output + */ +{ type: "FileMove"; data: { moved_count: number; failed_count: number; total_bytes: number } } | +/** + * File delete operation output + */ +{ type: "FileDelete"; data: { deleted_count: number; failed_count: number; total_bytes: number } } | +/** + * Duplicate detection output + */ +{ type: "DuplicateDetection"; data: { duplicate_groups: number; total_duplicates: number; potential_savings: number } } | +/** + * File validation output + */ +{ type: "FileValidation"; data: { validated_count: number; issues_found: number; total_bytes_validated: number } } | +/** + * OCR text extraction output + */ +{ type: "OcrExtraction"; data: { total_processed: number; success_count: number; error_count: number } } | +/** + * Speech-to-text transcription output + */ +{ type: "SpeechToText"; data: { total_processed: number; success_count: number; error_count: number } } | +/** + * Gaussian splat generation output + */ +{ type: "GaussianSplat"; data: { total_processed: number; success_count: number; error_count: number } }; + +export type JobPauseInput = { job_id: string }; + +export type JobPauseOutput = { job_id: string; success: boolean }; + +/** + * Job execution policies for a location + * + * Controls which automated jobs run on this location and their configuration. + * This allows per-location customization of thumbnail generation, OCR, speech-to-text, etc. + */ +export type JobPolicies = { +/** + * Thumbnail generation policy + */ +thumbnail?: ThumbnailPolicy; +/** + * Thumbstrip generation policy + */ +thumbstrip?: ThumbstripPolicy; +/** + * Proxy/sidecar generation policy (video scrubbing) + */ +proxy?: ProxyPolicy; +/** + * OCR (text extraction) policy + */ +ocr?: OcrPolicy; +/** + * Speech-to-text transcription policy + */ +speech_to_text?: SpeechPolicy; +/** + * Object detection policy (future) + */ +object_detection?: ObjectDetectionPolicy }; + +export type JobReceipt = { id: JobId; job_name: string }; + +export type JobResumeInput = { job_id: string }; + +export type JobResumeOutput = { job_id: string; success: boolean }; + +/** + * Statistics from the indexer job + */ +export type JobStats = { +/** + * Number of files indexed + */ +files: number; +/** + * Number of directories indexed + */ +dirs: number; +/** + * Number of symlinks indexed + */ +symlinks: number; +/** + * Total bytes indexed + */ +bytes: number }; + +/** + * Current status of a job + */ +export type JobStatus = +/** + * Job is waiting to be executed + */ +"queued" | +/** + * Job is currently running + */ +"running" | +/** + * Job has been paused + */ +"paused" | +/** + * Job completed successfully + */ +"completed" | +/** + * Job failed with an error + */ +"failed" | +/** + * Job was cancelled + */ +"cancelled"; + +/** + * Type of job to trigger for a location + */ +export type JobType = "thumbnail" | "thumbstrip" | "ocr" | "speech_to_text" | "object_detection"; + +export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }; + +/** + * Latency metrics snapshot + */ +export type LatencySnapshot = { count: number; avg_ms: number; min_ms: number; max_ms: number }; + +/** + * A Spacedrive library - the canonical domain model + * + * This is the resource type sent to the frontend for the normalized cache. + * It contains all the information needed to display library info in the UI. + */ +export type Library = { +/** + * Library unique identifier + */ +id: string; +/** + * Human-readable library name + */ +name: string; +/** + * Optional description + */ +description: string | null; +/** + * Path to the library directory + */ +path: string; +/** + * When the library was created + */ +created_at: string; +/** + * When the library was last modified + */ +updated_at: string; +/** + * Library-specific settings + */ +settings: LibrarySettings; +/** + * Library statistics + */ +statistics: LibraryStatistics }; + +/** + * Input for creating a new library + */ +export type LibraryCreateInput = { +/** + * Name of the library + */ +name: string; +/** + * Optional path for the library (if not provided, will use default location) + */ +path: string | null }; + +/** + * Output from library create action dispatch + */ +export type LibraryCreateOutput = { library_id: string; name: string; path: string }; + +/** + * Source of library creation for automatic switching behavior + */ +export type LibraryCreationSource = +/** + * User created locally via UI + */ +"Manual" | +/** + * Received via network sync from another device + */ +"Sync" | +/** + * Imported from cloud storage + */ +"CloudImport"; + +/** + * Input for deleting a library + */ +export type LibraryDeleteInput = { +/** + * ID of the library to delete + */ +library_id: string; +/** + * Whether to also delete the library's data directory + */ +delete_data: boolean }; + +/** + * Output from library delete action dispatch + */ +export type LibraryDeleteOutput = { library_id: string; name: string }; + +/** + * Input for exporting a library + */ +export type LibraryExportInput = { library_id: string; export_path: string; include_thumbnails: boolean; include_previews: boolean }; + +export type LibraryExportOutput = { library_id: string; library_name: string; export_path: string; exported_files: string[] }; + +/** + * Information about a library for listing purposes + */ +export type LibraryInfo = { +/** + * Library unique identifier + */ +id: string; +/** + * Human-readable library name + */ +name: string; +/** + * Path to the library directory + */ +path: string; +/** + * Optional statistics if requested + */ +stats: LibraryStatistics | null }; + +/** + * Input for library info query + */ +export type LibraryInfoQueryInput = null; + +export type LibraryOpenInput = { +/** + * Path to the library directory to open + */ +path: string }; + +export type LibraryOpenOutput = { +/** + * ID of the opened library + */ +library_id: string; +/** + * Name of the opened library + */ +name: string; +/** + * Path where the library is located + */ +path: string }; + +export type LibraryRenameInput = { library_id: string; new_name: string }; + +export type LibraryRenameOutput = { library_id: string; old_name: string; new_name: string }; + +/** + * Library-specific settings + */ +export type LibrarySettings = { +/** + * Whether to generate thumbnails for media files + */ +generate_thumbnails: boolean; +/** + * Thumbnail quality (0-100) + */ +thumbnail_quality: number; +/** + * Whether to enable AI-powered tagging + */ +enable_ai_tagging: boolean; +/** + * Whether sync is enabled for this library + */ +sync_enabled: boolean; +/** + * Whether the library is encrypted at rest + */ +encryption_enabled: boolean; +/** + * Custom thumbnail sizes to generate + */ +thumbnail_sizes: number[]; +/** + * File extensions to ignore during indexing + */ +ignored_extensions: string[]; +/** + * TODO: ai slop config pls remove this + */ +max_file_size: number | null; +/** + * Whether to automatically track system volumes + */ +auto_track_system_volumes: boolean; +/** + * Whether to automatically track external volumes when connected + */ +auto_track_external_volumes: boolean; +/** + * Indexer settings (rule toggles and related) + */ +indexer?: IndexerSettings }; + +/** + * Library statistics + */ +export type LibraryStatistics = { +/** + * Total number of files indexed + */ +total_files: number; +/** + * Total size of all files in bytes + */ +total_size: number; +/** + * Number of locations in this library + */ +location_count: number; +/** + * Number of tags created + */ +tag_count: number; +/** + * Number of devices in this library (v2 field, defaults to 0 for old configs) + */ +device_count?: number; +/** + * Number of unique content identities in this library (v2 field, defaults to 0 for old configs) + */ +unique_content_count?: number; +/** + * Total storage capacity across all volumes in bytes (v2 field, defaults to 0 for old configs) + */ +total_capacity?: number; +/** + * Available storage across all volumes in bytes (v2 field, defaults to 0 for old configs) + */ +available_capacity?: number; +/** + * Number of thumbnails generated + */ +thumbnail_count: number; +/** + * Database file size in bytes + */ +database_size: number; +/** + * Last time the library was fully indexed + */ +last_indexed: string | null; +/** + * When these statistics were last updated + */ +updated_at: string }; + +/** + * Action to take when setting up library sync + */ +export type LibrarySyncAction = +/** + * Share local library to remote device (creates same library with same UUID on remote) + * This is the primary way to create a shared library + */ +{ type: "shareLocalLibrary"; libraryName: string } | +/** + * Join an existing remote library (creates same library with same UUID locally) + * Use this when the other device has already shared their library + */ +{ type: "joinRemoteLibrary"; remoteLibraryId: string; remoteLibraryName: string } | +/** + * Future: Merge two different libraries into one (combines data from both) + * Not yet implemented - requires full sync system + */ +{ type: "mergeLibraries"; localLibraryId: string; remoteLibraryId: string; mergedName: string }; + +/** + * Input for setting up library sync between paired devices + */ +export type LibrarySyncSetupInput = { +/** + * Local device ID (should be current device) + */ +localDeviceId: string; +/** + * Remote paired device ID + */ +remoteDeviceId: string; +/** + * Local library to set up sync for + */ +localLibraryId: string; +/** + * Remote library to sync with (optional for RegisterOnly) + */ +remoteLibraryId: string | null; +/** + * Sync action to perform + */ +action: LibrarySyncAction; +/** + * DEPRICATED: Which device should be the sync leader (for future sync implementation) + */ +leaderDeviceId: string }; + +/** + * Result of library sync setup operation + */ +export type LibrarySyncSetupOutput = { +/** + * Whether setup was successful + */ +success: boolean; +/** + * Local library ID that was configured + */ +localLibraryId: string; +/** + * Remote library ID that was linked (if applicable) + */ +remoteLibraryId: string | null; +/** + * Whether devices were successfully registered in each other's libraries + */ +devicesRegistered: boolean; +/** + * Message describing the result + */ +message: string }; + +export type ListEventsInput = Record; + +export type ListEventsOutput = { +/** + * All available event types + */ +all_events: string[]; +/** + * Events that are high-frequency and should be excluded by default + */ +noisy_events: string[]; +/** + * Detailed information about each event + */ +event_info: EventInfo[] }; + +export type ListLibrariesInput = { +/** + * Whether to include detailed statistics for each library + */ +include_stats: boolean }; + +/** + * Input for listing devices from library database + */ +export type ListLibraryDevicesInput = { +/** + * Whether to include offline devices (default: true) + */ +include_offline: boolean; +/** + * Whether to include detailed capabilities and sync leadership info (default: false) + */ +include_details: boolean; +/** + * Whether to also include paired network devices (default: false) + */ +show_paired?: boolean }; + +export type ListPairedDevicesInput = { +/** + * Whether to include only connected devices + */ +connectedOnly?: boolean }; + +/** + * Output from listing paired devices + */ +export type ListPairedDevicesOutput = { +/** + * List of paired devices + */ +devices: PairedDeviceInfo[]; +/** + * Total number of paired devices + */ +total: number; +/** + * Number of currently connected devices + */ +connected: number }; + +export type ListWhisperModelsInput = Record; + +export type ListWhisperModelsOutput = { models: ModelInfo[]; total_downloaded_size: number }; + +/** + * An indexed directory that Spacedrive monitors + */ +export type Location = { +/** + * Unique identifier + */ +id: string; +/** + * Library this location belongs to + */ +library_id: string; +/** + * Root path of this location (includes device!) + */ +sd_path: SdPath; +/** + * Human-friendly name + */ +name: string; +/** + * Indexing configuration + */ +index_mode: IndexMode; +/** + * How often to rescan (None = manual only) + */ +scan_interval: { secs: number; nanos: number } | null; +/** + * Statistics + */ +total_size: number; file_count: number; directory_count: number; +/** + * Current state + */ +scan_state: ScanState; +/** + * Timestamps + */ +created_at: string; updated_at: string; last_scan_at: string | null; +/** + * Whether this location is currently available + */ +is_available: boolean; +/** + * Hidden glob patterns (e.g., [".*", "node_modules"]) + */ +ignore_patterns: string[]; +/** + * Job execution policies for this location + */ +job_policies?: JobPolicies }; + +export type LocationAddInput = { path: SdPath; name: string | null; mode: IndexMode; job_policies: JsonValue | null }; + +/** + * Output from location add action dispatch + */ +export type LocationAddOutput = { location_id: string; path: SdPath; name: string | null; job_id: string | null }; + +/** + * Input for exporting a location + */ +export type LocationExportInput = { +/** + * The UUID of the location to export + */ +location_uuid: string; +/** + * Path where the SQL dump file will be written + */ +export_path: string; +/** + * Include content identities (file hashes, dedup info) + */ +include_content_identities?: boolean; +/** + * Include media metadata (EXIF, video/audio info) + */ +include_media_data?: boolean; +/** + * Include user metadata (notes, favorites) + */ +include_user_metadata?: boolean; +/** + * Include tags and tag relationships + */ +include_tags?: boolean }; + +/** + * Output from location export action + */ +export type LocationExportOutput = { location_uuid: string; location_name: string | null; export_path: string; file_size_bytes: number; stats: ExportStats }; + +/** + * Input for importing a location from SQL dump + */ +export type LocationImportInput = { +/** + * Path to the SQL dump file to import + */ +import_path: string; +/** + * Optional new name for the imported location (overrides name in dump) + */ +new_name: string | null; +/** + * Whether to skip entries that already exist (by UUID) + */ +skip_existing?: boolean }; + +/** + * Output from location import action + */ +export type LocationImportOutput = { location_uuid: string; location_name: string | null; import_path: string; stats: ImportStats }; + +export type LocationRemoveInput = { location_id: string }; + +/** + * Output from location remove action dispatch + */ +export type LocationRemoveOutput = { location_id: string; path: string | null }; + +export type LocationRescanInput = { location_id: string; full_rescan: boolean }; + +export type LocationRescanOutput = { location_id: string; location_path: string; job_id: string; full_rescan: boolean }; + +export type LocationTriggerJobInput = { +/** + * UUID of the location to run the job on + */ +location_id: string; +/** + * Type of job to trigger + */ +job_type: JobType; +/** + * Force the job to run even if disabled in the location's policy + */ +force?: boolean }; + +export type LocationTriggerJobOutput = { +/** + * UUID of the dispatched job + */ +job_id: string; +/** + * Type of job that was triggered + */ +job_type: JobType; +/** + * UUID of the location the job is running on + */ +location_id: string }; + +export type LocationUpdateInput = { +/** + * UUID of the location to update + */ +id: string; +/** + * Optional new name for the location + */ +name: string | null; +/** + * Optional job policies to update + */ +job_policies: JobPolicies | null }; + +export type LocationUpdateOutput = { +/** + * UUID of the updated location + */ +id: string }; + +/** + * Output for location list queries + */ +export type LocationsListOutput = { locations: Location[] }; + +export type LocationsListQueryInput = null; + +/** + * Input for media listing + */ +export type MediaListingInput = { +/** + * The directory path to list media for + */ +path: SdPath; +/** + * Whether to include media from descendant directories (default: false) + */ +include_descendants: boolean | null; +/** + * Which media types to include (default: both Image and Video) + */ +media_types: ContentKind[] | null; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Sort order for results + */ +sort_by: MediaSortBy }; + +/** + * Output containing media files + */ +export type MediaListingOutput = { +/** + * Media files (images/videos) + */ +files: File[]; +/** + * Total count of media files found + */ +total_count: number; +/** + * Whether there are more results than returned + */ +has_more: boolean }; + +/** + * Sort options for media listing + */ +export type MediaSortBy = +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by creation date (newest first) + */ +"created" | +/** + * Sort by date taken/captured (newest first) + */ +"datetaken" | +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by size (largest first) + */ +"size"; + +/** + * Information about a model + */ +export type ModelInfo = { +/** + * Unique model identifier + */ +id: string; +/** + * Human-readable name + */ +name: string; +/** + * Model type + */ +model_type: ModelType; +/** + * File size in bytes + */ +size_bytes: number; +/** + * Where to download from + */ +provider: ModelProvider; +/** + * Filename on disk + */ +filename: string; +/** + * Whether this model is currently downloaded + */ +downloaded: boolean; +/** + * Optional description + */ +description: string | null }; + +/** + * Model provider + */ +export type ModelProvider = +/** + * Hugging Face + */ +{ HuggingFace: { repo: string } } | +/** + * GitHub Release + */ +{ GitHub: { owner: string; repo: string } } | +/** + * Direct URL + */ +{ Direct: { url: string } }; + +/** + * Type of model + */ +export type ModelType = +/** + * Whisper speech-to-text model + */ +"Whisper" | +/** + * Tesseract OCR language data + */ +"Tesseract"; + +/** + * Mount type classification + */ +export type MountType = +/** + * System mount (root, boot, etc.) + */ +"System" | +/** + * External device mount + */ +"External" | +/** + * Network mount + */ +"Network" | +/** + * User mount + */ +"User"; + +export type NetworkStartInput = Record; + +export type NetworkStartOutput = { started: boolean }; + +export type NetworkStatus = { running: boolean; node_id: string | null; addresses: string[]; paired_devices: number; connected_devices: number; version: string; relay_url: string | null }; + +export type NetworkStatusQueryInput = null; + +export type NetworkStopInput = Record; + +export type NetworkStopOutput = { stopped: boolean }; + +/** + * Object detection policy (for future AI features) + */ +export type ObjectDetectionPolicy = { +/** + * Whether to run object detection on this location + */ +enabled: boolean; +/** + * Minimum confidence threshold (0.0 - 1.0) + */ +min_confidence: number; +/** + * Categories to detect (empty = all) + */ +categories: string[]; +/** + * Whether to reprocess files that already have object data + */ +reprocess: boolean }; + +/** + * OCR (text extraction) policy + */ +export type OcrPolicy = { +/** + * Whether to run OCR on this location + */ +enabled: boolean; +/** + * Languages to use for OCR (e.g., ["eng", "spa"]) + */ +languages: string[]; +/** + * Minimum confidence threshold (0.0 - 1.0) + */ +min_confidence: number; +/** + * Whether to reprocess files that already have text + */ +reprocess: boolean }; + +/** + * Operating system types + */ +export type OperatingSystem = "MacOS" | "Windows" | "Linux" | "IOs" | "Android" | "Other"; + +/** + * Operation metrics snapshot + */ +export type OperationSnapshot = { broadcasts_sent: number; state_changes_broadcast: number; shared_changes_broadcast: number; broadcast_batches_sent: number; failed_broadcasts: number; changes_received: number; changes_applied: number; changes_rejected: number; buffer_queue_depth: number; active_backfill_sessions: number; backfill_sessions_completed: number; backfill_pagination_rounds: number; retry_queue_depth: number; retry_attempts: number; retry_successes: number }; + +/** + * Pagination information + */ +export type PaginationInfo = { current_page: number; total_pages: number; has_next: boolean; has_previous: boolean; limit: number; offset: number }; + +/** + * Pagination options + */ +export type PaginationOptions = { limit: number; offset: number }; + +export type PairCancelInput = { session_id: string }; + +export type PairCancelOutput = { cancelled: boolean }; + +export type PairGenerateInput = Record; + +export type PairGenerateOutput = { code: string; session_id: string; expires_at: string; +/** + * QR code JSON format (includes NodeId for remote pairing) + */ +qr_json: string; +/** + * Node ID for relay-based pairing (share this for cross-network pairing) + */ +node_id: string | null }; + +export type PairJoinInput = { code: string; +/** + * Optional node ID for relay-based pairing (enables cross-network connections) + */ +node_id: string | null }; + +export type PairJoinOutput = { paired_device_id: string; device_name: string }; + +export type PairStatusOutput = { sessions: PairingSessionSummary[] }; + +export type PairStatusQueryInput = null; + +/** + * Information about a paired device + */ +export type PairedDeviceInfo = { +/** + * Device ID + */ +id: string; +/** + * Device name + */ +name: string; +/** + * Device type + */ +deviceType: string; +/** + * OS version + */ +osVersion: string; +/** + * App version + */ +appVersion: string; +/** + * Whether the device is currently connected + */ +isConnected: boolean; +/** + * When the device was last seen + */ +lastSeen: string }; + +export type PairingSessionSummary = { id: string; state: SerializablePairingState; remote_device_id: string | null; expires_at: string | null }; + +/** + * Path mapping for resolving virtual paths to actual storage locations + */ +export type PathMapping = { virtual_path: string; actual_path: string }; + +/** + * Per-peer activity information + */ +export type PeerActivity = { deviceId: string; deviceName: string; isOnline: boolean; lastSeen: string; entriesReceived: number; bytesReceived: number; bytesSent: number; watermarkLagMs: number | null }; + +/** + * Performance and timing metrics + */ +export type PerformanceMetrics = { +/** + * Processing rate (items per second) + */ +rate: number; +/** + * Estimated time remaining + */ +estimated_remaining: { secs: number; nanos: number } | null; +/** + * Time elapsed since start + */ +elapsed: { secs: number; nanos: number } | null; +/** + * Number of errors encountered + */ +error_count: number; +/** + * Number of warnings + */ +warning_count: number }; + +/** + * Performance metrics snapshot + */ +export type PerformanceSnapshot = { broadcast_latency: LatencySnapshot; apply_latency: LatencySnapshot; backfill_request_latency: LatencySnapshot; state_watermark: string; shared_watermark: string; watermark_lag_ms: { [key in string]: number }; hlc_physical_drift_ms: number; hlc_counter_max: number; db_query_duration: LatencySnapshot; db_query_count: number }; + +export type PingInput = { message: string; count?: number | null }; + +export type PingOutput = { echo: string; count: number; extension_works: boolean }; + +/** + * Privacy levels for tag visibility control + */ +export type PrivacyLevel = +/** + * Standard visibility in all contexts + */ +"Normal" | +/** + * Hidden from normal searches but accessible via direct query + */ +"Archive" | +/** + * Completely hidden from standard UI + */ +"Hidden"; + +/** + * Progress completion information + */ +export type ProgressCompletion = { +/** + * Items completed (files, entries, operations, etc.) + */ +completed: number; +/** + * Total items to complete + */ +total: number; +/** + * Bytes processed (if applicable) + */ +bytes_completed: number | null; +/** + * Total bytes to process (if applicable) + */ +total_bytes: number | null }; + +/** + * Proxy/sidecar generation policy (video scrubbing) + */ +export type ProxyPolicy = { +/** + * Whether to generate proxy files for this location + */ +enabled: boolean; +/** + * Whether to regenerate existing proxies + */ +regenerate: boolean }; + +export type RegenerateThumbnailInput = { +/** + * UUID of the entry to regenerate thumbnails for + */ +entry_uuid: string; +/** + * Optional variant names (defaults to grid@1x, grid@2x, detail@1x) + */ +variants: string[] | null; +/** + * Force regeneration even if thumbnails exist + */ +force: boolean }; + +export type RegenerateThumbnailOutput = { +/** + * Number of thumbnails generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[] }; + +/** + * State of a job running on a remote device + */ +export type RemoteJobState = { job_id: string; job_type: string; library_id: string; device_id: string; device_name: string; status: JobStatus; progress: number | null; message: string | null; generic_progress: GenericProgress | null; started_at: string | null; completed_at: string | null; error: string | null }; + +/** + * Query for all remote jobs across all devices + */ +export type RemoteJobsAllDevicesInput = Record; + +export type RemoteJobsAllDevicesOutput = { jobs_by_device: { [key in string]: RemoteJobState[] } }; + +/** + * Query for remote jobs on a specific device + */ +export type RemoteJobsForDeviceInput = { device_id: string }; + +export type RemoteJobsForDeviceOutput = { jobs: RemoteJobState[] }; + +/** + * Information about a library discovered on a remote device + */ +export type RemoteLibraryInfo = { +/** + * Library ID + */ +id: string; +/** + * Library name + */ +name: string; +/** + * Library description (if any) + */ +description: string | null; +/** + * When the library was created + */ +createdAt: string; +/** + * Statistics about the library + */ +statistics: LibraryStatistics }; + +export type ReorderGroupsInput = { space_id: string; group_ids: string[] }; + +export type ReorderItemsInput = { group_id: string | null; item_ids: string[] }; + +export type ReorderOutput = { success: boolean }; + +export type ResetDataInput = { +/** + * Confirmation flag to prevent accidental data loss + */ +confirm: boolean }; + +export type ResetDataOutput = { +/** + * Whether the reset was successful + */ +success: boolean; +/** + * Message describing the result + */ +message: string }; + +/** + * Metadata for resource cache updates + */ +export type ResourceMetadata = { +/** + * Fields that should be replaced, not merged + */ +no_merge_fields: string[]; +/** + * Alternate IDs for matching (besides primary ID) + */ +alternate_ids: string[]; +/** + * Paths affected by this resource event (for path-scoped filtering) + */ +affected_paths?: SdPath[] }; + +/** + * Risk level for adding a path as a location + */ +export type RiskLevel = +/** + * Safe - nested path in user directories + */ +"low" | +/** + * Caution - shallow path on primary volume (e.g., /Users/jamie) + */ +"medium" | +/** + * Warning - system directory or root-level path (e.g., /, /System) + */ +"high"; + +/** + * Current scanning state of a location + */ +export type ScanState = +/** + * Not currently being scanned + */ +"Idle" | +/** + * Currently scanning + */ +{ Scanning: { +/** + * Progress percentage (0-100) + */ +progress: number } } | +/** + * Scan completed successfully + */ +"Completed" | +/** + * Scan failed with error + */ +"Failed" | +/** + * Scan was paused + */ +"Paused"; + +/** + * Detailed breakdown of how the score was calculated + */ +export type ScoreBreakdown = { temporal_score: number; semantic_score: number | null; metadata_score: number; recency_boost: number; user_preference_boost: number; final_score: number }; + +/** + * A path within the Spacedrive Virtual Distributed File System + * + * This is the core abstraction that enables cross-device operations. + * An SdPath can represent: + * - A physical file at a specific path on a specific device + * - A content-addressed file that can be sourced from any device + * - A sidecar (derivative data) attached to content + * + * This enum-based approach enables resilient file operations by allowing + * content-based paths to be resolved to optimal physical locations at runtime. + */ +export type SdPath = +/** + * A direct pointer to a file at a specific path on a specific device + */ +{ Physical: { +/** + * The device slug (e.g., "jamies-macbook") + */ +device_slug: string; +/** + * The local path on that device + */ +path: string } } | +/** + * A cloud storage path within a cloud volume + */ +{ Cloud: { +/** + * The cloud service type (S3, GoogleDrive, etc.) + */ +service: CloudServiceType; +/** + * The cloud identifier (bucket name, drive name, etc.) + */ +identifier: string; +/** + * The cloud-native path (e.g., "bucket/key" for S3) + */ +path: string } } | +/** + * An abstract, location-independent handle that refers to file content + */ +{ Content: { +/** + * The unique content identifier + */ +content_id: string } } | +/** + * A derivative data file (thumbnail, OCR text, embedding, etc.) + * Sidecars are content-scoped and addressed by content + kind + variant + */ +{ Sidecar: { +/** + * The content this sidecar is derived from + */ +content_id: string; +/** + * The type of sidecar (thumb, ocr, embeddings, etc.) + */ +kind: SidecarKind; +/** + * The specific variant (e.g., "grid@2x", "1080p", "all-MiniLM-L6-v2") + */ +variant: SidecarVariant; +/** + * The storage format (webp, json, msgpack, etc.) + */ +format: SidecarFormat } }; + +/** + * A batch of SdPaths, useful for operations on multiple files + */ +export type SdPathBatch = { paths: SdPath[] }; + +/** + * Search facets for filtering UI + */ +export type SearchFacets = { file_types: { [key in string]: number }; tags: { [key in string]: number }; locations: { [key in string]: number }; date_ranges: { [key in string]: number }; size_ranges: { [key in string]: number } }; + +/** + * 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 }; + +/** + * Defines the search mode and performance characteristics + */ +export type SearchMode = +/** + * Fast, metadata-only search (<10ms) + */ +"Fast" | +/** + * Normal search with semantic ranking (<100ms) + */ +"Normal" | +/** + * Full search with content analysis (<500ms) + */ +"Full"; + +/** + * Defines the scope of the filesystem to search within + */ +export type SearchScope = +/** + * Search the entire library (default) + */ +"Library" | +/** + * Restrict search to a specific location by its ID + */ +{ Location: { location_id: string } } | +/** + * Restrict search to a specific directory path and all its descendants + */ +{ Path: { path: SdPath } }; + +export type SearchTagsInput = { +/** + * Search query (searches across all name variants) + */ +query: string; +/** + * Optional namespace filter + */ +namespace: string | null; +/** + * Optional tag type filter + */ +tag_type: TagType | null; +/** + * Whether to include archived/hidden tags + */ +include_archived: boolean | null; +/** + * Maximum number of results to return + */ +limit: number | null; +/** + * Whether to resolve ambiguous results using context + */ +resolve_ambiguous: boolean | null; +/** + * Context tags for disambiguation (UUIDs) + */ +context_tag_ids: string[] | null }; + +export type SearchTagsOutput = { +/** + * Tags found by the search + */ +tags: TagSearchResult[]; +/** + * Total number of results found (may be more than returned if limited) + */ +total_found: number; +/** + * Whether results were disambiguated using context + */ +disambiguated: boolean; +/** + * Search query that was executed + */ +query: string; +/** + * Applied filters + */ +filters: TagSearchFilters }; + +export type SerializablePairingState = "Idle" | "GeneratingCode" | "Broadcasting" | "Scanning" | "WaitingForConnection" | "Connecting" | "Authenticating" | "ExchangingKeys" | "AwaitingConfirmation" | "EstablishingSession" | "ChallengeReceived" | "ResponsePending" | "ResponseSent" | "Completed" | { Failed: { reason: string } }; + +export type ServiceState = { running: boolean; details: string | null }; + +export type ServiceStatus = { location_watcher: ServiceState; networking: ServiceState; volume_monitor: ServiceState; file_sharing: ServiceState }; + +/** + * Domain representation of a sidecar + */ +export type Sidecar = { id: number; content_uuid: string; kind: string; variant: string; format: string; status: string; size: number; created_at: string; updated_at: string }; + +/** + * Format for storing sidecar files + * + * Format selection guidelines: + * - Webp: Thumbnails and image derivatives (compressed images) + * - Mp4: Video/audio proxies (standard media format) + * - Json: Text-based structured data (OCR, transcripts) + * - MessagePack: Binary structured data (embeddings, vectors) + * - Text: Plain text extractions + * - Ply: 3D model format for Gaussian splats + * + * MessagePack is preferred for embeddings because: + * - 6x smaller than JSON (1.7KB vs 10KB per 384-dim vector) + * - 10x faster to parse + * - Already used in Spacedrive (job serialization) + * - Enables sub-30ms semantic search on 1M+ files + */ +export type SidecarFormat = "webp" | "mp_4" | "json" | "message_pack" | "text" | "ply"; + +export type SidecarKind = "thumb" | "thumbstrip" | "proxy" | "embeddings" | "ocr" | "transcript" | "gaussian_splat"; + +export type SidecarVariant = string; + +/** + * Filter for file size in bytes + */ +export type SizeRangeFilter = { min: number | null; max: number | null }; + +/** + * Sort direction + */ +export type SortDirection = "Asc" | "Desc"; + +/** + * Fields that can be used for sorting + */ +export type SortField = "Relevance" | "Name" | "Size" | "ModifiedAt" | "CreatedAt"; + +/** + * Sorting options for search results + */ +export type SortOptions = { field: SortField; direction: SortDirection }; + +/** + * A Space defines a sidebar layout and filtering context + */ +export type Space = { +/** + * Unique identifier + */ +id: string; +/** + * Human-friendly name (e.g., "All Devices", "Work Files") + */ +name: string; +/** + * Icon identifier (Phosphor icon name or emoji) + */ +icon: string; +/** + * Color for visual identification (hex format: #RRGGBB) + */ +color: string; +/** + * Sort order in space switcher + */ +order: number; +/** + * Timestamps + */ +created_at: string; updated_at: string }; + +export type SpaceCreateInput = { name: string; icon: string; color: string }; + +export type SpaceCreateOutput = { space: Space }; + +export type SpaceDeleteInput = { space_id: string }; + +export type SpaceDeleteOutput = { success: boolean }; + +export type SpaceGetOutput = { space: Space }; + +export type SpaceGetQueryInput = { space_id: string }; + +/** + * A SpaceGroup is a collapsible section in the sidebar + */ +export type SpaceGroup = { +/** + * Unique identifier + */ +id: string; +/** + * Space this group belongs to + */ +space_id: string; +/** + * Group name (e.g., "Quick Access", "MacBook Pro") + */ +name: string; +/** + * Type of group (determines content and behavior) + */ +group_type: GroupType; +/** + * Whether group is collapsed + */ +is_collapsed: boolean; +/** + * Sort order within space + */ +order: number; +/** + * Timestamp + */ +created_at: string }; + +/** + * A group with its items + */ +export type SpaceGroupWithItems = { +/** + * The group + */ +group: SpaceGroup; +/** + * Items in this group (sorted by order) + */ +items: SpaceItem[] }; + +/** + * An item within a space (can be space-level or within a group) + */ +export type SpaceItem = { +/** + * Unique identifier + */ +id: string; +/** + * Space this item belongs to + */ +space_id: string; +/** + * Group this item belongs to (None = space-level item) + */ +group_id: string | null; +/** + * Type discriminant (for quick type checking) + */ +item_type: ItemType; +/** + * Sort order within space or group + */ +order: number; +/** + * Timestamp + */ +created_at: string; +/** + * Resolved file data for Path items (populated by get_layout query) + */ +resolved_file?: File | null }; + +/** + * Complete sidebar layout for a space + */ +export type SpaceLayout = { +/** + * Unique identifier (same as space.id for cache matching) + */ +id: string; +/** + * The space + */ +space: Space; +/** + * Space-level items (pinned shortcuts, no group) + */ +space_items: SpaceItem[]; +/** + * Groups with their items + */ +groups: SpaceGroupWithItems[] }; + +export type SpaceLayoutQueryInput = { space_id: string }; + +export type SpaceUpdateInput = { space_id: string; name: string | null; icon: string | null; color: string | null }; + +export type SpaceUpdateOutput = { space: Space }; + +export type SpacedropSendInput = { device_id: string; paths: SdPath[]; sender: string | null }; + +export type SpacedropSendOutput = { job_id: string | null; session_id: string | null }; + +export type SpacesListOutput = { spaces: Space[] }; + +export type SpacesListQueryInput = null; + +/** + * Speech-to-text transcription policy + */ +export type SpeechPolicy = { +/** + * Whether to run speech-to-text on this location + */ +enabled: boolean; +/** + * Language for transcription + */ +language: string | null; +/** + * Model to use (e.g., "base", "small", "medium", "large") + */ +model: string; +/** + * Whether to reprocess files that already have transcriptions + */ +reprocess: boolean }; + +/** + * State transition event + */ +export type StateTransition = { from: DeviceSyncState; to: DeviceSyncState; timestamp: string; reason: string | null }; + +export type SuggestedLocation = { name: string; path: string; sd_path: SdPath }; + +export type SuggestedLocationsOutput = { locations: SuggestedLocation[] }; + +export type SuggestedLocationsQueryInput = null; + +/** + * Sync activity types for detailed sync monitoring + */ +export type SyncActivityType = { type: "BroadcastSent"; data: { changes: number } } | { type: "ChangesReceived"; data: { changes: number } } | { type: "ChangesApplied"; data: { changes: number } } | { type: "BackfillStarted" } | { type: "BackfillCompleted"; data: { records: number } } | { type: "CatchUpStarted" } | { type: "CatchUpCompleted" }; + +/** + * A logged sync event + */ +export type SyncEventLog = { id: number | null; timestamp: string; device_id: string; event_type: SyncEventType; category: EventCategory; severity: EventSeverity; summary: string; details?: JsonValue | null; correlation_id?: string | null; peer_device_id?: string | null; model_types?: string[] | null; record_count?: number | null; duration_ms?: number | null }; + +/** + * High-level sync event types + */ +export type SyncEventType = +/** + * State machine transition (Uninitialized → Backfilling → CatchingUp → Ready ⇄ Paused) + */ +"state_transition" | +/** + * Backfill session started + */ +"backfill_session_started" | +/** + * Backfill session completed successfully + */ +"backfill_session_completed" | +/** + * Backfill session failed + */ +"backfill_session_failed" | +/** + * Catch-up session started (incremental sync) + */ +"catch_up_session_started" | +/** + * Catch-up session completed + */ +"catch_up_session_completed" | +/** + * Batch of records ingested (aggregated, not per-record) + */ +"batch_ingestion" | +/** + * Sent backfill request to peer + */ +"backfill_request_sent" | +/** + * Received backfill request from peer + */ +"backfill_request_received" | +/** + * Sent backfill response to peer + */ +"backfill_response_sent" | +/** + * Peer device connected + */ +"peer_connected" | +/** + * Peer device disconnected + */ +"peer_disconnected" | +/** + * Sync error occurred + */ +"sync_error"; + +/** + * Point-in-time snapshot of all sync metrics + */ +export type SyncMetricsSnapshot = { +/** + * When this snapshot was taken + */ +timestamp: string; +/** + * State metrics + */ +state: SyncStateSnapshot; +/** + * Operation metrics + */ +operations: OperationSnapshot; +/** + * Data volume metrics + */ +data_volume: DataVolumeSnapshot; +/** + * Performance metrics + */ +performance: PerformanceSnapshot; +/** + * Error metrics + */ +errors: ErrorSnapshot }; + +/** + * State metrics snapshot + */ +export type SyncStateSnapshot = { current_state: DeviceSyncState; state_entered_at: string; uptime_seconds: number; state_history: StateTransition[]; total_time_in_state: ([DeviceSyncState, number])[]; transition_count: ([[DeviceSyncState, DeviceSyncState], number])[] }; + +export type SystemInfo = { uptime: number | null; data_directory: string; instance_name: string | null; current_library: string | null }; + +/** + * A tag with advanced capabilities for contextual organization + */ +export type Tag = { +/** + * Unique identifier + */ +id: string; +/** + * Core identity + */ +canonical_name: string; display_name: string | null; +/** + * Semantic variants for flexible access + */ +formal_name: string | null; abbreviation: string | null; aliases: string[]; +/** + * Context and categorization + */ +namespace: string | null; tag_type: TagType; +/** + * Visual and behavioral properties + */ +color: string | null; icon: string | null; description: string | null; +/** + * Advanced capabilities + */ +is_organizational_anchor: boolean; privacy_level: PrivacyLevel; search_weight: number; +/** + * Compositional attributes + */ +attributes: { [key in string]: JsonValue }; composition_rules: CompositionRule[]; +/** + * Metadata + */ +created_at: string; updated_at: string; created_by_device: string }; + +/** + * Filter for tags, supporting complex boolean logic + */ +export type TagFilter = { +/** + * Must have all of these tag IDs + */ +include: string[]; +/** + * Must not have any of these tag IDs + */ +exclude: string[] }; + +export type TagSearchFilters = { namespace: string | null; tag_type: string | null; include_archived: boolean; limit: number | null }; + +export type TagSearchResult = { +/** + * The semantic tag + */ +tag: Tag; +/** + * Relevance score (0.0-1.0) + */ +relevance: number; +/** + * Which name variant matched the search + */ +matched_variant: string | null; +/** + * Context score if disambiguation was used + */ +context_score: number | null }; + +/** + * Source of tag application + */ +export type TagSource = +/** + * Manually applied by user + */ +"User" | +/** + * Applied by AI analysis + */ +"AI" | +/** + * Imported from external source + */ +"Import" | +/** + * Synchronized from another device + */ +"Sync"; + +/** + * Specifies what to tag: content (all instances) or specific entries + */ +export type TagTargets = +/** + * Tag by content identity (applies to ALL instances of this content across devices) + * This is the preferred/default approach + */ +{ type: "Content"; ids: string[] } | +/** + * Tag by entry ID (applies to ONLY this specific file instance) + * Use when you want instance-specific tags + */ +{ type: "Entry"; ids: number[] }; + +/** + * Types of semantic tags with different behaviors + */ +export type TagType = +/** + * Standard user-created tag + */ +"Standard" | +/** + * Creates visual hierarchies in the interface + */ +"Organizational" | +/** + * Controls search and display visibility + */ +"Privacy" | +/** + * System-generated tag (AI, import, etc.) + */ +"System"; + +/** + * Text highlighting information + */ +export type TextHighlight = { field: string; text: string; start: number; end: number }; + +export type ThumbnailInput = { paths: string[]; size: number; quality: number }; + +/** + * Thumbnail generation policy + */ +export type ThumbnailPolicy = { +/** + * Whether to generate thumbnails for this location + */ +enabled: boolean; +/** + * Specific thumbnail sizes to generate (empty = use defaults) + */ +sizes: number[]; +/** + * JPEG quality (0-100) + */ +quality: number; +/** + * Whether to regenerate existing thumbnails + */ +regenerate: boolean }; + +/** + * Thumbstrip generation policy + */ +export type ThumbstripPolicy = { +/** + * Whether to generate thumbstrips for this location + */ +enabled: boolean; +/** + * Whether to regenerate existing thumbstrips + */ +regenerate: boolean }; + +export type TranscribeAudioInput = { entry_uuid: string; model: string | null; language: string | null }; + +export type TranscribeAudioOutput = { +/** + * Job ID for tracking transcription progress + */ +job_id: string }; + +/** + * Statistics for the unified ephemeral index + */ +export type UnifiedIndexStats = { +/** + * Total entries in the shared arena + */ +total_entries: number; +/** + * Number of entries indexed by path + */ +path_index_count: number; +/** + * Number of unique interned names (shared across all paths) + */ +unique_names: number; +/** + * Number of interned strings in shared cache + */ +interned_strings: number; +/** + * Number of content kinds stored + */ +content_kinds: number; +/** + * Estimated memory usage in bytes + */ +memory_bytes: number; +/** + * Age of the cache in seconds + */ +age_seconds: number; +/** + * Seconds since last access + */ +idle_seconds: number }; + +/** + * Input for finding files unique to a location + */ +export type UniqueToLocationInput = { +/** + * The location ID to find unique files for + */ +location_id: string; +/** + * Optional limit on number of results + */ +limit: number | null }; + +/** + * Output containing files that are unique to the specified location + */ +export type UniqueToLocationOutput = { +/** + * Files that exist only in the specified location + */ +unique_files: File[]; +/** + * Total count of unique files + */ +total_count: number; +/** + * Total size of unique files in bytes + */ +total_size: number }; + +export type UpdateGroupInput = { group_id: string; name: string | null; is_collapsed: boolean | null }; + +export type UpdateGroupOutput = { group: SpaceGroup }; + +/** + * Input for location path validation + */ +export type ValidateLocationPathInput = { path: SdPath }; + +/** + * Output from location path validation + */ +export type ValidateLocationPathOutput = { +/** + * Whether this path is recommended for use as a location + */ +is_recommended: boolean; +/** + * Risk level assessment + */ +risk_level: RiskLevel; +/** + * List of warnings (empty if no issues) + */ +warnings: ValidationWarning[]; +/** + * Alternative suggestion to use volume indexing + */ +suggested_alternative: VolumeIndexingSuggestion | null; +/** + * Path depth from root (number of components) + */ +path_depth: number; +/** + * Whether path is on the primary system volume + */ +is_on_primary_volume: boolean }; + +/** + * A validation warning message + */ +export type ValidationWarning = { message: string; suggestion: string | null }; + +/** + * Video metadata extracted from FFmpeg + */ +export type VideoMediaData = { uuid: string; width: number; height: number; blurhash: string | null; duration_seconds: number | null; bit_rate: number | null; codec: string | null; pixel_format: string | null; color_space: string | null; color_range: string | null; color_primaries: string | null; color_transfer: string | null; fps_num: number | null; fps_den: number | null; audio_codec: string | null; audio_channels: string | null; audio_sample_rate: number | null; audio_bit_rate: number | null; title: string | null; artist: string | null; album: string | null; creation_time: string | null; date_captured: string | null }; + +/** + * A volume in Spacedrive - unified model for runtime and database + */ +export type Volume = { +/** + * Unique identifier (used in SdPath addressing) + */ +id: string; +/** + * Volume fingerprint for identification + */ +fingerprint: VolumeFingerprint; +/** + * Device this volume is attached to + */ +device_id: string; +/** + * Human-readable name + */ +name: string; +/** + * Library this volume belongs to (None for untracked volumes) + */ +library_id: string | null; +/** + * Whether this volume is being tracked by Spacedrive + */ +is_tracked: boolean; +/** + * Primary mount point + */ +mount_point: string; +/** + * Additional mount points for the same volume + */ +mount_points: string[]; +/** + * Volume type/category + */ +volume_type: VolumeType; +/** + * Mount type classification + */ +mount_type: MountType; +/** + * Disk type (SSD, HDD, etc.) + */ +disk_type: DiskType; +/** + * Filesystem type + */ +file_system: FileSystem; +/** + * Total capacity in bytes + */ +total_capacity: number; +/** + * Currently available space in bytes + */ +available_space: number; +/** + * Whether volume is read-only + */ +is_read_only: boolean; +/** + * Whether volume is currently mounted/available + */ +is_mounted: boolean; +/** + * Hardware identifier (device path, UUID, etc.) + */ +hardware_id: string | null; +/** + * Cloud identifier (bucket/drive/container name) for cloud volumes + * This is separate from mount_point to allow display names with suffixes + * while maintaining the correct cloud resource identifier for backend operations + */ +cloud_identifier: string | null; +/** + * Cloud service configuration (service-specific settings like region, endpoint) + */ +cloud_config: JsonValue | null; +/** + * APFS container information (macOS only) + */ +apfs_container: ApfsContainer | null; +/** + * Container-relative volume ID for same-container detection + */ +container_volume_id: string | null; +/** + * Path resolution mappings (for firmlinks/symlinks) + */ +path_mappings: PathMapping[]; +/** + * Whether this volume should be visible in default views + */ +is_user_visible: boolean; +/** + * Whether this volume should be auto-tracked + */ +auto_track_eligible: boolean; +/** + * Performance metrics + */ +read_speed_mbps: number | null; write_speed_mbps: number | null; +/** + * Timestamps + */ +created_at: string; updated_at: string; last_seen_at: string; +/** + * Statistics + */ +total_files: number | null; total_directories: number | null; last_stats_update: string | null; +/** + * User preferences + */ +display_name: string | null; is_favorite: boolean; color: string | null; icon: string | null; +/** + * Error state + */ +error_message: string | null }; + +export type VolumeAddCloudInput = { service: CloudServiceType; display_name: string; config: CloudStorageConfig }; + +export type VolumeAddCloudOutput = { fingerprint: VolumeFingerprint; volume_name: string; service: CloudServiceType }; + +export type VolumeFilter = +/** + * Only return tracked volumes + */ +"TrackedOnly" | +/** + * Only return untracked volumes + */ +"UntrackedOnly" | +/** + * Return all volumes (tracked and untracked) + */ +"All"; + +/** + * Unique fingerprint for a storage volume + */ +export type VolumeFingerprint = string; + +/** + * Suggestion to use volume indexing instead + */ +export type VolumeIndexingSuggestion = { volume_fingerprint: string; volume_name: string; message: string }; + +/** + * Summary information about a volume (for updates and caching) + */ +export type VolumeInfo = { is_mounted: boolean; total_bytes_available: number; read_speed_mbps: number | null; write_speed_mbps: number | null; error_status: string | null }; + +export type VolumeItem = { id: string; name: string; fingerprint: VolumeFingerprint; volume_type: string; mount_point: string | null; +/** + * Whether this volume is currently tracked in the library + */ +is_tracked: boolean; +/** + * Whether this volume is currently online/mounted + */ +is_online: boolean; +/** + * Total capacity in bytes + */ +total_capacity: number | null; +/** + * Available capacity in bytes + */ +available_capacity: number | null; +/** + * Unique bytes (deduplicated by content_identity) + */ +unique_bytes: number | null; +/** + * Filesystem type (APFS, NTFS, ext4, etc.) + */ +file_system: string | null; +/** + * Disk type (SSD, HDD, etc.) + */ +disk_type: string | null; +/** + * Read speed in MB/s + */ +read_speed_mbps: number | null; +/** + * Write speed in MB/s + */ +write_speed_mbps: number | null; +/** + * Device ID that owns this volume + */ +device_id: string; +/** + * Device slug for constructing SdPaths + */ +device_slug: string }; + +export type VolumeListOutput = { volumes: VolumeItem[] }; + +export type VolumeListQueryInput = { +/** + * Filter volumes by tracking status (default: TrackedOnly) + */ +filter?: VolumeFilter }; + +export type VolumeRefreshInput = { +/** + * Optional: Set to true to force recalculation even if recently calculated + */ +force?: boolean }; + +export type VolumeRefreshOutput = { +/** + * Number of volumes that had their unique_bytes calculated + */ +volumes_refreshed: number; +/** + * Number of volumes that failed to refresh + */ +volumes_failed: number }; + +export type VolumeRemoveCloudInput = { fingerprint: VolumeFingerprint }; + +export type VolumeRemoveCloudOutput = { fingerprint: VolumeFingerprint }; + +export type VolumeSpeedTestInput = { fingerprint: VolumeFingerprint }; + +/** + * Output from volume speed test operation + */ +export type VolumeSpeedTestOutput = { +/** + * The fingerprint of the tested volume + */ +fingerprint: VolumeFingerprint; +/** + * Read speed in MB/s (if measured) + */ +read_speed_mbps: number | null; +/** + * Write speed in MB/s (if measured) + */ +write_speed_mbps: number | null }; + +export type VolumeTrackInput = { +/** + * Fingerprint of the volume to track + */ +fingerprint: string; +/** + * Optional custom display name + */ +display_name: string | null }; + +export type VolumeTrackOutput = { +/** + * UUID of the tracked volume + */ +volume_id: string; +/** + * Fingerprint of the volume + */ +fingerprint: string; +/** + * Display name + */ +name: string; +/** + * Whether the volume is currently online + */ +is_online: boolean }; + +/** + * Volume type classification + */ +export type VolumeType = +/** + * Primary system drive containing OS and user data + */ +"Primary" | +/** + * Dedicated user data volumes (separate from OS) + */ +"UserData" | +/** + * External or removable storage devices + */ +"External" | +/** + * Secondary internal storage (additional drives/partitions) + */ +"Secondary" | +/** + * System/OS internal volumes (hidden from normal view) + */ +"System" | +/** + * Network attached storage + */ +"Network" | +/** + * Cloud storage mounts + */ +"Cloud" | +/** + * Virtual/temporary storage + */ +"Virtual" | +/** + * Unknown or unclassified volumes + */ +"Unknown"; + +export type VolumeUntrackInput = { +/** + * UUID of the volume to untrack + */ +volume_id: string }; + +export type VolumeUntrackOutput = { +/** + * UUID of the untracked volume + */ +volume_id: string; +/** + * Whether the operation was successful + */ +success: boolean }; +// ===== API Type Unions ===== + +export type CoreAction = + { type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput } + | { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput } + | { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput } + | { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput } + | { type: 'libraries.open'; input: LibraryOpenInput; output: LibraryOpenOutput } + | { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput } + | { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput } + | { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput } + | { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput } + | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } + | { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput } + | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } + | { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput } + | { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput } + | { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput } +; + +export type LibraryAction = + { type: 'files.copy'; input: FileCopyInput; output: JobReceipt } + | { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput } + | { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } + | { type: 'files.rename'; input: FileRenameInput; output: JobReceipt } + | { type: 'indexing.start'; input: IndexInput; output: JobReceipt } + | { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } + | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } + | { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput } + | { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput } + | { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput } + | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } + | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } + | { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput } + | { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput } + | { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput } + | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } + | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } + | { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput } + | { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput } + | { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput } + | { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput } + | { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput } + | { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput } + | { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt } + | { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput } + | { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput } + | { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput } + | { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput } + | { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput } + | { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput } + | { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput } + | { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput } + | { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput } + | { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput } + | { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput } + | { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput } + | { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } + | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } + | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } + | { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput } + | { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput } + | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } + | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } + | { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput } + | { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput } +; + +export type CoreQuery = + { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus } + | { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput } + | { type: 'core.status'; input: Empty; output: CoreStatus } + | { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput } + | { type: 'jobs.remote.for_device'; input: RemoteJobsForDeviceInput; output: RemoteJobsForDeviceOutput } + | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } + | { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput } + | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } + | { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput } + | { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } + | { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } +; + +export type LibraryQuery = + { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] } + | { type: 'files.alternate_instances'; input: AlternateInstancesInput; output: AlternateInstancesOutput } + | { type: 'files.by_id'; input: FileByIdQuery; output: File } + | { type: 'files.by_path'; input: FileByPathQuery; output: File } + | { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput } + | { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } + | { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput } + | { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput } + | { type: 'jobs.active'; input: ActiveJobsInput; output: ActiveJobsOutput } + | { type: 'jobs.get_copy_metadata'; input: CopyMetadataQueryInput; output: CopyMetadataOutput } + | { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput } + | { type: 'jobs.list'; input: JobListInput; output: JobListOutput } + | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library } + | { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } + | { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } + | { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput } + | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } + | { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput } + | { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout } + | { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput } + | { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput } + | { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput } + | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } + | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + | { type: 'test.ping'; input: PingInput; output: PingOutput } + | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } +; + +// ===== Wire Method Mappings ===== + +export const WIRE_METHODS = { + coreActions: { + 'core.ephemeral_reset': 'action:core.ephemeral_reset.input', + 'core.reset': 'action:core.reset.input', + 'libraries.create': 'action:libraries.create.input', + 'libraries.delete': 'action:libraries.delete.input', + 'libraries.open': 'action:libraries.open.input', + 'models.whisper.delete': 'action:models.whisper.delete.input', + 'models.whisper.download': 'action:models.whisper.download.input', + 'network.device.revoke': 'action:network.device.revoke.input', + 'network.pair.cancel': 'action:network.pair.cancel.input', + 'network.pair.generate': 'action:network.pair.generate.input', + 'network.pair.join': 'action:network.pair.join.input', + 'network.spacedrop.send': 'action:network.spacedrop.send.input', + 'network.start': 'action:network.start.input', + 'network.stop': 'action:network.stop.input', + 'network.sync_setup': 'action:network.sync_setup.input', + }, + + libraryActions: { + 'files.copy': 'action:files.copy.input', + 'files.createFolder': 'action:files.createFolder.input', + 'files.delete': 'action:files.delete.input', + 'files.rename': 'action:files.rename.input', + 'indexing.start': 'action:indexing.start.input', + 'indexing.verify': 'action:indexing.verify.input', + 'jobs.cancel': 'action:jobs.cancel.input', + 'jobs.pause': 'action:jobs.pause.input', + 'jobs.resume': 'action:jobs.resume.input', + 'libraries.export': 'action:libraries.export.input', + 'libraries.rename': 'action:libraries.rename.input', + 'locations.add': 'action:locations.add.input', + 'locations.enable_indexing': 'action:locations.enable_indexing.input', + 'locations.export': 'action:locations.export.input', + 'locations.import': 'action:locations.import.input', + 'locations.remove': 'action:locations.remove.input', + 'locations.rescan': 'action:locations.rescan.input', + 'locations.triggerJob': 'action:locations.triggerJob.input', + 'locations.update': 'action:locations.update.input', + 'media.ocr.extract': 'action:media.ocr.extract.input', + 'media.proxy.generate': 'action:media.proxy.generate.input', + 'media.speech.transcribe': 'action:media.speech.transcribe.input', + 'media.splat.generate': 'action:media.splat.generate.input', + 'media.thumbnail': 'action:media.thumbnail.input', + 'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input', + 'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input', + 'spaces.add_group': 'action:spaces.add_group.input', + 'spaces.add_item': 'action:spaces.add_item.input', + 'spaces.create': 'action:spaces.create.input', + 'spaces.delete': 'action:spaces.delete.input', + 'spaces.delete_group': 'action:spaces.delete_group.input', + 'spaces.delete_item': 'action:spaces.delete_item.input', + 'spaces.reorder_groups': 'action:spaces.reorder_groups.input', + 'spaces.reorder_items': 'action:spaces.reorder_items.input', + 'spaces.update': 'action:spaces.update.input', + 'spaces.update_group': 'action:spaces.update_group.input', + 'tags.apply': 'action:tags.apply.input', + 'tags.create': 'action:tags.create.input', + 'volumes.add_cloud': 'action:volumes.add_cloud.input', + 'volumes.index': 'action:volumes.index.input', + 'volumes.refresh': 'action:volumes.refresh.input', + 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', + 'volumes.speed_test': 'action:volumes.speed_test.input', + 'volumes.track': 'action:volumes.track.input', + 'volumes.untrack': 'action:volumes.untrack.input', + }, + + coreQueries: { + 'core.ephemeral_status': 'query:core.ephemeral_status', + 'core.events.list': 'query:core.events.list', + 'core.status': 'query:core.status', + 'jobs.remote.all_devices': 'query:jobs.remote.all_devices', + 'jobs.remote.for_device': 'query:jobs.remote.for_device', + 'libraries.list': 'query:libraries.list', + 'models.whisper.list': 'query:models.whisper.list', + 'network.devices.list': 'query:network.devices.list', + 'network.pair.status': 'query:network.pair.status', + 'network.status': 'query:network.status', + 'network.sync_setup.discover': 'query:network.sync_setup.discover', + }, + + libraryQueries: { + 'devices.list': 'query:devices.list', + 'files.alternate_instances': 'query:files.alternate_instances', + 'files.by_id': 'query:files.by_id', + 'files.by_path': 'query:files.by_path', + 'files.content_kind_stats': 'query:files.content_kind_stats', + 'files.directory_listing': 'query:files.directory_listing', + 'files.media_listing': 'query:files.media_listing', + 'files.unique_to_location': 'query:files.unique_to_location', + 'jobs.active': 'query:jobs.active', + 'jobs.get_copy_metadata': 'query:jobs.get_copy_metadata', + 'jobs.info': 'query:jobs.info', + 'jobs.list': 'query:jobs.list', + 'libraries.info': 'query:libraries.info', + 'locations.list': 'query:locations.list', + 'locations.suggested': 'query:locations.suggested', + 'locations.validate_path': 'query:locations.validate_path', + 'search.files': 'query:search.files', + 'spaces.get': 'query:spaces.get', + 'spaces.get_layout': 'query:spaces.get_layout', + 'spaces.list': 'query:spaces.list', + 'sync.activity': 'query:sync.activity', + 'sync.eventLog': 'query:sync.eventLog', + 'sync.metrics': 'query:sync.metrics', + 'tags.search': 'query:tags.search', + 'test.ping': 'query:test.ping', + 'volumes.list': 'query:volumes.list', + }, + +} as const; diff --git a/core/packages/ts-client/src/generated/types.ts b/core/packages/ts-client/src/generated/types.ts new file mode 100644 index 000000000..8b77de44c --- /dev/null +++ b/core/packages/ts-client/src/generated/types.ts @@ -0,0 +1,4599 @@ +// Generated by Spacedrive using Specta + rspc-inspired type extraction - DO NOT EDIT +// This file is auto-generated. See core/src/bin/generate_typescript_types.rs + +// Empty type for operations with no input +export type Empty = Record; + +// This file has been generated by Specta. DO NOT EDIT. + +export type ActionContextInfo = { action_type: string; initiated_at: string; initiated_by: string | null; action_input: JsonValue; context: JsonValue }; + +export type ActiveJobItem = { id: string; name: string; status: JobStatus; progress: number; action_type: string | null; action_context: ActionContextInfo | null }; + +export type ActiveJobsInput = Record; + +export type ActiveJobsOutput = { jobs: ActiveJobItem[]; running_count: number; paused_count: number }; + +export type AddGroupInput = { space_id: string; name: string; group_type: GroupType }; + +export type AddGroupOutput = { group: SpaceGroup }; + +export type AddItemInput = { space_id: string; group_id: string | null; item_type: ItemType }; + +export type AddItemOutput = { item: SpaceItem }; + +/** + * Input for alternate instances query + */ +export type AlternateInstancesInput = { +/** + * The entry UUID to find alternates for + */ +entry_uuid: string }; + +/** + * Output containing alternate instances + */ +export type AlternateInstancesOutput = { +/** + * All instances of this file (including the original) + */ +instances: File[]; +/** + * Total number of instances found + */ +total_count: number }; + +/** + * Represents an APFS container (physical storage with multiple volumes) + */ +export type ApfsContainer = { container_id: string; uuid: string; physical_store: string; total_capacity: number; capacity_in_use: number; capacity_free: number; volumes: ApfsVolumeInfo[] }; + +/** + * APFS volume information within a container + */ +export type ApfsVolumeInfo = { disk_id: string; uuid: string; role: ApfsVolumeRole; name: string; mount_point: string | null; snapshot_mount_point: string | null; capacity_consumed: number; sealed: boolean; filevault: boolean }; + +/** + * APFS volume roles in the container + */ +export type ApfsVolumeRole = "System" | "Data" | "Preboot" | "Recovery" | "VM" | { Other: string }; + +export type ApplyTagsInput = { +/** + * What to tag: content identities or specific entries + */ +targets: TagTargets; +/** + * Tag IDs to apply + */ +tag_ids: string[]; +/** + * Source of the tag application + */ +source: TagSource | null; +/** + * Confidence score (for AI-applied tags) + */ +confidence: number | null; +/** + * Context when applying (e.g., "image_analysis", "user_input") + */ +applied_context: string | null; +/** + * Instance-specific attributes for this application + */ +instance_attributes: { [key in string]: JsonValue } | null }; + +export type ApplyTagsOutput = { +/** + * Number of entries that had tags applied + */ +entries_affected: number; +/** + * Number of tags that were applied + */ +tags_applied: number; +/** + * Tag IDs that were successfully applied + */ +applied_tag_ids: string[]; +/** + * Entry IDs that were successfully tagged + */ +tagged_entry_ids: number[]; +/** + * Any warnings or notes about the operation + */ +warnings: string[]; +/** + * Success message + */ +message: string }; + +/** + * Targets for immediately applying a newly created tag + */ +export type ApplyToTargets = +/** + * Apply to content identities (all instances) + */ +{ type: "Content"; ids: string[] } | +/** + * Apply to specific entries (single instance) + */ +{ type: "Entry"; ids: number[] }; + +/** + * Audio metadata extracted from FFmpeg + */ +export type AudioMediaData = { uuid: string; duration_seconds: number | null; bit_rate: number | null; sample_rate: number | null; channels: string | null; codec: string | null; title: string | null; artist: string | null; album: string | null; album_artist: string | null; genre: string | null; year: number | null; track_number: number | null; disc_number: number | null; composer: string | null; publisher: string | null; copyright: string | null }; + +/** + * Cloud service type identifier + */ +export type CloudServiceType = "s3" | "gdrive" | "dropbox" | "onedrive" | "gcs" | "azblob" | "b2" | "wasabi" | "spaces" | "cloud"; + +export type CloudStorageConfig = { type: "S3"; bucket: string; region: string; access_key_id: string; secret_access_key: string; endpoint: string | null } | +/** + * Google Drive with OAuth 2.0 credentials. + * Requires both access_token and refresh_token for automatic token renewal. + */ +{ type: "GoogleDrive"; root: string | null; access_token: string; refresh_token: string; client_id: string; client_secret: string } | +/** + * OneDrive with OAuth 2.0 credentials. + * Requires both access_token and refresh_token for automatic token renewal. + */ +{ type: "OneDrive"; root: string | null; access_token: string; refresh_token: string; client_id: string; client_secret: string } | +/** + * Dropbox with OAuth 2.0 refresh token for long-term access. + * OpenDAL automatically obtains and refreshes access tokens as needed. + * Only refresh_token is required (not access_token). + */ +{ type: "Dropbox"; root: string | null; refresh_token: string; client_id: string; client_secret: string } | { type: "AzureBlob"; container: string; endpoint: string | null; account_name: string; account_key: string } | { type: "GoogleCloudStorage"; bucket: string; root: string | null; endpoint: string | null; credential: string }; + +/** + * Operators for combining tag attributes + */ +export type CompositionOperator = +/** + * All conditions must be true + */ +"And" | +/** + * Any condition must be true + */ +"Or" | +/** + * Must have this property + */ +"With" | +/** + * Must not have this property + */ +"Without"; + +/** + * Rules for composing attributes from multiple tags + */ +export type CompositionRule = { operator: CompositionOperator; operands: string[]; result_attribute: string }; + +/** + * Network connection method for a device + */ +export type ConnectionMethod = +/** + * Direct peer-to-peer connection (mDNS/local network) + */ +"Direct" | +/** + * Connection via relay server + */ +"Relay" | +/** + * Mixed connection (both direct and relay) + */ +"Mixed"; + +/** + * Domain representation of content identity + */ +export type ContentIdentity = { uuid: string; kind: ContentKind; content_hash: string; integrity_hash: string | null; mime_type_id: number | null; text_content: string | null; total_size: number; entry_count: number; first_seen_at: string; last_verified_at: string }; + +/** + * Type of content + */ +export type ContentKind = "unknown" | "image" | "video" | "audio" | "document" | "archive" | "code" | "text" | "database" | "book" | "font" | "mesh" | "config" | "encrypted" | "key" | "executable" | "binary" | "spreadsheet" | "presentation" | "email" | "calendar" | "contact" | "web" | "shortcut" | "package" | "model_entry" | "memory"; + +/** + * A single content kind with its file count + */ +export type ContentKindStat = { +/** + * The content kind (image, video, audio, etc.) + */ +kind: ContentKind; +/** + * The name of the content kind + */ +name: string; +/** + * The number of files with this content kind + */ +file_count: number }; + +/** + * Input for content kind statistics query + */ +export type ContentKindStatsInput = Record; + +/** + * Output containing content kind statistics + */ +export type ContentKindStatsOutput = { +/** + * Statistics for each content kind + */ +stats: ContentKindStat[]; +/** + * Total number of files across all content kinds + */ +total_files: number }; + +/** + * Metadata for a single file or directory in the copy operation. + * For directories, this represents the entire directory (not flattened). + */ +export type CopyFileEntry = { +/** + * Source path + */ +source_path: SdPath; +/** + * Destination path + */ +dest_path: SdPath; +/** + * Total size in bytes (for directories, this is the recursive total) + */ +size_bytes: number; +/** + * Whether this entry is a directory + */ +is_directory: boolean; +/** + * Current status of this file/directory + */ +status: CopyFileStatus; +/** + * Error message if status is Failed + */ +error: string | null; +/** + * Entry UUID if source is in database (for building File objects) + */ +entry_id: string | null }; + +/** + * Status of a file in the copy operation. + */ +export type CopyFileStatus = +/** + * File is waiting to be copied + */ +"pending" | +/** + * File is currently being copied + */ +"copying" | +/** + * File has been successfully copied + */ +"completed" | +/** + * File copy failed + */ +"failed" | +/** + * File was skipped (already exists or user choice) + */ +"skipped"; + +/** + * Full metadata for a copy job, queryable via jobs.get_copy_metadata. + */ +export type CopyJobMetadata = { +/** + * Strategy metadata (name, description, flags) + */ +strategy: CopyStrategyMetadata | null; +/** + * List of files/directories being copied + */ +files: CopyFileEntry[]; +/** + * Total bytes across all files + */ +total_bytes: number; +/** + * Total file count (actual files, not directories) + */ +total_file_count: number; +/** + * Whether this is a move operation + */ +is_move_operation: boolean; +/** + * Full File domain objects (populated by query, not stored in job) + */ +file_objects?: File[] }; + +/** + * Output from the copy metadata query. + */ +export type CopyMetadataOutput = { +/** + * The copy job metadata, if the job exists and is a copy job + */ +metadata: CopyJobMetadata | null; +/** + * Error message if the job is not a copy job or doesn't have metadata + */ +error: string | null }; + +/** + * Input for the copy metadata query. + */ +export type CopyMetadataQueryInput = { +/** + * The job ID to query metadata for + */ +job_id: string }; + +/** + * Copy method preference for file operations + */ +export type CopyMethod = +/** + * Automatically select the best method based on source and destination + */ +"Auto" | +/** + * Use atomic operations (rename for moves, APFS clone for copies, etc.) + */ +"Atomic" | +/** + * Use streaming copy/move (works across all scenarios) + */ +"Streaming"; + +/** + * Metadata about the selected copy strategy for UI display. + */ +export type CopyStrategyMetadata = { +/** + * Internal strategy name (e.g., "LocalMove", "FastCopy", "LocalStream", "RemoteTransfer") + */ +strategy_name: string; +/** + * Human-readable description (e.g., "Atomic move (same storage)") + */ +strategy_description: string; +/** + * Whether operation crosses device boundaries + */ +is_cross_device: boolean; +/** + * Whether operation crosses volume/partition boundaries on same device + */ +is_cross_volume: boolean; +/** + * Whether this is expected to be a fast operation (instant or near-instant) + */ +is_fast_operation: boolean; +/** + * The copy method used (Auto, Atomic, Streaming) + */ +copy_method: CopyMethod }; + +export type CoreStatus = { version: string; built_at: string; library_count: number; device_info: DeviceInfo; libraries: LibraryInfo[]; services: ServiceStatus; network: NetworkStatus; system: SystemInfo }; + +/** + * Input for creating a new folder + */ +export type CreateFolderInput = { +/** + * Parent directory where the folder will be created + */ +parent: SdPath; +/** + * Name for the new folder + */ +name: string; +/** + * Optional items to move into the new folder after creation + */ +items?: SdPath[] }; + +/** + * Output from creating a folder + */ +export type CreateFolderOutput = { +/** + * Path to the created folder + */ +folder_path: SdPath; +/** + * Job receipt if items were moved into the folder + */ +job_receipt?: JobReceipt | null }; + +export type CreateTagInput = { +/** + * The canonical name for this tag + */ +canonical_name: string; +/** + * Optional display name (if different from canonical) + */ +display_name: string | null; +/** + * Semantic variants + */ +formal_name: string | null; abbreviation: string | null; aliases: string[]; +/** + * Context and categorization + */ +namespace: string | null; tag_type: TagType | null; +/** + * Visual properties + */ +color: string | null; icon: string | null; description: string | null; +/** + * Advanced capabilities + */ +is_organizational_anchor: boolean | null; privacy_level: PrivacyLevel | null; search_weight: number | null; +/** + * Initial attributes + */ +attributes: { [key in string]: JsonValue } | null; +/** + * Optional: Targets to immediately apply this tag to after creation + */ +apply_to: ApplyToTargets | null }; + +export type CreateTagOutput = { +/** + * The created tag's UUID + */ +tag_id: string; +/** + * The canonical name of the created tag + */ +canonical_name: string; +/** + * The namespace if specified + */ +namespace: string | null; +/** + * Success message + */ +message: string }; + +/** + * Data volume metrics snapshot + */ +export type DataVolumeSnapshot = { entries_synced: { [key in string]: number }; entries_by_device: { [key in string]: DeviceMetricsSnapshot }; bytes_sent: number; bytes_received: number; last_sync_per_peer: { [key in string]: string }; last_sync_per_model: { [key in string]: string } }; + +/** + * Time-based fields that can be filtered + */ +export type DateField = "CreatedAt" | "ModifiedAt" | "AccessedAt"; + +/** + * Filter for a time-based field + */ +export type DateRangeFilter = { field: DateField; start: string | null; end: string | null }; + +export type DeleteGroupInput = { group_id: string }; + +export type DeleteGroupOutput = { success: boolean }; + +export type DeleteItemInput = { item_id: string }; + +export type DeleteItemOutput = { success: boolean }; + +export type DeleteWhisperModelInput = { model: string }; + +export type DeleteWhisperModelOutput = { deleted: boolean }; + +/** + * A device running Spacedrive + * + * This is the canonical device type used throughout the application. + * It represents both database-registered devices and network-paired devices. + */ +export type Device = { +/** + * Unique identifier for this device + */ +id: string; +/** + * Human-readable name + */ +name: string; +/** + * Unique slug for URI addressing (e.g., "jamies-macbook") + */ +slug: string; +/** + * Operating system + */ +os: OperatingSystem; +/** + * Operating system version + */ +os_version: string | null; +/** + * Hardware model (e.g., "MacBook Pro", "iPhone 15") + */ +hardware_model: string | null; +/** + * CPU model name (e.g., "Apple M3 Max", "Intel Core i9-13900K") + */ +cpu_model: string | null; +/** + * CPU architecture (e.g., "arm64", "x86_64") + */ +cpu_architecture: string | null; +/** + * Number of physical CPU cores + */ +cpu_cores_physical: number | null; +/** + * Number of logical CPU cores (with hyperthreading) + */ +cpu_cores_logical: number | null; +/** + * CPU base frequency in MHz + */ +cpu_frequency_mhz: number | null; +/** + * Total system memory in bytes + */ +memory_total_bytes: number | null; +/** + * Device form factor + */ +form_factor: DeviceFormFactor | null; +/** + * Device manufacturer (e.g., "Apple", "Dell", "Lenovo") + */ +manufacturer: string | null; +/** + * GPU model names (can have multiple GPUs) + */ +gpu_models: string[] | null; +/** + * Boot disk type (e.g., "SSD", "HDD", "NVMe") + */ +boot_disk_type: string | null; +/** + * Boot disk capacity in bytes + */ +boot_disk_capacity_bytes: number | null; +/** + * Total swap space in bytes + */ +swap_total_bytes: number | null; +/** + * Network addresses for P2P connections + */ +network_addresses: string[]; +/** + * Device capabilities (indexing, P2P, volume detection, etc.) + */ +capabilities: JsonValue; +/** + * Whether this device is currently online + */ +is_online: boolean; +/** + * Last time this device was seen + */ +last_seen_at: string; +/** + * Whether sync is enabled for this device + */ +sync_enabled: boolean; +/** + * Last time this device synced + */ +last_sync_at: string | null; +/** + * When this device was first added + */ +created_at: string; +/** + * When this device info was last updated + */ +updated_at: string; +/** + * Whether this is the current device (computed) + */ +is_current?: boolean; +/** + * Whether this device is paired via network but not in library DB + */ +is_paired?: boolean; +/** + * Whether this device is currently connected via network + */ +is_connected?: boolean; +/** + * Connection method when connected (Direct, Relay, or Mixed) + */ +connection_method?: ConnectionMethod | null }; + +/** + * Device form factor types + */ +export type DeviceFormFactor = "Desktop" | "Laptop" | "Mobile" | "Tablet" | "Server" | "Other"; + +export type DeviceInfo = { id: string; name: string; os: string; hardware_model: string | null; created_at: string }; + +/** + * Device metrics snapshot + */ +export type DeviceMetricsSnapshot = { device_id: string; device_name: string; entries_received: number; last_seen: string; is_online: boolean }; + +export type DeviceRevokeInput = { device_id: string; +/** + * Whether to also remove the device from all library databases + * + * If false (default), only unpairs from network but keeps device history in libraries. + * If true, completely removes device from libraries (deletes all records). + */ +remove_from_library?: boolean }; + +export type DeviceRevokeOutput = { revoked: boolean }; + +/** + * Device sync state for state machine + */ +export type DeviceSyncState = +/** + * Not yet synced, no backfill started + */ +"Uninitialized" | +/** + * Currently backfilling from peer(s) + * Buffers all live updates during this phase + */ +{ Backfilling: { peer: string; progress: number } } | +/** + * Backfill complete, processing buffered updates + * Still buffers new updates while catching up + */ +{ CatchingUp: { buffered_count: number } } | +/** + * Fully synced, applying live updates immediately + */ +"Ready" | +/** + * Sync paused (offline or user disabled) + */ +"Paused"; + +/** + * Input for directory listing + */ +export type DirectoryListingInput = { +/** + * The directory path to list contents for + */ +path: SdPath; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Whether to include hidden files (default: false) + */ +include_hidden: boolean | null; +/** + * Sort order for results + */ +sort_by: DirectorySortBy; +/** + * Whether to show folders before files (default: false) + */ +folders_first: boolean | null }; + +/** + * Output containing directory contents + */ +export type DirectoryListingOutput = { +/** + * Direct children of the directory as File objects + */ +files: File[]; +/** + * Total count of direct children + */ +total_count: number; +/** + * Whether this directory has more children than returned + */ +has_more: boolean }; + +/** + * Sort options for directory listing + */ +export type DirectorySortBy = +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by size (largest first) + */ +"size" | +/** + * Sort by type (directories first, then files) + */ +"type"; + +export type DiscoverRemoteLibrariesInput = { +/** + * Device ID to query for libraries + */ +deviceId: string }; + +/** + * Output from discovering remote libraries + */ +export type DiscoverRemoteLibrariesOutput = { +/** + * Remote device ID that was queried + */ +deviceId: string; +/** + * Remote device name + */ +deviceName: string; +/** + * List of libraries available on the remote device + */ +libraries: RemoteLibraryInfo[]; +/** + * Whether the device is currently online + */ +isOnline: boolean }; + +/** + * Disk type classification + */ +export type DiskType = +/** + * Solid State Drive + */ +"SSD" | +/** + * Hard Disk Drive + */ +"HDD" | +/** + * Network storage + */ +"Network" | +/** + * Virtual/RAM disk + */ +"Virtual" | +/** + * Unknown type + */ +"Unknown"; + +export type DownloadWhisperModelInput = { +/** + * Model size: "tiny", "base", "small", "medium", "large" + */ +model: string }; + +export type DownloadWhisperModelOutput = { +/** + * Job ID for tracking download progress + */ +job_id: string }; + +export type EnableIndexingInput = { +/** + * UUID of the location to enable indexing for + */ +id: string; +/** + * Index mode to use (defaults to Deep if not specified) + */ +index_mode?: string }; + +export type EnableIndexingOutput = { +/** + * UUID of the location that had indexing enabled + */ +location_id: string; +/** + * Job ID of the indexing job that was started + */ +job_id: string }; + +/** + * Type of filesystem entry + */ +export type EntryKind = +/** + * Regular file + */ +"File" | +/** + * Directory + */ +"Directory" | +/** + * Symbolic link + */ +"Symlink"; + +/** + * Input for resetting the ephemeral cache + */ +export type EphemeralCacheResetInput = { +/** + * Confirmation flag to prevent accidental cache clearing + */ +confirm: boolean }; + +/** + * Output from resetting the ephemeral cache + */ +export type EphemeralCacheResetOutput = { +/** + * Number of paths that were cleared from the cache + */ +cleared_paths: number; +/** + * Message describing the result + */ +message: string }; + +/** + * Status of the unified ephemeral index cache + */ +export type EphemeralCacheStatus = { +/** + * Number of paths that have been indexed + */ +indexed_paths_count: number; +/** + * Number of paths currently being indexed + */ +indexing_in_progress_count: number; +/** + * Unified index statistics (shared arena and string interning) + */ +index_stats: UnifiedIndexStats; +/** + * List of indexed paths (directories whose contents are ready) + */ +indexed_paths: IndexedPathInfo[]; +/** + * List of paths currently being indexed + */ +paths_in_progress: string[]; total_indexes?: number | null; indexing_in_progress?: number | null; indexes?: EphemeralIndexInfo[] }; + +/** + * Input for the ephemeral cache status query + */ +export type EphemeralCacheStatusInput = { +/** + * Optional: only include indexed paths containing this substring + */ +path_filter?: string | null }; + +/** + * Legacy: Information about a single ephemeral index (for backward compatibility) + */ +export type EphemeralIndexInfo = { +/** + * Root path this index covers + */ +root_path: string; +/** + * Whether indexing is currently in progress + */ +indexing_in_progress: boolean; +/** + * Total entries in the arena + */ +total_entries: number; +/** + * Number of entries indexed by path + */ +path_index_count: number; +/** + * Number of unique interned names + */ +unique_names: number; +/** + * Number of interned strings in cache + */ +interned_strings: number; +/** + * Number of content kinds stored + */ +content_kinds: number; +/** + * Estimated memory usage in bytes + */ +memory_bytes: number; +/** + * Age of the index in seconds + */ +age_seconds: number; +/** + * Seconds since last access + */ +idle_seconds: number; +/** + * Indexer job statistics (files/dirs/bytes counted) + */ +job_stats: JobStats }; + +/** + * Error event for tracking recent errors + */ +export type ErrorEvent = { timestamp: string; error_type: string; message: string; model_type: string | null; device_id: string | null }; + +/** + * Error metrics snapshot + */ +export type ErrorSnapshot = { total_errors: number; network_errors: number; database_errors: number; apply_errors: number; validation_errors: number; recent_errors: ErrorEvent[]; conflicts_detected: number; conflicts_resolved_by_hlc: number }; + +/** + * A central event type that represents all events that can be emitted throughout the system + */ +export type Event = "CoreStarted" | "CoreShutdown" | { LibraryCreated: { id: string; name: string; path: string; +/** + * How the library was created (manual, sync, cloud import) + */ +source?: LibraryCreationSource } } | { LibraryOpened: { id: string; name: string; path: string } } | { LibraryClosed: { id: string; name: string } } | { LibraryDeleted: { id: string; name: string; deleted_data: boolean } } | { LibraryLoadFailed: { +/** + * Library ID if config was readable, None otherwise + */ +id: string | null; +/** + * Path to the library directory + */ +path: string; +/** + * Human-readable error message + */ +error: string; +/** + * Error type for frontend categorization (e.g., "DatabaseError", "ConfigError") + */ +error_type: string } } | { LibraryStatisticsUpdated: { library_id: string; statistics: LibraryStatistics } } | +/** + * Refresh event - signals that all frontend caches should be invalidated + * Emitted after major data recalculations (e.g., volume unique_bytes refresh) + */ +"Refresh" | { EntryCreated: { library_id: string; entry_id: string } } | { EntryModified: { library_id: string; entry_id: string } } | { EntryDeleted: { library_id: string; entry_id: string } } | { EntryMoved: { library_id: string; entry_id: string; old_path: string; new_path: string } } | { FsRawChange: { library_id: string; kind: FsRawEventKind } } | { VolumeAdded: Volume } | { VolumeRemoved: { fingerprint: VolumeFingerprint } } | { VolumeUpdated: { fingerprint: VolumeFingerprint; old_info: VolumeInfo; new_info: VolumeInfo } } | { VolumeSpeedTested: { fingerprint: VolumeFingerprint; read_speed_mbps: number; write_speed_mbps: number } } | { VolumeMountChanged: { fingerprint: VolumeFingerprint; is_mounted: boolean } } | { VolumeError: { fingerprint: VolumeFingerprint; error: string } } | { JobQueued: { job_id: string; job_type: string; device_id: string } } | { JobStarted: { job_id: string; job_type: string; device_id: string } } | { JobProgress: { job_id: string; job_type: string; device_id: string; progress: number; message: string | null; generic_progress: GenericProgress | null } } | { JobCompleted: { job_id: string; job_type: string; device_id: string; output: JobOutput } } | { JobFailed: { job_id: string; job_type: string; device_id: string; error: string } } | { JobCancelled: { job_id: string; job_type: string; device_id: string } } | { JobPaused: { job_id: string; device_id: string } } | { JobResumed: { job_id: string; device_id: string } } | { IndexingStarted: { location_id: string } } | { IndexingProgress: { location_id: string; processed: number; total: number | null } } | { IndexingCompleted: { location_id: string; total_files: number; total_dirs: number } } | { IndexingFailed: { location_id: string; error: string } } | { DeviceConnected: { device_id: string; device_name: string } } | { DeviceDisconnected: { device_id: string } } | { SyncStateChanged: { library_id: string; previous_state: string; new_state: string; timestamp: string } } | { SyncActivity: { library_id: string; peer_device_id: string; activity_type: SyncActivityType; model_type: string | null; count: number; timestamp: string } } | { SyncConnectionChanged: { library_id: string; peer_device_id: string; peer_name: string; connected: boolean; timestamp: string } } | { SyncError: { library_id: string; peer_device_id: string | null; error_type: string; message: string; timestamp: string } } | { ResourceChanged: { +/** + * Resource type identifier (e.g., "location", "tag", "album") + */ +resource_type: string; +/** + * The full resource data as JSON + */ +resource: JsonValue; +/** + * Metadata for proper cache updates + */ +metadata?: ResourceMetadata | null } } | { ResourceChangedBatch: { +/** + * Resource type identifier (e.g., "file") + */ +resource_type: string; +/** + * Array of full resource data as JSON + * Used for batch updates during indexing to reduce event overhead + */ +resources: JsonValue; +/** + * Metadata for proper cache updates + */ +metadata?: ResourceMetadata | null } } | { ResourceDeleted: { +/** + * Resource type identifier + */ +resource_type: string; +/** + * The deleted resource's ID + */ +resource_id: string } } | { LocationAdded: { library_id: string; location_id: string; path: string } } | { LocationRemoved: { library_id: string; location_id: string } } | { FilesIndexed: { library_id: string; location_id: string; count: number } } | { ThumbnailsGenerated: { library_id: string; count: number } } | { FileOperationCompleted: { library_id: string; operation: FileOperation; affected_files: number } } | { FilesModified: { library_id: string; paths: string[] } } | { Custom: { event_type: string } }; + +/** + * Event category for grouping related events + */ +export type EventCategory = +/** + * State machine lifecycle events + */ +"lifecycle" | +/** + * Data synchronization flow + */ +"data_flow" | +/** + * Network communication + */ +"network" | +/** + * Errors and failures + */ +"error"; + +export type EventInfo = { +/** + * The event variant name (e.g., "JobProgress", "LibraryCreated") + */ +variant: string; +/** + * Whether this event is considered "noisy" (high frequency, should be excluded by default) + */ +is_noisy: boolean; +/** + * Human-readable description + */ +description: string }; + +/** + * Event severity level + */ +export type EventSeverity = +/** + * Debug-level information + */ +"debug" | +/** + * Informational event + */ +"info" | +/** + * Warning condition + */ +"warning" | +/** + * Error condition + */ +"error"; + +/** + * Statistics about what was exported + */ +export type ExportStats = { entries: number; content_identities: number; user_metadata: number; tags: number; media_data: number }; + +export type ExtractTextInput = { +/** + * UUID of the entry to extract text from + */ +entry_uuid: string; +/** + * Languages to use for OCR (e.g., ["eng", "spa"]) + */ +languages: string[] | null; +/** + * Force re-extraction even if text exists + */ +force: boolean }; + +export type ExtractTextOutput = { +/** + * Job ID for tracking OCR progress + */ +job_id: string }; + +/** + * Represents a file within the Spacedrive VDFS. + * + * This is a computed domain model that aggregates data from Entry, ContentIdentity, + * Tags, and Sidecars. It provides a rich, developer-friendly interface without + * duplicating data in the database. + */ +export type File = { +/** + * The unique identifier of the file entry + */ +id: string; +/** + * The universal path to the file in Spacedrive's VDFS + */ +sd_path: SdPath; +/** + * The file kind (file, directory, symlink) + */ +kind: EntryKind; +/** + * The name of the file, including the extension + */ +name: string; +/** + * The file extension (without dot) + */ +extension: string | null; +/** + * The size of the file in bytes + */ +size: number; +/** + * Information about the file's content, including its content hash + */ +content_identity: ContentIdentity | null; +/** + * A list of other paths that share the same content identity + */ +alternate_paths: SdPath[]; +/** + * The semantic tags associated with this file + */ +tags: Tag[]; +/** + * A list of sidecars associated with this file + */ +sidecars: Sidecar[]; +/** + * Media-specific metadata (extracted from EXIF/FFmpeg) + */ +image_media_data: ImageMediaData | null; video_media_data: VideoMediaData | null; audio_media_data: AudioMediaData | null; +/** + * Timestamps for creation, modification, and access + */ +created_at: string; modified_at: string; accessed_at: string | null; +/** + * Additional computed fields + */ +content_kind: ContentKind; is_local: boolean; +/** + * Video duration (for grid display optimization) + */ +duration_seconds: number | null }; + +/** + * Query to get a file by its ID with all related data + */ +export type FileByIdQuery = { file_id: string }; + +/** + * Query to get a file by its local path with all related data + */ +export type FileByPathQuery = { path: string }; + +/** + * Internal enum for file conflict resolution strategies + */ +export type FileConflictResolution = "Overwrite" | "AutoModifyName" | "Skip" | "Abort"; + +/** + * Core input structure for file copy operations + * This is the canonical interface that all external APIs (CLI, REST) convert to + */ +export type FileCopyInput = { +/** + * Source files or directories to copy (domain addressing) + */ +sources: SdPathBatch; +/** + * Destination path (domain addressing) + */ +destination: SdPath; +/** + * Whether to overwrite existing files + */ +overwrite: boolean; +/** + * Whether to verify checksums during copy + */ +verify_checksum: boolean; +/** + * Whether to preserve file timestamps + */ +preserve_timestamps: boolean; +/** + * Whether to delete source files after copying (move operation) + */ +move_files: boolean; +/** + * Preferred copy method to use + */ +copy_method: CopyMethod; +/** + * How to handle file conflicts (set by CLI confirmation) + */ +on_conflict: FileConflictResolution | null }; + +/** + * Input for deleting files + */ +export type FileDeleteInput = { +/** + * Files or directories to delete + */ +targets: SdPathBatch; +/** + * Whether to permanently delete (true) or move to trash (false) + */ +permanent: boolean; +/** + * Whether to delete directories recursively + */ +recursive: boolean }; + +/** + * Types of file operations + */ +export type FileOperation = "Copy" | "Move" | "Delete" | "Rename"; + +/** + * Input for renaming a file or directory + */ +export type FileRenameInput = { +/** + * The file or directory to rename + */ +target: SdPath; +/** + * The new name (filename only, no path separators) + */ +new_name: string }; + +/** + * Main input structure for file search operations + */ +export type FileSearchInput = { +/** + * Primary search query (filename, content, or natural language) + */ +query: string; +/** + * Search scope (library, location, or specific path) + */ +scope: SearchScope; +/** + * Search mode (fast, normal, full) + */ +mode: SearchMode; +/** + * Filters to narrow results + */ +filters: SearchFilters; +/** + * Sorting options + */ +sort: SortOptions; +/** + * Pagination + */ +pagination: PaginationOptions }; + +/** + * Main output structure for file search operations + */ +export type FileSearchOutput = { +/** + * Flat file array matching DirectoryListingOutput - primary field for explorer + */ +files: File[]; +/** + * Search results with scoring metadata - use for search-specific UI (scores, highlights) + */ +results: FileSearchResult[]; total_found: number; search_id: string; facets: SearchFacets; suggestions: string[]; pagination: PaginationInfo; execution_time_ms: number; +/** + * Which index type was used for this search + */ +index_type: IndexType; +/** + * Which filters are available for this search type + */ +available_filters: FilterKind[] }; + +/** + * Individual search result + */ +export type FileSearchResult = { file: File; score: number; score_breakdown: ScoreBreakdown; highlights: TextHighlight[]; matched_content: string | null }; + +/** + * Filesystem type + */ +export type FileSystem = +/** + * Apple File System + */ +"APFS" | +/** + * NT File System (Windows) + */ +"NTFS" | +/** + * Fourth Extended Filesystem (Linux) + */ +"Ext4" | +/** + * B-tree Filesystem (Linux) + */ +"Btrfs" | +/** + * ZFS + */ +"ZFS" | +/** + * Resilient File System (Windows) + */ +"ReFS" | +/** + * File Allocation Table 32 + */ +"FAT32" | +/** + * Extended File Allocation Table + */ +"ExFAT" | +/** + * Hierarchical File System Plus (macOS legacy) + */ +"HFSPlus" | +/** + * Network File System + */ +"NFS" | +/** + * Server Message Block + */ +"SMB" | +/** + * Other filesystem + */ +{ Other: string }; + +/** + * Indicates which filters are available for a given search type + */ +export type FilterKind = "FileTypes" | "DateRange" | "SizeRange" | "ContentTypes" | "Tags" | "Locations" | "Hidden" | "Archived"; + +/** + * Raw filesystem event kinds emitted by the watcher without DB resolution + */ +export type FsRawEventKind = { Create: { path: string } } | { Modify: { path: string } } | { Remove: { path: string } } | { Rename: { from: string; to: string } }; + +/** + * Generate proxy for a single video file + */ +export type GenerateProxyInput = { +/** + * UUID of the entry to generate proxy for + */ +entry_uuid: string; +/** + * Proxy resolution (scrubbing, ultra_low, quick, editing) + */ +resolution: string | null; +/** + * Force regeneration even if proxy exists + */ +force: boolean; +/** + * Use hardware acceleration if available + */ +use_hardware_accel: boolean | null }; + +export type GenerateProxyOutput = { +/** + * Number of proxies generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[]; +/** + * Total encoding time in seconds + */ +encoding_time_secs: number }; + +export type GenerateSplatInput = { entry_uuid: string; model_path: string | null }; + +export type GenerateSplatOutput = { +/** + * Job ID for tracking splat generation progress + */ +job_id: string }; + +/** + * Generate thumbstrip for a single video file + */ +export type GenerateThumbstripInput = { +/** + * UUID of the entry to generate thumbstrip for + */ +entry_uuid: string; +/** + * Optional variant names (defaults to thumbstrip_preview) + */ +variants: string[] | null; +/** + * Force regeneration even if thumbstrip exists + */ +force: boolean }; + +export type GenerateThumbstripOutput = { +/** + * Number of thumbstrips generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[] }; + +/** + * Generic progress information that all job types can convert into + */ +export type GenericProgress = { +/** + * Current progress as a percentage (0.0 to 1.0) + */ +percentage: number; +/** + * Current phase or stage name (e.g., "Discovery", "Processing", "Finalizing") + */ +phase: string; +/** + * Current path being processed (if applicable) + */ +current_path: SdPath | null; +/** + * Human-readable message describing current activity + */ +message: string; +/** + * Completion metrics + */ +completion: ProgressCompletion; +/** + * Performance metrics + */ +performance: PerformanceMetrics }; + +/** + * Input for getting sync activity summary + */ +export type GetSyncActivityInput = Record; + +/** + * Sync activity summary for the UI + */ +export type GetSyncActivityOutput = { currentState: DeviceSyncState; peers: PeerActivity[]; errorCount: number }; + +export type GetSyncEventLogInput = { +/** + * Time range filter (start) + */ +start_time?: string | null; +/** + * Time range filter (end) + */ +end_time?: string | null; +/** + * Filter by event types + */ +event_types?: SyncEventType[] | null; +/** + * Filter by categories + */ +categories?: EventCategory[] | null; +/** + * Filter by severity levels + */ +severities?: EventSeverity[] | null; +/** + * Filter by peer device + */ +peer_id?: string | null; +/** + * Filter by model type + */ +model_type?: string | null; +/** + * Filter by correlation ID + */ +correlation_id?: string | null; +/** + * Maximum number of results + */ +limit?: number | null; +/** + * Offset for pagination + */ +offset?: number | null; +/** + * Include events from remote peers + */ +include_remote_peers?: boolean | null }; + +export type GetSyncEventLogOutput = { events: SyncEventLog[] }; + +export type GetSyncMetricsInput = { +/** + * Filter metrics since this time + */ +since: string | null; +/** + * Filter metrics for specific peer device + */ +peer_id: string | null; +/** + * Filter metrics for specific model type + */ +model_type: string | null; +/** + * Show only state metrics + */ +state_only: boolean | null; +/** + * Show only operation metrics + */ +operations_only: boolean | null; +/** + * Show only error metrics + */ +errors_only: boolean | null }; + +export type GetSyncMetricsOutput = { +/** + * The metrics snapshot + */ +metrics: SyncMetricsSnapshot }; + +/** + * Types of groups that can appear in a space + */ +export type GroupType = +/** + * Fixed quick navigation (Overview, Recents, Favorites) + */ +"QuickAccess" | +/** + * Device with its volumes and locations as children + */ +{ Device: { device_id: string } } | +/** + * All devices (library and paired) across the system + */ +"Devices" | +/** + * All locations across all devices + */ +"Locations" | +/** + * All volumes across all devices + */ +"Volumes" | +/** + * Tag collection + */ +"Tags" | +/** + * Cloud storage providers + */ +"Cloud" | +/** + * User-defined custom group + */ +"Custom"; + +/** + * Image metadata extracted from EXIF + */ +export type ImageMediaData = { uuid: string; width: number; height: number; blurhash: string | null; date_taken: string | null; latitude: number | null; longitude: number | null; camera_make: string | null; camera_model: string | null; lens_model: string | null; focal_length: string | null; aperture: string | null; shutter_speed: string | null; iso: number | null; orientation: number | null; color_space: string | null; color_profile: string | null; bit_depth: string | null; artist: string | null; copyright: string | null; description: string | null }; + +/** + * Statistics about what was imported + */ +export type ImportStats = { entries_imported: number; entries_skipped: number; content_identities: number; user_metadata: number; tags: number; media_data: number }; + +/** + * Canonical input for indexing requests from any interface (CLI, API, etc.) + */ +export type IndexInput = { +/** + * The library within which the operation runs + */ +library_id: string; +/** + * One or more filesystem paths to index + */ +paths: string[]; +/** + * Indexing scope (current directory only vs recursive) + */ +scope: IndexScope; +/** + * Indexing mode (shallow/content/deep) + */ +mode: IndexMode; +/** + * Whether to include hidden files/directories + */ +include_hidden: boolean; +/** + * Where results are stored (ephemeral vs persistent) + */ +persistence: IndexPersistence }; + +/** + * How deeply to index files in this location + */ +export type IndexMode = +/** + * Location exists but is not indexed + */ +"None" | +/** + * Just filesystem metadata (name, size, dates) + */ +"Shallow" | +/** + * Generate content IDs for deduplication + */ +"Content" | +/** + * Full indexing - content IDs, text extraction, thumbnails + */ +"Deep"; + +/** + * Whether to write indexing results to the database or keep them in memory. + * + * Ephemeral persistence allows users to browse external drives and network shares + * without adding them as managed locations. The in-memory index survives for the + * session duration and provides the same API surface as persistent entries, enabling + * features like search and navigation to work identically for both modes. If an + * ephemeral path is later promoted to a managed location, UUIDs are preserved to + * maintain continuity for user metadata. + */ +export type IndexPersistence = +/** + * Write all results to database (normal operation) + */ +"Persistent" | +/** + * Keep results in memory only (for unmanaged paths) + */ +"Ephemeral"; + +/** + * Whether to index just one directory level or recurse through subdirectories. + * + * Current scope is used for UI navigation where users expand folders on-demand, + * while Recursive scope is used for full location indexing. Current scope with + * persistent storage enables progressive indexing where the UI drives which + * directories get indexed based on user interaction. + */ +export type IndexScope = +/** + * Index only the current directory (single level) + */ +"Current" | +/** + * Index recursively through all subdirectories + */ +"Recursive"; + +/** + * Indicates which index type was used for a search query + */ +export type IndexType = +/** + * Database FTS5 search (persistent index) + */ +"Persistent" | +/** + * In-memory ephemeral search + */ +"Ephemeral" | +/** + * Mix of both (future: hybrid searches) + */ +"Hybrid"; + +export type IndexVerifyInput = { +/** + * Path to verify (can be a location root or subdirectory) + */ +path: string; +/** + * Whether to check content hashes (slower but more thorough) + */ +verify_content?: boolean; +/** + * Whether to include detailed file-by-file comparison + */ +detailed_report?: boolean; +/** + * Whether to fix issues automatically (future feature) + */ +auto_fix?: boolean }; + +/** + * Result of index integrity verification + */ +export type IndexVerifyOutput = { +/** + * Overall integrity status + */ +is_valid: boolean; +/** + * Integrity report with detailed findings + */ +report: IntegrityReport; +/** + * Path that was verified + */ +path: string; +/** + * Time taken to verify (seconds) + */ +duration_secs: number }; + +/** + * Input for volume indexing action + */ +export type IndexVolumeInput = { +/** + * Volume fingerprint to index + */ +fingerprint: string; +/** + * Indexing scope (defaults to Recursive for full volume) + */ +scope?: IndexScope }; + +/** + * Output from volume indexing action + */ +export type IndexVolumeOutput = { +/** + * UUID of the indexed volume + */ +volume_id: string; +/** + * Job ID for tracking progress + */ +job_id: string; +/** + * Total files found (if job completed) + */ +total_files: number | null; +/** + * Total directories found (if job completed) + */ +total_directories: number | null; +/** + * Success message + */ +message: string }; + +/** + * Information about an indexed path + */ +export type IndexedPathInfo = { +/** + * The directory path that was indexed + */ +path: string; +/** + * Number of direct children in this directory + */ +child_count: number }; + +/** + * Complete snapshot of indexer performance after job completion. + */ +export type IndexerMetrics = { total_duration: { secs: number; nanos: number }; discovery_duration: { secs: number; nanos: number }; processing_duration: { secs: number; nanos: number }; content_duration: { secs: number; nanos: number }; files_per_second: number; bytes_per_second: number; dirs_per_second: number; db_writes: number; db_reads: number; batch_count: number; avg_batch_size: number; total_errors: number; critical_errors: number; non_critical_errors: number; skipped_paths: number; peak_memory_bytes: number | null; avg_memory_bytes: number | null }; + +/** + * Indexer settings controlling rule toggles + */ +export type IndexerSettings = { no_system_files?: boolean; no_git?: boolean; no_dev_dirs?: boolean; no_hidden?: boolean; gitignore?: boolean; only_images?: boolean }; + +/** + * Cumulative statistics tracked throughout the indexing process. + */ +export type IndexerStats = { files: number; dirs: number; bytes: number; symlinks: number; skipped: number; errors: number }; + +/** + * Represents a single integrity difference + */ +export type IntegrityDifference = { +/** + * Path relative to verification root + */ +path: string; +/** + * Type of issue + */ +issue_type: IssueType; +/** + * Expected value (from filesystem or correct state) + */ +expected: string | null; +/** + * Actual value (from database) + */ +actual: string | null; +/** + * Human-readable description + */ +description: string; +/** + * Debug: database entry ID for investigation + */ +db_entry_id?: number | null; +/** + * Debug: database entry name + */ +db_entry_name?: string | null }; + +/** + * Detailed integrity report + */ +export type IntegrityReport = { +/** + * Total files found on filesystem + */ +filesystem_file_count: number; +/** + * Total files in database index + */ +database_file_count: number; +/** + * Total directories found on filesystem + */ +filesystem_dir_count: number; +/** + * Total directories in database index + */ +database_dir_count: number; +/** + * Files missing from index (on filesystem but not in DB) + */ +missing_from_index: IntegrityDifference[]; +/** + * Stale entries in index (in DB but not on filesystem) + */ +stale_in_index: IntegrityDifference[]; +/** + * Entries with incorrect metadata + */ +metadata_mismatches: IntegrityDifference[]; +/** + * Entries with incorrect parent relationships + */ +hierarchy_errors: IntegrityDifference[]; +/** + * Summary statistics + */ +summary: string }; + +export type IssueType = { type: "MissingFromIndex" } | { type: "StaleInIndex" } | { type: "SizeMismatch" } | { type: "ModifiedTimeMismatch" } | { type: "InodeMismatch" } | { type: "ExtensionMismatch" } | { type: "ParentMismatch" } | { type: "KindMismatch" }; + +/** + * Types of items that can appear in a group + */ +export type ItemType = +/** + * Overview screen (fixed) + */ +"Overview" | +/** + * Recent files (fixed) + */ +"Recents" | +/** + * Favorited files (fixed) + */ +"Favorites" | +/** + * File kinds (images, videos, audio, etc.) + */ +"FileKinds" | +/** + * Indexed location + */ +{ Location: { location_id: string } } | +/** + * Storage volume (with locations as children) + */ +{ Volume: { volume_id: string } } | +/** + * Tag filter + */ +{ Tag: { tag_id: string } } | +/** + * Any arbitrary path (dragged from explorer) + */ +{ Path: { sd_path: SdPath } }; + +export type JobCancelInput = { job_id: string }; + +export type JobCancelOutput = { job_id: string; success: boolean }; + +/** + * Unique identifier for a job + */ +export type JobId = string; + +export type JobInfoOutput = { id: string; name: string; status: JobStatus; progress: number; created_at: string; started_at: string | null; completed_at: string | null; error_message: string | null }; + +export type JobInfoQueryInput = { job_id: string }; + +export type JobListInput = { status: JobStatus | null }; + +export type JobListItem = { id: string; name: string; device_id: string; status: JobStatus; progress: number; action_type: string | null; action_context: ActionContextInfo | null; created_at: string; started_at: string | null; completed_at: string | null }; + +export type JobListOutput = { jobs: JobListItem[] }; + +/** + * Output from a completed job + */ +export type JobOutput = +/** + * Job completed successfully with no specific output + */ +{ type: "Success" } | +/** + * File copy job output + */ +{ type: "FileCopy"; data: { copied_count: number; total_bytes: number } } | +/** + * Indexer job output + */ +{ type: "Indexed"; data: { stats: IndexerStats; metrics: IndexerMetrics } } | +/** + * Thumbnail generation output + */ +{ type: "ThumbnailsGenerated"; data: { generated_count: number; failed_count: number } } | +/** + * Thumbnail generation output (detailed) + */ +{ type: "ThumbnailGeneration"; data: { generated_count: number; skipped_count: number; error_count: number; total_size_bytes: number } } | +/** + * File move/rename operation output + */ +{ type: "FileMove"; data: { moved_count: number; failed_count: number; total_bytes: number } } | +/** + * File delete operation output + */ +{ type: "FileDelete"; data: { deleted_count: number; failed_count: number; total_bytes: number } } | +/** + * Duplicate detection output + */ +{ type: "DuplicateDetection"; data: { duplicate_groups: number; total_duplicates: number; potential_savings: number } } | +/** + * File validation output + */ +{ type: "FileValidation"; data: { validated_count: number; issues_found: number; total_bytes_validated: number } } | +/** + * OCR text extraction output + */ +{ type: "OcrExtraction"; data: { total_processed: number; success_count: number; error_count: number } } | +/** + * Speech-to-text transcription output + */ +{ type: "SpeechToText"; data: { total_processed: number; success_count: number; error_count: number } } | +/** + * Gaussian splat generation output + */ +{ type: "GaussianSplat"; data: { total_processed: number; success_count: number; error_count: number } }; + +export type JobPauseInput = { job_id: string }; + +export type JobPauseOutput = { job_id: string; success: boolean }; + +/** + * Job execution policies for a location + * + * Controls which automated jobs run on this location and their configuration. + * This allows per-location customization of thumbnail generation, OCR, speech-to-text, etc. + */ +export type JobPolicies = { +/** + * Thumbnail generation policy + */ +thumbnail?: ThumbnailPolicy; +/** + * Thumbstrip generation policy + */ +thumbstrip?: ThumbstripPolicy; +/** + * Proxy/sidecar generation policy (video scrubbing) + */ +proxy?: ProxyPolicy; +/** + * OCR (text extraction) policy + */ +ocr?: OcrPolicy; +/** + * Speech-to-text transcription policy + */ +speech_to_text?: SpeechPolicy; +/** + * Object detection policy (future) + */ +object_detection?: ObjectDetectionPolicy }; + +export type JobReceipt = { id: JobId; job_name: string }; + +export type JobResumeInput = { job_id: string }; + +export type JobResumeOutput = { job_id: string; success: boolean }; + +/** + * Statistics from the indexer job + */ +export type JobStats = { +/** + * Number of files indexed + */ +files: number; +/** + * Number of directories indexed + */ +dirs: number; +/** + * Number of symlinks indexed + */ +symlinks: number; +/** + * Total bytes indexed + */ +bytes: number }; + +/** + * Current status of a job + */ +export type JobStatus = +/** + * Job is waiting to be executed + */ +"queued" | +/** + * Job is currently running + */ +"running" | +/** + * Job has been paused + */ +"paused" | +/** + * Job completed successfully + */ +"completed" | +/** + * Job failed with an error + */ +"failed" | +/** + * Job was cancelled + */ +"cancelled"; + +/** + * Type of job to trigger for a location + */ +export type JobType = "thumbnail" | "thumbstrip" | "ocr" | "speech_to_text" | "object_detection"; + +export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }; + +/** + * Latency metrics snapshot + */ +export type LatencySnapshot = { count: number; avg_ms: number; min_ms: number; max_ms: number }; + +/** + * A Spacedrive library - the canonical domain model + * + * This is the resource type sent to the frontend for the normalized cache. + * It contains all the information needed to display library info in the UI. + */ +export type Library = { +/** + * Library unique identifier + */ +id: string; +/** + * Human-readable library name + */ +name: string; +/** + * Optional description + */ +description: string | null; +/** + * Path to the library directory + */ +path: string; +/** + * When the library was created + */ +created_at: string; +/** + * When the library was last modified + */ +updated_at: string; +/** + * Library-specific settings + */ +settings: LibrarySettings; +/** + * Library statistics + */ +statistics: LibraryStatistics }; + +/** + * Input for creating a new library + */ +export type LibraryCreateInput = { +/** + * Name of the library + */ +name: string; +/** + * Optional path for the library (if not provided, will use default location) + */ +path: string | null }; + +/** + * Output from library create action dispatch + */ +export type LibraryCreateOutput = { library_id: string; name: string; path: string }; + +/** + * Source of library creation for automatic switching behavior + */ +export type LibraryCreationSource = +/** + * User created locally via UI + */ +"Manual" | +/** + * Received via network sync from another device + */ +"Sync" | +/** + * Imported from cloud storage + */ +"CloudImport"; + +/** + * Input for deleting a library + */ +export type LibraryDeleteInput = { +/** + * ID of the library to delete + */ +library_id: string; +/** + * Whether to also delete the library's data directory + */ +delete_data: boolean }; + +/** + * Output from library delete action dispatch + */ +export type LibraryDeleteOutput = { library_id: string; name: string }; + +/** + * Input for exporting a library + */ +export type LibraryExportInput = { library_id: string; export_path: string; include_thumbnails: boolean; include_previews: boolean }; + +export type LibraryExportOutput = { library_id: string; library_name: string; export_path: string; exported_files: string[] }; + +/** + * Information about a library for listing purposes + */ +export type LibraryInfo = { +/** + * Library unique identifier + */ +id: string; +/** + * Human-readable library name + */ +name: string; +/** + * Path to the library directory + */ +path: string; +/** + * Optional statistics if requested + */ +stats: LibraryStatistics | null }; + +/** + * Input for library info query + */ +export type LibraryInfoQueryInput = null; + +export type LibraryOpenInput = { +/** + * Path to the library directory to open + */ +path: string }; + +export type LibraryOpenOutput = { +/** + * ID of the opened library + */ +library_id: string; +/** + * Name of the opened library + */ +name: string; +/** + * Path where the library is located + */ +path: string }; + +export type LibraryRenameInput = { library_id: string; new_name: string }; + +export type LibraryRenameOutput = { library_id: string; old_name: string; new_name: string }; + +/** + * Library-specific settings + */ +export type LibrarySettings = { +/** + * Whether to generate thumbnails for media files + */ +generate_thumbnails: boolean; +/** + * Thumbnail quality (0-100) + */ +thumbnail_quality: number; +/** + * Whether to enable AI-powered tagging + */ +enable_ai_tagging: boolean; +/** + * Whether sync is enabled for this library + */ +sync_enabled: boolean; +/** + * Whether the library is encrypted at rest + */ +encryption_enabled: boolean; +/** + * Custom thumbnail sizes to generate + */ +thumbnail_sizes: number[]; +/** + * File extensions to ignore during indexing + */ +ignored_extensions: string[]; +/** + * TODO: ai slop config pls remove this + */ +max_file_size: number | null; +/** + * Whether to automatically track system volumes + */ +auto_track_system_volumes: boolean; +/** + * Whether to automatically track external volumes when connected + */ +auto_track_external_volumes: boolean; +/** + * Indexer settings (rule toggles and related) + */ +indexer?: IndexerSettings }; + +/** + * Library statistics + */ +export type LibraryStatistics = { +/** + * Total number of files indexed + */ +total_files: number; +/** + * Total size of all files in bytes + */ +total_size: number; +/** + * Number of locations in this library + */ +location_count: number; +/** + * Number of tags created + */ +tag_count: number; +/** + * Number of devices in this library (v2 field, defaults to 0 for old configs) + */ +device_count?: number; +/** + * Number of unique content identities in this library (v2 field, defaults to 0 for old configs) + */ +unique_content_count?: number; +/** + * Total storage capacity across all volumes in bytes (v2 field, defaults to 0 for old configs) + */ +total_capacity?: number; +/** + * Available storage across all volumes in bytes (v2 field, defaults to 0 for old configs) + */ +available_capacity?: number; +/** + * Number of thumbnails generated + */ +thumbnail_count: number; +/** + * Database file size in bytes + */ +database_size: number; +/** + * Last time the library was fully indexed + */ +last_indexed: string | null; +/** + * When these statistics were last updated + */ +updated_at: string }; + +/** + * Action to take when setting up library sync + */ +export type LibrarySyncAction = +/** + * Share local library to remote device (creates same library with same UUID on remote) + * This is the primary way to create a shared library + */ +{ type: "shareLocalLibrary"; libraryName: string } | +/** + * Join an existing remote library (creates same library with same UUID locally) + * Use this when the other device has already shared their library + */ +{ type: "joinRemoteLibrary"; remoteLibraryId: string; remoteLibraryName: string } | +/** + * Future: Merge two different libraries into one (combines data from both) + * Not yet implemented - requires full sync system + */ +{ type: "mergeLibraries"; localLibraryId: string; remoteLibraryId: string; mergedName: string }; + +/** + * Input for setting up library sync between paired devices + */ +export type LibrarySyncSetupInput = { +/** + * Local device ID (should be current device) + */ +localDeviceId: string; +/** + * Remote paired device ID + */ +remoteDeviceId: string; +/** + * Local library to set up sync for + */ +localLibraryId: string; +/** + * Remote library to sync with (optional for RegisterOnly) + */ +remoteLibraryId: string | null; +/** + * Sync action to perform + */ +action: LibrarySyncAction; +/** + * DEPRICATED: Which device should be the sync leader (for future sync implementation) + */ +leaderDeviceId: string }; + +/** + * Result of library sync setup operation + */ +export type LibrarySyncSetupOutput = { +/** + * Whether setup was successful + */ +success: boolean; +/** + * Local library ID that was configured + */ +localLibraryId: string; +/** + * Remote library ID that was linked (if applicable) + */ +remoteLibraryId: string | null; +/** + * Whether devices were successfully registered in each other's libraries + */ +devicesRegistered: boolean; +/** + * Message describing the result + */ +message: string }; + +export type ListEventsInput = Record; + +export type ListEventsOutput = { +/** + * All available event types + */ +all_events: string[]; +/** + * Events that are high-frequency and should be excluded by default + */ +noisy_events: string[]; +/** + * Detailed information about each event + */ +event_info: EventInfo[] }; + +export type ListLibrariesInput = { +/** + * Whether to include detailed statistics for each library + */ +include_stats: boolean }; + +/** + * Input for listing devices from library database + */ +export type ListLibraryDevicesInput = { +/** + * Whether to include offline devices (default: true) + */ +include_offline: boolean; +/** + * Whether to include detailed capabilities and sync leadership info (default: false) + */ +include_details: boolean; +/** + * Whether to also include paired network devices (default: false) + */ +show_paired?: boolean }; + +export type ListPairedDevicesInput = { +/** + * Whether to include only connected devices + */ +connectedOnly?: boolean }; + +/** + * Output from listing paired devices + */ +export type ListPairedDevicesOutput = { +/** + * List of paired devices + */ +devices: PairedDeviceInfo[]; +/** + * Total number of paired devices + */ +total: number; +/** + * Number of currently connected devices + */ +connected: number }; + +export type ListWhisperModelsInput = Record; + +export type ListWhisperModelsOutput = { models: ModelInfo[]; total_downloaded_size: number }; + +/** + * An indexed directory that Spacedrive monitors + */ +export type Location = { +/** + * Unique identifier + */ +id: string; +/** + * Library this location belongs to + */ +library_id: string; +/** + * Root path of this location (includes device!) + */ +sd_path: SdPath; +/** + * Human-friendly name + */ +name: string; +/** + * Indexing configuration + */ +index_mode: IndexMode; +/** + * How often to rescan (None = manual only) + */ +scan_interval: { secs: number; nanos: number } | null; +/** + * Statistics + */ +total_size: number; file_count: number; directory_count: number; +/** + * Current state + */ +scan_state: ScanState; +/** + * Timestamps + */ +created_at: string; updated_at: string; last_scan_at: string | null; +/** + * Whether this location is currently available + */ +is_available: boolean; +/** + * Hidden glob patterns (e.g., [".*", "node_modules"]) + */ +ignore_patterns: string[]; +/** + * Job execution policies for this location + */ +job_policies?: JobPolicies }; + +export type LocationAddInput = { path: SdPath; name: string | null; mode: IndexMode; job_policies: JsonValue | null }; + +/** + * Output from location add action dispatch + */ +export type LocationAddOutput = { location_id: string; path: SdPath; name: string | null; job_id: string | null }; + +/** + * Input for exporting a location + */ +export type LocationExportInput = { +/** + * The UUID of the location to export + */ +location_uuid: string; +/** + * Path where the SQL dump file will be written + */ +export_path: string; +/** + * Include content identities (file hashes, dedup info) + */ +include_content_identities?: boolean; +/** + * Include media metadata (EXIF, video/audio info) + */ +include_media_data?: boolean; +/** + * Include user metadata (notes, favorites) + */ +include_user_metadata?: boolean; +/** + * Include tags and tag relationships + */ +include_tags?: boolean }; + +/** + * Output from location export action + */ +export type LocationExportOutput = { location_uuid: string; location_name: string | null; export_path: string; file_size_bytes: number; stats: ExportStats }; + +/** + * Input for importing a location from SQL dump + */ +export type LocationImportInput = { +/** + * Path to the SQL dump file to import + */ +import_path: string; +/** + * Optional new name for the imported location (overrides name in dump) + */ +new_name: string | null; +/** + * Whether to skip entries that already exist (by UUID) + */ +skip_existing?: boolean }; + +/** + * Output from location import action + */ +export type LocationImportOutput = { location_uuid: string; location_name: string | null; import_path: string; stats: ImportStats }; + +export type LocationRemoveInput = { location_id: string }; + +/** + * Output from location remove action dispatch + */ +export type LocationRemoveOutput = { location_id: string; path: string | null }; + +export type LocationRescanInput = { location_id: string; full_rescan: boolean }; + +export type LocationRescanOutput = { location_id: string; location_path: string; job_id: string; full_rescan: boolean }; + +export type LocationTriggerJobInput = { +/** + * UUID of the location to run the job on + */ +location_id: string; +/** + * Type of job to trigger + */ +job_type: JobType; +/** + * Force the job to run even if disabled in the location's policy + */ +force?: boolean }; + +export type LocationTriggerJobOutput = { +/** + * UUID of the dispatched job + */ +job_id: string; +/** + * Type of job that was triggered + */ +job_type: JobType; +/** + * UUID of the location the job is running on + */ +location_id: string }; + +export type LocationUpdateInput = { +/** + * UUID of the location to update + */ +id: string; +/** + * Optional new name for the location + */ +name: string | null; +/** + * Optional job policies to update + */ +job_policies: JobPolicies | null }; + +export type LocationUpdateOutput = { +/** + * UUID of the updated location + */ +id: string }; + +/** + * Output for location list queries + */ +export type LocationsListOutput = { locations: Location[] }; + +export type LocationsListQueryInput = null; + +/** + * Input for media listing + */ +export type MediaListingInput = { +/** + * The directory path to list media for + */ +path: SdPath; +/** + * Whether to include media from descendant directories (default: false) + */ +include_descendants: boolean | null; +/** + * Which media types to include (default: both Image and Video) + */ +media_types: ContentKind[] | null; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Sort order for results + */ +sort_by: MediaSortBy }; + +/** + * Output containing media files + */ +export type MediaListingOutput = { +/** + * Media files (images/videos) + */ +files: File[]; +/** + * Total count of media files found + */ +total_count: number; +/** + * Whether there are more results than returned + */ +has_more: boolean }; + +/** + * Sort options for media listing + */ +export type MediaSortBy = +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by creation date (newest first) + */ +"created" | +/** + * Sort by date taken/captured (newest first) + */ +"datetaken" | +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by size (largest first) + */ +"size"; + +/** + * Information about a model + */ +export type ModelInfo = { +/** + * Unique model identifier + */ +id: string; +/** + * Human-readable name + */ +name: string; +/** + * Model type + */ +model_type: ModelType; +/** + * File size in bytes + */ +size_bytes: number; +/** + * Where to download from + */ +provider: ModelProvider; +/** + * Filename on disk + */ +filename: string; +/** + * Whether this model is currently downloaded + */ +downloaded: boolean; +/** + * Optional description + */ +description: string | null }; + +/** + * Model provider + */ +export type ModelProvider = +/** + * Hugging Face + */ +{ HuggingFace: { repo: string } } | +/** + * GitHub Release + */ +{ GitHub: { owner: string; repo: string } } | +/** + * Direct URL + */ +{ Direct: { url: string } }; + +/** + * Type of model + */ +export type ModelType = +/** + * Whisper speech-to-text model + */ +"Whisper" | +/** + * Tesseract OCR language data + */ +"Tesseract"; + +/** + * Mount type classification + */ +export type MountType = +/** + * System mount (root, boot, etc.) + */ +"System" | +/** + * External device mount + */ +"External" | +/** + * Network mount + */ +"Network" | +/** + * User mount + */ +"User"; + +export type NetworkStartInput = Record; + +export type NetworkStartOutput = { started: boolean }; + +export type NetworkStatus = { running: boolean; node_id: string | null; addresses: string[]; paired_devices: number; connected_devices: number; version: string; relay_url: string | null }; + +export type NetworkStatusQueryInput = null; + +export type NetworkStopInput = Record; + +export type NetworkStopOutput = { stopped: boolean }; + +/** + * Object detection policy (for future AI features) + */ +export type ObjectDetectionPolicy = { +/** + * Whether to run object detection on this location + */ +enabled: boolean; +/** + * Minimum confidence threshold (0.0 - 1.0) + */ +min_confidence: number; +/** + * Categories to detect (empty = all) + */ +categories: string[]; +/** + * Whether to reprocess files that already have object data + */ +reprocess: boolean }; + +/** + * OCR (text extraction) policy + */ +export type OcrPolicy = { +/** + * Whether to run OCR on this location + */ +enabled: boolean; +/** + * Languages to use for OCR (e.g., ["eng", "spa"]) + */ +languages: string[]; +/** + * Minimum confidence threshold (0.0 - 1.0) + */ +min_confidence: number; +/** + * Whether to reprocess files that already have text + */ +reprocess: boolean }; + +/** + * Operating system types + */ +export type OperatingSystem = "MacOS" | "Windows" | "Linux" | "IOs" | "Android" | "Other"; + +/** + * Operation metrics snapshot + */ +export type OperationSnapshot = { broadcasts_sent: number; state_changes_broadcast: number; shared_changes_broadcast: number; broadcast_batches_sent: number; failed_broadcasts: number; changes_received: number; changes_applied: number; changes_rejected: number; buffer_queue_depth: number; active_backfill_sessions: number; backfill_sessions_completed: number; backfill_pagination_rounds: number; retry_queue_depth: number; retry_attempts: number; retry_successes: number }; + +/** + * Pagination information + */ +export type PaginationInfo = { current_page: number; total_pages: number; has_next: boolean; has_previous: boolean; limit: number; offset: number }; + +/** + * Pagination options + */ +export type PaginationOptions = { limit: number; offset: number }; + +export type PairCancelInput = { session_id: string }; + +export type PairCancelOutput = { cancelled: boolean }; + +export type PairGenerateInput = Record; + +export type PairGenerateOutput = { code: string; session_id: string; expires_at: string; +/** + * QR code JSON format (includes NodeId for remote pairing) + */ +qr_json: string; +/** + * Node ID for relay-based pairing (share this for cross-network pairing) + */ +node_id: string | null }; + +export type PairJoinInput = { code: string; +/** + * Optional node ID for relay-based pairing (enables cross-network connections) + */ +node_id: string | null }; + +export type PairJoinOutput = { paired_device_id: string; device_name: string }; + +export type PairStatusOutput = { sessions: PairingSessionSummary[] }; + +export type PairStatusQueryInput = null; + +/** + * Information about a paired device + */ +export type PairedDeviceInfo = { +/** + * Device ID + */ +id: string; +/** + * Device name + */ +name: string; +/** + * Device type + */ +deviceType: string; +/** + * OS version + */ +osVersion: string; +/** + * App version + */ +appVersion: string; +/** + * Whether the device is currently connected + */ +isConnected: boolean; +/** + * When the device was last seen + */ +lastSeen: string }; + +export type PairingSessionSummary = { id: string; state: SerializablePairingState; remote_device_id: string | null; expires_at: string | null }; + +/** + * Path mapping for resolving virtual paths to actual storage locations + */ +export type PathMapping = { virtual_path: string; actual_path: string }; + +/** + * Per-peer activity information + */ +export type PeerActivity = { deviceId: string; deviceName: string; isOnline: boolean; lastSeen: string; entriesReceived: number; bytesReceived: number; bytesSent: number; watermarkLagMs: number | null }; + +/** + * Performance and timing metrics + */ +export type PerformanceMetrics = { +/** + * Processing rate (items per second) + */ +rate: number; +/** + * Estimated time remaining + */ +estimated_remaining: { secs: number; nanos: number } | null; +/** + * Time elapsed since start + */ +elapsed: { secs: number; nanos: number } | null; +/** + * Number of errors encountered + */ +error_count: number; +/** + * Number of warnings + */ +warning_count: number }; + +/** + * Performance metrics snapshot + */ +export type PerformanceSnapshot = { broadcast_latency: LatencySnapshot; apply_latency: LatencySnapshot; backfill_request_latency: LatencySnapshot; state_watermark: string; shared_watermark: string; watermark_lag_ms: { [key in string]: number }; hlc_physical_drift_ms: number; hlc_counter_max: number; db_query_duration: LatencySnapshot; db_query_count: number }; + +export type PingInput = { message: string; count?: number | null }; + +export type PingOutput = { echo: string; count: number; extension_works: boolean }; + +/** + * Privacy levels for tag visibility control + */ +export type PrivacyLevel = +/** + * Standard visibility in all contexts + */ +"Normal" | +/** + * Hidden from normal searches but accessible via direct query + */ +"Archive" | +/** + * Completely hidden from standard UI + */ +"Hidden"; + +/** + * Progress completion information + */ +export type ProgressCompletion = { +/** + * Items completed (files, entries, operations, etc.) + */ +completed: number; +/** + * Total items to complete + */ +total: number; +/** + * Bytes processed (if applicable) + */ +bytes_completed: number | null; +/** + * Total bytes to process (if applicable) + */ +total_bytes: number | null }; + +/** + * Proxy/sidecar generation policy (video scrubbing) + */ +export type ProxyPolicy = { +/** + * Whether to generate proxy files for this location + */ +enabled: boolean; +/** + * Whether to regenerate existing proxies + */ +regenerate: boolean }; + +export type RegenerateThumbnailInput = { +/** + * UUID of the entry to regenerate thumbnails for + */ +entry_uuid: string; +/** + * Optional variant names (defaults to grid@1x, grid@2x, detail@1x) + */ +variants: string[] | null; +/** + * Force regeneration even if thumbnails exist + */ +force: boolean }; + +export type RegenerateThumbnailOutput = { +/** + * Number of thumbnails generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[] }; + +/** + * State of a job running on a remote device + */ +export type RemoteJobState = { job_id: string; job_type: string; library_id: string; device_id: string; device_name: string; status: JobStatus; progress: number | null; message: string | null; generic_progress: GenericProgress | null; started_at: string | null; completed_at: string | null; error: string | null }; + +/** + * Query for all remote jobs across all devices + */ +export type RemoteJobsAllDevicesInput = Record; + +export type RemoteJobsAllDevicesOutput = { jobs_by_device: { [key in string]: RemoteJobState[] } }; + +/** + * Query for remote jobs on a specific device + */ +export type RemoteJobsForDeviceInput = { device_id: string }; + +export type RemoteJobsForDeviceOutput = { jobs: RemoteJobState[] }; + +/** + * Information about a library discovered on a remote device + */ +export type RemoteLibraryInfo = { +/** + * Library ID + */ +id: string; +/** + * Library name + */ +name: string; +/** + * Library description (if any) + */ +description: string | null; +/** + * When the library was created + */ +createdAt: string; +/** + * Statistics about the library + */ +statistics: LibraryStatistics }; + +export type ReorderGroupsInput = { space_id: string; group_ids: string[] }; + +export type ReorderItemsInput = { group_id: string | null; item_ids: string[] }; + +export type ReorderOutput = { success: boolean }; + +export type ResetDataInput = { +/** + * Confirmation flag to prevent accidental data loss + */ +confirm: boolean }; + +export type ResetDataOutput = { +/** + * Whether the reset was successful + */ +success: boolean; +/** + * Message describing the result + */ +message: string }; + +/** + * Metadata for resource cache updates + */ +export type ResourceMetadata = { +/** + * Fields that should be replaced, not merged + */ +no_merge_fields: string[]; +/** + * Alternate IDs for matching (besides primary ID) + */ +alternate_ids: string[]; +/** + * Paths affected by this resource event (for path-scoped filtering) + */ +affected_paths?: SdPath[] }; + +/** + * Risk level for adding a path as a location + */ +export type RiskLevel = +/** + * Safe - nested path in user directories + */ +"low" | +/** + * Caution - shallow path on primary volume (e.g., /Users/jamie) + */ +"medium" | +/** + * Warning - system directory or root-level path (e.g., /, /System) + */ +"high"; + +/** + * Current scanning state of a location + */ +export type ScanState = +/** + * Not currently being scanned + */ +"Idle" | +/** + * Currently scanning + */ +{ Scanning: { +/** + * Progress percentage (0-100) + */ +progress: number } } | +/** + * Scan completed successfully + */ +"Completed" | +/** + * Scan failed with error + */ +"Failed" | +/** + * Scan was paused + */ +"Paused"; + +/** + * Detailed breakdown of how the score was calculated + */ +export type ScoreBreakdown = { temporal_score: number; semantic_score: number | null; metadata_score: number; recency_boost: number; user_preference_boost: number; final_score: number }; + +/** + * A path within the Spacedrive Virtual Distributed File System + * + * This is the core abstraction that enables cross-device operations. + * An SdPath can represent: + * - A physical file at a specific path on a specific device + * - A content-addressed file that can be sourced from any device + * - A sidecar (derivative data) attached to content + * + * This enum-based approach enables resilient file operations by allowing + * content-based paths to be resolved to optimal physical locations at runtime. + */ +export type SdPath = +/** + * A direct pointer to a file at a specific path on a specific device + */ +{ Physical: { +/** + * The device slug (e.g., "jamies-macbook") + */ +device_slug: string; +/** + * The local path on that device + */ +path: string } } | +/** + * A cloud storage path within a cloud volume + */ +{ Cloud: { +/** + * The cloud service type (S3, GoogleDrive, etc.) + */ +service: CloudServiceType; +/** + * The cloud identifier (bucket name, drive name, etc.) + */ +identifier: string; +/** + * The cloud-native path (e.g., "bucket/key" for S3) + */ +path: string } } | +/** + * An abstract, location-independent handle that refers to file content + */ +{ Content: { +/** + * The unique content identifier + */ +content_id: string } } | +/** + * A derivative data file (thumbnail, OCR text, embedding, etc.) + * Sidecars are content-scoped and addressed by content + kind + variant + */ +{ Sidecar: { +/** + * The content this sidecar is derived from + */ +content_id: string; +/** + * The type of sidecar (thumb, ocr, embeddings, etc.) + */ +kind: SidecarKind; +/** + * The specific variant (e.g., "grid@2x", "1080p", "all-MiniLM-L6-v2") + */ +variant: SidecarVariant; +/** + * The storage format (webp, json, msgpack, etc.) + */ +format: SidecarFormat } }; + +/** + * A batch of SdPaths, useful for operations on multiple files + */ +export type SdPathBatch = { paths: SdPath[] }; + +/** + * Search facets for filtering UI + */ +export type SearchFacets = { file_types: { [key in string]: number }; tags: { [key in string]: number }; locations: { [key in string]: number }; date_ranges: { [key in string]: number }; size_ranges: { [key in string]: number } }; + +/** + * 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 }; + +/** + * Defines the search mode and performance characteristics + */ +export type SearchMode = +/** + * Fast, metadata-only search (<10ms) + */ +"Fast" | +/** + * Normal search with semantic ranking (<100ms) + */ +"Normal" | +/** + * Full search with content analysis (<500ms) + */ +"Full"; + +/** + * Defines the scope of the filesystem to search within + */ +export type SearchScope = +/** + * Search the entire library (default) + */ +"Library" | +/** + * Restrict search to a specific location by its ID + */ +{ Location: { location_id: string } } | +/** + * Restrict search to a specific directory path and all its descendants + */ +{ Path: { path: SdPath } }; + +export type SearchTagsInput = { +/** + * Search query (searches across all name variants) + */ +query: string; +/** + * Optional namespace filter + */ +namespace: string | null; +/** + * Optional tag type filter + */ +tag_type: TagType | null; +/** + * Whether to include archived/hidden tags + */ +include_archived: boolean | null; +/** + * Maximum number of results to return + */ +limit: number | null; +/** + * Whether to resolve ambiguous results using context + */ +resolve_ambiguous: boolean | null; +/** + * Context tags for disambiguation (UUIDs) + */ +context_tag_ids: string[] | null }; + +export type SearchTagsOutput = { +/** + * Tags found by the search + */ +tags: TagSearchResult[]; +/** + * Total number of results found (may be more than returned if limited) + */ +total_found: number; +/** + * Whether results were disambiguated using context + */ +disambiguated: boolean; +/** + * Search query that was executed + */ +query: string; +/** + * Applied filters + */ +filters: TagSearchFilters }; + +export type SerializablePairingState = "Idle" | "GeneratingCode" | "Broadcasting" | "Scanning" | "WaitingForConnection" | "Connecting" | "Authenticating" | "ExchangingKeys" | "AwaitingConfirmation" | "EstablishingSession" | "ChallengeReceived" | "ResponsePending" | "ResponseSent" | "Completed" | { Failed: { reason: string } }; + +export type ServiceState = { running: boolean; details: string | null }; + +export type ServiceStatus = { location_watcher: ServiceState; networking: ServiceState; volume_monitor: ServiceState; file_sharing: ServiceState }; + +/** + * Domain representation of a sidecar + */ +export type Sidecar = { id: number; content_uuid: string; kind: string; variant: string; format: string; status: string; size: number; created_at: string; updated_at: string }; + +/** + * Format for storing sidecar files + * + * Format selection guidelines: + * - Webp: Thumbnails and image derivatives (compressed images) + * - Mp4: Video/audio proxies (standard media format) + * - Json: Text-based structured data (OCR, transcripts) + * - MessagePack: Binary structured data (embeddings, vectors) + * - Text: Plain text extractions + * - Ply: 3D model format for Gaussian splats + * + * MessagePack is preferred for embeddings because: + * - 6x smaller than JSON (1.7KB vs 10KB per 384-dim vector) + * - 10x faster to parse + * - Already used in Spacedrive (job serialization) + * - Enables sub-30ms semantic search on 1M+ files + */ +export type SidecarFormat = "webp" | "mp_4" | "json" | "message_pack" | "text" | "ply"; + +export type SidecarKind = "thumb" | "thumbstrip" | "proxy" | "embeddings" | "ocr" | "transcript" | "gaussian_splat"; + +export type SidecarVariant = string; + +/** + * Filter for file size in bytes + */ +export type SizeRangeFilter = { min: number | null; max: number | null }; + +/** + * Sort direction + */ +export type SortDirection = "Asc" | "Desc"; + +/** + * Fields that can be used for sorting + */ +export type SortField = "Relevance" | "Name" | "Size" | "ModifiedAt" | "CreatedAt"; + +/** + * Sorting options for search results + */ +export type SortOptions = { field: SortField; direction: SortDirection }; + +/** + * A Space defines a sidebar layout and filtering context + */ +export type Space = { +/** + * Unique identifier + */ +id: string; +/** + * Human-friendly name (e.g., "All Devices", "Work Files") + */ +name: string; +/** + * Icon identifier (Phosphor icon name or emoji) + */ +icon: string; +/** + * Color for visual identification (hex format: #RRGGBB) + */ +color: string; +/** + * Sort order in space switcher + */ +order: number; +/** + * Timestamps + */ +created_at: string; updated_at: string }; + +export type SpaceCreateInput = { name: string; icon: string; color: string }; + +export type SpaceCreateOutput = { space: Space }; + +export type SpaceDeleteInput = { space_id: string }; + +export type SpaceDeleteOutput = { success: boolean }; + +export type SpaceGetOutput = { space: Space }; + +export type SpaceGetQueryInput = { space_id: string }; + +/** + * A SpaceGroup is a collapsible section in the sidebar + */ +export type SpaceGroup = { +/** + * Unique identifier + */ +id: string; +/** + * Space this group belongs to + */ +space_id: string; +/** + * Group name (e.g., "Quick Access", "MacBook Pro") + */ +name: string; +/** + * Type of group (determines content and behavior) + */ +group_type: GroupType; +/** + * Whether group is collapsed + */ +is_collapsed: boolean; +/** + * Sort order within space + */ +order: number; +/** + * Timestamp + */ +created_at: string }; + +/** + * A group with its items + */ +export type SpaceGroupWithItems = { +/** + * The group + */ +group: SpaceGroup; +/** + * Items in this group (sorted by order) + */ +items: SpaceItem[] }; + +/** + * An item within a space (can be space-level or within a group) + */ +export type SpaceItem = { +/** + * Unique identifier + */ +id: string; +/** + * Space this item belongs to + */ +space_id: string; +/** + * Group this item belongs to (None = space-level item) + */ +group_id: string | null; +/** + * Type discriminant (for quick type checking) + */ +item_type: ItemType; +/** + * Sort order within space or group + */ +order: number; +/** + * Timestamp + */ +created_at: string; +/** + * Resolved file data for Path items (populated by get_layout query) + */ +resolved_file?: File | null }; + +/** + * Complete sidebar layout for a space + */ +export type SpaceLayout = { +/** + * Unique identifier (same as space.id for cache matching) + */ +id: string; +/** + * The space + */ +space: Space; +/** + * Space-level items (pinned shortcuts, no group) + */ +space_items: SpaceItem[]; +/** + * Groups with their items + */ +groups: SpaceGroupWithItems[] }; + +export type SpaceLayoutQueryInput = { space_id: string }; + +export type SpaceUpdateInput = { space_id: string; name: string | null; icon: string | null; color: string | null }; + +export type SpaceUpdateOutput = { space: Space }; + +export type SpacedropSendInput = { device_id: string; paths: SdPath[]; sender: string | null }; + +export type SpacedropSendOutput = { job_id: string | null; session_id: string | null }; + +export type SpacesListOutput = { spaces: Space[] }; + +export type SpacesListQueryInput = null; + +/** + * Speech-to-text transcription policy + */ +export type SpeechPolicy = { +/** + * Whether to run speech-to-text on this location + */ +enabled: boolean; +/** + * Language for transcription + */ +language: string | null; +/** + * Model to use (e.g., "base", "small", "medium", "large") + */ +model: string; +/** + * Whether to reprocess files that already have transcriptions + */ +reprocess: boolean }; + +/** + * State transition event + */ +export type StateTransition = { from: DeviceSyncState; to: DeviceSyncState; timestamp: string; reason: string | null }; + +export type SuggestedLocation = { name: string; path: string; sd_path: SdPath }; + +export type SuggestedLocationsOutput = { locations: SuggestedLocation[] }; + +export type SuggestedLocationsQueryInput = null; + +/** + * Sync activity types for detailed sync monitoring + */ +export type SyncActivityType = { type: "BroadcastSent"; data: { changes: number } } | { type: "ChangesReceived"; data: { changes: number } } | { type: "ChangesApplied"; data: { changes: number } } | { type: "BackfillStarted" } | { type: "BackfillCompleted"; data: { records: number } } | { type: "CatchUpStarted" } | { type: "CatchUpCompleted" }; + +/** + * A logged sync event + */ +export type SyncEventLog = { id: number | null; timestamp: string; device_id: string; event_type: SyncEventType; category: EventCategory; severity: EventSeverity; summary: string; details?: JsonValue | null; correlation_id?: string | null; peer_device_id?: string | null; model_types?: string[] | null; record_count?: number | null; duration_ms?: number | null }; + +/** + * High-level sync event types + */ +export type SyncEventType = +/** + * State machine transition (Uninitialized → Backfilling → CatchingUp → Ready ⇄ Paused) + */ +"state_transition" | +/** + * Backfill session started + */ +"backfill_session_started" | +/** + * Backfill session completed successfully + */ +"backfill_session_completed" | +/** + * Backfill session failed + */ +"backfill_session_failed" | +/** + * Catch-up session started (incremental sync) + */ +"catch_up_session_started" | +/** + * Catch-up session completed + */ +"catch_up_session_completed" | +/** + * Batch of records ingested (aggregated, not per-record) + */ +"batch_ingestion" | +/** + * Sent backfill request to peer + */ +"backfill_request_sent" | +/** + * Received backfill request from peer + */ +"backfill_request_received" | +/** + * Sent backfill response to peer + */ +"backfill_response_sent" | +/** + * Peer device connected + */ +"peer_connected" | +/** + * Peer device disconnected + */ +"peer_disconnected" | +/** + * Sync error occurred + */ +"sync_error"; + +/** + * Point-in-time snapshot of all sync metrics + */ +export type SyncMetricsSnapshot = { +/** + * When this snapshot was taken + */ +timestamp: string; +/** + * State metrics + */ +state: SyncStateSnapshot; +/** + * Operation metrics + */ +operations: OperationSnapshot; +/** + * Data volume metrics + */ +data_volume: DataVolumeSnapshot; +/** + * Performance metrics + */ +performance: PerformanceSnapshot; +/** + * Error metrics + */ +errors: ErrorSnapshot }; + +/** + * State metrics snapshot + */ +export type SyncStateSnapshot = { current_state: DeviceSyncState; state_entered_at: string; uptime_seconds: number; state_history: StateTransition[]; total_time_in_state: ([DeviceSyncState, number])[]; transition_count: ([[DeviceSyncState, DeviceSyncState], number])[] }; + +export type SystemInfo = { uptime: number | null; data_directory: string; instance_name: string | null; current_library: string | null }; + +/** + * A tag with advanced capabilities for contextual organization + */ +export type Tag = { +/** + * Unique identifier + */ +id: string; +/** + * Core identity + */ +canonical_name: string; display_name: string | null; +/** + * Semantic variants for flexible access + */ +formal_name: string | null; abbreviation: string | null; aliases: string[]; +/** + * Context and categorization + */ +namespace: string | null; tag_type: TagType; +/** + * Visual and behavioral properties + */ +color: string | null; icon: string | null; description: string | null; +/** + * Advanced capabilities + */ +is_organizational_anchor: boolean; privacy_level: PrivacyLevel; search_weight: number; +/** + * Compositional attributes + */ +attributes: { [key in string]: JsonValue }; composition_rules: CompositionRule[]; +/** + * Metadata + */ +created_at: string; updated_at: string; created_by_device: string }; + +/** + * Filter for tags, supporting complex boolean logic + */ +export type TagFilter = { +/** + * Must have all of these tag IDs + */ +include: string[]; +/** + * Must not have any of these tag IDs + */ +exclude: string[] }; + +export type TagSearchFilters = { namespace: string | null; tag_type: string | null; include_archived: boolean; limit: number | null }; + +export type TagSearchResult = { +/** + * The semantic tag + */ +tag: Tag; +/** + * Relevance score (0.0-1.0) + */ +relevance: number; +/** + * Which name variant matched the search + */ +matched_variant: string | null; +/** + * Context score if disambiguation was used + */ +context_score: number | null }; + +/** + * Source of tag application + */ +export type TagSource = +/** + * Manually applied by user + */ +"User" | +/** + * Applied by AI analysis + */ +"AI" | +/** + * Imported from external source + */ +"Import" | +/** + * Synchronized from another device + */ +"Sync"; + +/** + * Specifies what to tag: content (all instances) or specific entries + */ +export type TagTargets = +/** + * Tag by content identity (applies to ALL instances of this content across devices) + * This is the preferred/default approach + */ +{ type: "Content"; ids: string[] } | +/** + * Tag by entry ID (applies to ONLY this specific file instance) + * Use when you want instance-specific tags + */ +{ type: "Entry"; ids: number[] }; + +/** + * Types of semantic tags with different behaviors + */ +export type TagType = +/** + * Standard user-created tag + */ +"Standard" | +/** + * Creates visual hierarchies in the interface + */ +"Organizational" | +/** + * Controls search and display visibility + */ +"Privacy" | +/** + * System-generated tag (AI, import, etc.) + */ +"System"; + +/** + * Text highlighting information + */ +export type TextHighlight = { field: string; text: string; start: number; end: number }; + +export type ThumbnailInput = { paths: string[]; size: number; quality: number }; + +/** + * Thumbnail generation policy + */ +export type ThumbnailPolicy = { +/** + * Whether to generate thumbnails for this location + */ +enabled: boolean; +/** + * Specific thumbnail sizes to generate (empty = use defaults) + */ +sizes: number[]; +/** + * JPEG quality (0-100) + */ +quality: number; +/** + * Whether to regenerate existing thumbnails + */ +regenerate: boolean }; + +/** + * Thumbstrip generation policy + */ +export type ThumbstripPolicy = { +/** + * Whether to generate thumbstrips for this location + */ +enabled: boolean; +/** + * Whether to regenerate existing thumbstrips + */ +regenerate: boolean }; + +export type TranscribeAudioInput = { entry_uuid: string; model: string | null; language: string | null }; + +export type TranscribeAudioOutput = { +/** + * Job ID for tracking transcription progress + */ +job_id: string }; + +/** + * Statistics for the unified ephemeral index + */ +export type UnifiedIndexStats = { +/** + * Total entries in the shared arena + */ +total_entries: number; +/** + * Number of entries indexed by path + */ +path_index_count: number; +/** + * Number of unique interned names (shared across all paths) + */ +unique_names: number; +/** + * Number of interned strings in shared cache + */ +interned_strings: number; +/** + * Number of content kinds stored + */ +content_kinds: number; +/** + * Estimated memory usage in bytes + */ +memory_bytes: number; +/** + * Age of the cache in seconds + */ +age_seconds: number; +/** + * Seconds since last access + */ +idle_seconds: number }; + +/** + * Input for finding files unique to a location + */ +export type UniqueToLocationInput = { +/** + * The location ID to find unique files for + */ +location_id: string; +/** + * Optional limit on number of results + */ +limit: number | null }; + +/** + * Output containing files that are unique to the specified location + */ +export type UniqueToLocationOutput = { +/** + * Files that exist only in the specified location + */ +unique_files: File[]; +/** + * Total count of unique files + */ +total_count: number; +/** + * Total size of unique files in bytes + */ +total_size: number }; + +export type UpdateGroupInput = { group_id: string; name: string | null; is_collapsed: boolean | null }; + +export type UpdateGroupOutput = { group: SpaceGroup }; + +/** + * Input for location path validation + */ +export type ValidateLocationPathInput = { path: SdPath }; + +/** + * Output from location path validation + */ +export type ValidateLocationPathOutput = { +/** + * Whether this path is recommended for use as a location + */ +is_recommended: boolean; +/** + * Risk level assessment + */ +risk_level: RiskLevel; +/** + * List of warnings (empty if no issues) + */ +warnings: ValidationWarning[]; +/** + * Alternative suggestion to use volume indexing + */ +suggested_alternative: VolumeIndexingSuggestion | null; +/** + * Path depth from root (number of components) + */ +path_depth: number; +/** + * Whether path is on the primary system volume + */ +is_on_primary_volume: boolean }; + +/** + * A validation warning message + */ +export type ValidationWarning = { message: string; suggestion: string | null }; + +/** + * Video metadata extracted from FFmpeg + */ +export type VideoMediaData = { uuid: string; width: number; height: number; blurhash: string | null; duration_seconds: number | null; bit_rate: number | null; codec: string | null; pixel_format: string | null; color_space: string | null; color_range: string | null; color_primaries: string | null; color_transfer: string | null; fps_num: number | null; fps_den: number | null; audio_codec: string | null; audio_channels: string | null; audio_sample_rate: number | null; audio_bit_rate: number | null; title: string | null; artist: string | null; album: string | null; creation_time: string | null; date_captured: string | null }; + +/** + * A volume in Spacedrive - unified model for runtime and database + */ +export type Volume = { +/** + * Unique identifier (used in SdPath addressing) + */ +id: string; +/** + * Volume fingerprint for identification + */ +fingerprint: VolumeFingerprint; +/** + * Device this volume is attached to + */ +device_id: string; +/** + * Human-readable name + */ +name: string; +/** + * Library this volume belongs to (None for untracked volumes) + */ +library_id: string | null; +/** + * Whether this volume is being tracked by Spacedrive + */ +is_tracked: boolean; +/** + * Primary mount point + */ +mount_point: string; +/** + * Additional mount points for the same volume + */ +mount_points: string[]; +/** + * Volume type/category + */ +volume_type: VolumeType; +/** + * Mount type classification + */ +mount_type: MountType; +/** + * Disk type (SSD, HDD, etc.) + */ +disk_type: DiskType; +/** + * Filesystem type + */ +file_system: FileSystem; +/** + * Total capacity in bytes + */ +total_capacity: number; +/** + * Currently available space in bytes + */ +available_space: number; +/** + * Whether volume is read-only + */ +is_read_only: boolean; +/** + * Whether volume is currently mounted/available + */ +is_mounted: boolean; +/** + * Hardware identifier (device path, UUID, etc.) + */ +hardware_id: string | null; +/** + * Cloud identifier (bucket/drive/container name) for cloud volumes + * This is separate from mount_point to allow display names with suffixes + * while maintaining the correct cloud resource identifier for backend operations + */ +cloud_identifier: string | null; +/** + * Cloud service configuration (service-specific settings like region, endpoint) + */ +cloud_config: JsonValue | null; +/** + * APFS container information (macOS only) + */ +apfs_container: ApfsContainer | null; +/** + * Container-relative volume ID for same-container detection + */ +container_volume_id: string | null; +/** + * Path resolution mappings (for firmlinks/symlinks) + */ +path_mappings: PathMapping[]; +/** + * Whether this volume should be visible in default views + */ +is_user_visible: boolean; +/** + * Whether this volume should be auto-tracked + */ +auto_track_eligible: boolean; +/** + * Performance metrics + */ +read_speed_mbps: number | null; write_speed_mbps: number | null; +/** + * Timestamps + */ +created_at: string; updated_at: string; last_seen_at: string; +/** + * Statistics + */ +total_files: number | null; total_directories: number | null; last_stats_update: string | null; +/** + * User preferences + */ +display_name: string | null; is_favorite: boolean; color: string | null; icon: string | null; +/** + * Error state + */ +error_message: string | null }; + +export type VolumeAddCloudInput = { service: CloudServiceType; display_name: string; config: CloudStorageConfig }; + +export type VolumeAddCloudOutput = { fingerprint: VolumeFingerprint; volume_name: string; service: CloudServiceType }; + +export type VolumeFilter = +/** + * Only return tracked volumes + */ +"TrackedOnly" | +/** + * Only return untracked volumes + */ +"UntrackedOnly" | +/** + * Return all volumes (tracked and untracked) + */ +"All"; + +/** + * Unique fingerprint for a storage volume + */ +export type VolumeFingerprint = string; + +/** + * Suggestion to use volume indexing instead + */ +export type VolumeIndexingSuggestion = { volume_fingerprint: string; volume_name: string; message: string }; + +/** + * Summary information about a volume (for updates and caching) + */ +export type VolumeInfo = { is_mounted: boolean; total_bytes_available: number; read_speed_mbps: number | null; write_speed_mbps: number | null; error_status: string | null }; + +export type VolumeItem = { id: string; name: string; fingerprint: VolumeFingerprint; volume_type: string; mount_point: string | null; +/** + * Whether this volume is currently tracked in the library + */ +is_tracked: boolean; +/** + * Whether this volume is currently online/mounted + */ +is_online: boolean; +/** + * Total capacity in bytes + */ +total_capacity: number | null; +/** + * Available capacity in bytes + */ +available_capacity: number | null; +/** + * Unique bytes (deduplicated by content_identity) + */ +unique_bytes: number | null; +/** + * Filesystem type (APFS, NTFS, ext4, etc.) + */ +file_system: string | null; +/** + * Disk type (SSD, HDD, etc.) + */ +disk_type: string | null; +/** + * Read speed in MB/s + */ +read_speed_mbps: number | null; +/** + * Write speed in MB/s + */ +write_speed_mbps: number | null; +/** + * Device ID that owns this volume + */ +device_id: string; +/** + * Device slug for constructing SdPaths + */ +device_slug: string }; + +export type VolumeListOutput = { volumes: VolumeItem[] }; + +export type VolumeListQueryInput = { +/** + * Filter volumes by tracking status (default: TrackedOnly) + */ +filter?: VolumeFilter }; + +export type VolumeRefreshInput = { +/** + * Optional: Set to true to force recalculation even if recently calculated + */ +force?: boolean }; + +export type VolumeRefreshOutput = { +/** + * Number of volumes that had their unique_bytes calculated + */ +volumes_refreshed: number; +/** + * Number of volumes that failed to refresh + */ +volumes_failed: number }; + +export type VolumeRemoveCloudInput = { fingerprint: VolumeFingerprint }; + +export type VolumeRemoveCloudOutput = { fingerprint: VolumeFingerprint }; + +export type VolumeSpeedTestInput = { fingerprint: VolumeFingerprint }; + +/** + * Output from volume speed test operation + */ +export type VolumeSpeedTestOutput = { +/** + * The fingerprint of the tested volume + */ +fingerprint: VolumeFingerprint; +/** + * Read speed in MB/s (if measured) + */ +read_speed_mbps: number | null; +/** + * Write speed in MB/s (if measured) + */ +write_speed_mbps: number | null }; + +export type VolumeTrackInput = { +/** + * Fingerprint of the volume to track + */ +fingerprint: string; +/** + * Optional custom display name + */ +display_name: string | null }; + +export type VolumeTrackOutput = { +/** + * UUID of the tracked volume + */ +volume_id: string; +/** + * Fingerprint of the volume + */ +fingerprint: string; +/** + * Display name + */ +name: string; +/** + * Whether the volume is currently online + */ +is_online: boolean }; + +/** + * Volume type classification + */ +export type VolumeType = +/** + * Primary system drive containing OS and user data + */ +"Primary" | +/** + * Dedicated user data volumes (separate from OS) + */ +"UserData" | +/** + * External or removable storage devices + */ +"External" | +/** + * Secondary internal storage (additional drives/partitions) + */ +"Secondary" | +/** + * System/OS internal volumes (hidden from normal view) + */ +"System" | +/** + * Network attached storage + */ +"Network" | +/** + * Cloud storage mounts + */ +"Cloud" | +/** + * Virtual/temporary storage + */ +"Virtual" | +/** + * Unknown or unclassified volumes + */ +"Unknown"; + +export type VolumeUntrackInput = { +/** + * UUID of the volume to untrack + */ +volume_id: string }; + +export type VolumeUntrackOutput = { +/** + * UUID of the untracked volume + */ +volume_id: string; +/** + * Whether the operation was successful + */ +success: boolean }; +// ===== API Type Unions ===== + +export type CoreAction = + { type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput } + | { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput } + | { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput } + | { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput } + | { type: 'libraries.open'; input: LibraryOpenInput; output: LibraryOpenOutput } + | { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput } + | { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput } + | { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput } + | { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput } + | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } + | { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput } + | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } + | { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput } + | { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput } + | { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput } +; + +export type LibraryAction = + { type: 'files.copy'; input: FileCopyInput; output: JobReceipt } + | { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput } + | { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } + | { type: 'files.rename'; input: FileRenameInput; output: JobReceipt } + | { type: 'indexing.start'; input: IndexInput; output: JobReceipt } + | { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } + | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } + | { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput } + | { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput } + | { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput } + | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } + | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } + | { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput } + | { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput } + | { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput } + | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } + | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } + | { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput } + | { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput } + | { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput } + | { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput } + | { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput } + | { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput } + | { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt } + | { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput } + | { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput } + | { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput } + | { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput } + | { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput } + | { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput } + | { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput } + | { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput } + | { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput } + | { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput } + | { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput } + | { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput } + | { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } + | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } + | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } + | { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput } + | { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput } + | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } + | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } + | { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput } + | { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput } +; + +export type CoreQuery = + { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus } + | { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput } + | { type: 'core.status'; input: Empty; output: CoreStatus } + | { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput } + | { type: 'jobs.remote.for_device'; input: RemoteJobsForDeviceInput; output: RemoteJobsForDeviceOutput } + | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } + | { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput } + | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } + | { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput } + | { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } + | { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } +; + +export type LibraryQuery = + { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] } + | { type: 'files.alternate_instances'; input: AlternateInstancesInput; output: AlternateInstancesOutput } + | { type: 'files.by_id'; input: FileByIdQuery; output: File } + | { type: 'files.by_path'; input: FileByPathQuery; output: File } + | { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput } + | { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } + | { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput } + | { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput } + | { type: 'jobs.active'; input: ActiveJobsInput; output: ActiveJobsOutput } + | { type: 'jobs.get_copy_metadata'; input: CopyMetadataQueryInput; output: CopyMetadataOutput } + | { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput } + | { type: 'jobs.list'; input: JobListInput; output: JobListOutput } + | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library } + | { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } + | { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } + | { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput } + | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } + | { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput } + | { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout } + | { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput } + | { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput } + | { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput } + | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } + | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + | { type: 'test.ping'; input: PingInput; output: PingOutput } + | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } +; + +// ===== Wire Method Mappings ===== + +export const WIRE_METHODS = { + coreActions: { + 'core.ephemeral_reset': 'action:core.ephemeral_reset.input', + 'core.reset': 'action:core.reset.input', + 'libraries.create': 'action:libraries.create.input', + 'libraries.delete': 'action:libraries.delete.input', + 'libraries.open': 'action:libraries.open.input', + 'models.whisper.delete': 'action:models.whisper.delete.input', + 'models.whisper.download': 'action:models.whisper.download.input', + 'network.device.revoke': 'action:network.device.revoke.input', + 'network.pair.cancel': 'action:network.pair.cancel.input', + 'network.pair.generate': 'action:network.pair.generate.input', + 'network.pair.join': 'action:network.pair.join.input', + 'network.spacedrop.send': 'action:network.spacedrop.send.input', + 'network.start': 'action:network.start.input', + 'network.stop': 'action:network.stop.input', + 'network.sync_setup': 'action:network.sync_setup.input', + }, + + libraryActions: { + 'files.copy': 'action:files.copy.input', + 'files.createFolder': 'action:files.createFolder.input', + 'files.delete': 'action:files.delete.input', + 'files.rename': 'action:files.rename.input', + 'indexing.start': 'action:indexing.start.input', + 'indexing.verify': 'action:indexing.verify.input', + 'jobs.cancel': 'action:jobs.cancel.input', + 'jobs.pause': 'action:jobs.pause.input', + 'jobs.resume': 'action:jobs.resume.input', + 'libraries.export': 'action:libraries.export.input', + 'libraries.rename': 'action:libraries.rename.input', + 'locations.add': 'action:locations.add.input', + 'locations.enable_indexing': 'action:locations.enable_indexing.input', + 'locations.export': 'action:locations.export.input', + 'locations.import': 'action:locations.import.input', + 'locations.remove': 'action:locations.remove.input', + 'locations.rescan': 'action:locations.rescan.input', + 'locations.triggerJob': 'action:locations.triggerJob.input', + 'locations.update': 'action:locations.update.input', + 'media.ocr.extract': 'action:media.ocr.extract.input', + 'media.proxy.generate': 'action:media.proxy.generate.input', + 'media.speech.transcribe': 'action:media.speech.transcribe.input', + 'media.splat.generate': 'action:media.splat.generate.input', + 'media.thumbnail': 'action:media.thumbnail.input', + 'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input', + 'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input', + 'spaces.add_group': 'action:spaces.add_group.input', + 'spaces.add_item': 'action:spaces.add_item.input', + 'spaces.create': 'action:spaces.create.input', + 'spaces.delete': 'action:spaces.delete.input', + 'spaces.delete_group': 'action:spaces.delete_group.input', + 'spaces.delete_item': 'action:spaces.delete_item.input', + 'spaces.reorder_groups': 'action:spaces.reorder_groups.input', + 'spaces.reorder_items': 'action:spaces.reorder_items.input', + 'spaces.update': 'action:spaces.update.input', + 'spaces.update_group': 'action:spaces.update_group.input', + 'tags.apply': 'action:tags.apply.input', + 'tags.create': 'action:tags.create.input', + 'volumes.add_cloud': 'action:volumes.add_cloud.input', + 'volumes.index': 'action:volumes.index.input', + 'volumes.refresh': 'action:volumes.refresh.input', + 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', + 'volumes.speed_test': 'action:volumes.speed_test.input', + 'volumes.track': 'action:volumes.track.input', + 'volumes.untrack': 'action:volumes.untrack.input', + }, + + coreQueries: { + 'core.ephemeral_status': 'query:core.ephemeral_status', + 'core.events.list': 'query:core.events.list', + 'core.status': 'query:core.status', + 'jobs.remote.all_devices': 'query:jobs.remote.all_devices', + 'jobs.remote.for_device': 'query:jobs.remote.for_device', + 'libraries.list': 'query:libraries.list', + 'models.whisper.list': 'query:models.whisper.list', + 'network.devices.list': 'query:network.devices.list', + 'network.pair.status': 'query:network.pair.status', + 'network.status': 'query:network.status', + 'network.sync_setup.discover': 'query:network.sync_setup.discover', + }, + + libraryQueries: { + 'devices.list': 'query:devices.list', + 'files.alternate_instances': 'query:files.alternate_instances', + 'files.by_id': 'query:files.by_id', + 'files.by_path': 'query:files.by_path', + 'files.content_kind_stats': 'query:files.content_kind_stats', + 'files.directory_listing': 'query:files.directory_listing', + 'files.media_listing': 'query:files.media_listing', + 'files.unique_to_location': 'query:files.unique_to_location', + 'jobs.active': 'query:jobs.active', + 'jobs.get_copy_metadata': 'query:jobs.get_copy_metadata', + 'jobs.info': 'query:jobs.info', + 'jobs.list': 'query:jobs.list', + 'libraries.info': 'query:libraries.info', + 'locations.list': 'query:locations.list', + 'locations.suggested': 'query:locations.suggested', + 'locations.validate_path': 'query:locations.validate_path', + 'search.files': 'query:search.files', + 'spaces.get': 'query:spaces.get', + 'spaces.get_layout': 'query:spaces.get_layout', + 'spaces.list': 'query:spaces.list', + 'sync.activity': 'query:sync.activity', + 'sync.eventLog': 'query:sync.eventLog', + 'sync.metrics': 'query:sync.metrics', + 'tags.search': 'query:tags.search', + 'test.ping': 'query:test.ping', + 'volumes.list': 'query:volumes.list', + }, + +} as const; diff --git a/core/src/device/manager.rs b/core/src/device/manager.rs index 07e67dd2c..5ad8778e4 100644 --- a/core/src/device/manager.rs +++ b/core/src/device/manager.rs @@ -352,6 +352,7 @@ impl DeviceManager { is_current: true, is_paired: false, is_connected: false, + connection_method: None, // Current device doesn't connect to itself } } diff --git a/core/src/domain/device.rs b/core/src/domain/device.rs index 4c0274720..5825dfca5 100644 --- a/core/src/domain/device.rs +++ b/core/src/domain/device.rs @@ -106,6 +106,35 @@ pub struct Device { /// Whether this device is currently connected via network #[serde(default)] pub is_connected: bool, + + /// Connection method when connected (Direct, Relay, or Mixed) + #[serde(default)] + #[specta(optional)] + pub connection_method: Option, +} + +/// Network connection method for a device +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Type)] +pub enum ConnectionMethod { + /// Direct peer-to-peer connection (mDNS/local network) + Direct, + /// Connection via relay server + Relay, + /// Mixed connection (both direct and relay) + Mixed, +} + +impl ConnectionMethod { + /// Convert from Iroh's ConnectionType + pub fn from_iroh_connection_type(conn_type: iroh::endpoint::ConnectionType) -> Option { + use iroh::endpoint::ConnectionType; + match conn_type { + ConnectionType::Direct(_) => Some(Self::Direct), + ConnectionType::Relay(_) => Some(Self::Relay), + ConnectionType::Mixed(_, _) => Some(Self::Mixed), + ConnectionType::None => None, + } + } } /// Operating system types @@ -184,6 +213,7 @@ impl Device { is_current: false, is_paired: false, is_connected: false, + connection_method: None, } } @@ -223,6 +253,7 @@ impl Device { pub fn from_network_info( info: &crate::service::network::device::DeviceInfo, is_connected: bool, + connection_method: Option, ) -> Self { use crate::service::network::device::DeviceType; @@ -292,6 +323,7 @@ impl Device { is_current: false, is_paired: true, is_connected, + connection_method, } } } @@ -1158,6 +1190,7 @@ impl TryFrom for Device { is_current: false, is_paired: false, is_connected: false, + connection_method: None, // Populated by caller when connection info available }) } } diff --git a/core/src/domain/mod.rs b/core/src/domain/mod.rs index 63cfe4482..9955bb471 100644 --- a/core/src/domain/mod.rs +++ b/core/src/domain/mod.rs @@ -24,7 +24,7 @@ pub mod volume; // Re-export commonly used types pub use addressing::{PathResolutionError, SdPath, SdPathBatch, SdPathParseError}; pub use content_identity::{ContentHashError, ContentHashGenerator, ContentIdentity, ContentKind}; -pub use device::{Device, OperatingSystem}; +pub use device::{ConnectionMethod, Device, OperatingSystem}; pub use file::{EntryKind, File, Sidecar}; pub use library::Library; pub use location::{IndexMode, Location, ScanState}; diff --git a/core/src/lib.rs b/core/src/lib.rs index 9ce27ec7c..228ec61ca 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -329,6 +329,8 @@ impl Core { context.set_networking(networking.clone()).await; // Set event bus for device registry to emit ResourceChanged events networking.set_event_bus(context.events.clone()).await; + // Set library manager for device registry to query complete device data + networking.set_library_manager(Arc::downgrade(&context.libraries().await)).await; info!("Networking service registered in context"); // Initialize sync service on already-loaded libraries @@ -540,6 +542,8 @@ impl Core { // Set event bus for device registry to emit ResourceChanged events networking_service.set_event_bus(self.events.clone()).await; + // Set library manager for device registry to query complete device data + networking_service.set_library_manager(Arc::downgrade(&self.context.libraries().await)).await; } logger.info("Networking initialized successfully").await; diff --git a/core/src/ops/devices/list/query.rs b/core/src/ops/devices/list/query.rs index a466ddbe7..510485166 100644 --- a/core/src/ops/devices/list/query.rs +++ b/core/src/ops/devices/list/query.rs @@ -120,6 +120,15 @@ impl LibraryQuery for ListLibraryDevicesQuery { device.is_current = device.id == current_device_id; device.is_paired = false; // Updated below if device is also in network registry device.is_connected = false; // Updated below if device is connected via network + + // For remote devices, set is_online based on network connection (will be updated below) + // For current device, it's always online + if device.is_current { + device.is_online = true; + } else { + device.is_online = false; // Will be set to true if connected via network + } + result.push(device); } Err(e) => { @@ -128,64 +137,84 @@ impl LibraryQuery for ListLibraryDevicesQuery { } } - // If show_paired is true, also fetch paired network devices - if self.input.show_paired { - // Get networking service - if let Some(networking) = context.get_networking().await { - let device_registry = networking.device_registry(); - let registry = device_registry.read().await; - let all_devices = registry.get_all_devices(); + // Always check network registry to update connection status for database devices + // and optionally add paired-only devices + if let Some(networking) = context.get_networking().await { + let device_registry = networking.device_registry(); + let registry = device_registry.read().await; + let all_devices = registry.get_all_devices(); - // Get Iroh endpoint for verifying actual connection status - // This is the source of truth, not the cached DeviceState - let endpoint = networking.endpoint(); + // Get Iroh endpoint for verifying actual connection status + // This is the source of truth, not the cached DeviceState + let endpoint = networking.endpoint(); - for (device_id, state) in all_devices { - use crate::service::network::device::DeviceState; + for (device_id, state) in all_devices { + use crate::service::network::device::DeviceState; - // Query Iroh directly for actual connection status - let is_actually_connected = if let Some(ep) = endpoint { - registry.is_node_connected(ep, device_id) + // Query Iroh directly for actual connection status and method + let (is_actually_connected, connection_method) = if let Some(ep) = endpoint { + // Get node ID for this device + let node_id = registry.get_node_id_for_device(device_id); + if let Some(node_id) = node_id { + // Query Iroh for connection info + if let Some(remote_info) = ep.remote_info(node_id) { + let conn_method = crate::domain::device::ConnectionMethod::from_iroh_connection_type(remote_info.conn_type); + let is_connected = conn_method.is_some(); + (is_connected, conn_method) + } else { + (false, None) + } } else { - // No endpoint available, fall back to cached state - matches!(state, DeviceState::Connected { .. }) - }; + (false, None) + } + } else { + // No endpoint available, fall back to cached state + let is_connected = matches!(state, DeviceState::Connected { .. }); + (is_connected, None) + }; - // Check if this device is already in the library results - if let Some(existing) = result.iter_mut().find(|d| d.id == device_id) { - // Update pairing/connection status for library device that's also in network registry - match state { - DeviceState::Paired { .. } - | DeviceState::Connected { .. } - | DeviceState::Disconnected { .. } => { - existing.is_paired = true; - } - _ => {} - } - if is_actually_connected { - existing.is_connected = true; - existing.is_online = true; + // Check if this device is already in the library results + if let Some(existing) = result.iter_mut().find(|d| d.id == device_id) { + // Update pairing/connection status for library device that's also in network registry + match state { + DeviceState::Paired { .. } + | DeviceState::Connected { .. } + | DeviceState::Disconnected { .. } => { + existing.is_paired = true; } + _ => {} + } + + // Always update online/connected status based on current network state + // (database is_online column can be stale for remote devices) + existing.is_connected = is_actually_connected; + existing.is_online = is_actually_connected; + existing.connection_method = connection_method; + + continue; + } + + // Only add paired-only devices (not in database) if show_paired is true + if !self.input.show_paired { + continue; + } + + let device_info = match state { + DeviceState::Paired { info, .. } => Some(info), + DeviceState::Connected { info, .. } => Some(info), + DeviceState::Disconnected { info, .. } => Some(info), + _ => None, + }; + + if let Some(info) = device_info { + // Filter by online status if requested + if !self.input.include_offline && !is_actually_connected { continue; } - let device_info = match state { - DeviceState::Paired { info, .. } => Some(info), - DeviceState::Connected { info, .. } => Some(info), - DeviceState::Disconnected { info, .. } => Some(info), - _ => None, - }; - - if let Some(info) = device_info { - // Filter by online status if requested - if !self.input.include_offline && !is_actually_connected { - continue; - } - - // Convert network DeviceInfo to domain Device - let device = Device::from_network_info(&info, is_actually_connected); - result.push(device); - } + // Convert network DeviceInfo to domain Device + let mut device = Device::from_network_info(&info, is_actually_connected, connection_method); + result.push(device); } } } diff --git a/core/src/ops/files/copy/metadata.rs b/core/src/ops/files/copy/metadata.rs index bda25ce3e..d9eb9c69a 100644 --- a/core/src/ops/files/copy/metadata.rs +++ b/core/src/ops/files/copy/metadata.rs @@ -111,14 +111,22 @@ impl CopyJobMetadata { /// Update status of a file by source path pub fn update_status(&mut self, source_path: &SdPath, status: CopyFileStatus) { - if let Some(entry) = self.files.iter_mut().find(|e| &e.source_path == source_path) { + if let Some(entry) = self + .files + .iter_mut() + .find(|e| &e.source_path == source_path) + { entry.status = status; } } /// Set error for a file by source path pub fn set_error(&mut self, source_path: &SdPath, error: String) { - if let Some(entry) = self.files.iter_mut().find(|e| &e.source_path == source_path) { + if let Some(entry) = self + .files + .iter_mut() + .find(|e| &e.source_path == source_path) + { entry.status = CopyFileStatus::Failed; entry.error = Some(error); } diff --git a/core/src/service/network/core/event_loop.rs b/core/src/service/network/core/event_loop.rs index ddc18a1b1..97031ddf2 100644 --- a/core/src/service/network/core/event_loop.rs +++ b/core/src/service/network/core/event_loop.rs @@ -84,6 +84,9 @@ pub struct NetworkingEventLoop { /// Active connections tracker (keyed by NodeId and ALPN) active_connections: Arc), Connection>>>, + /// Nodes that already have connection watchers spawned (to prevent duplicates) + watched_nodes: Arc>>, + /// Logger for event loop operations logger: Arc, } @@ -113,6 +116,7 @@ impl NetworkingEventLoop { shutdown_tx, identity, active_connections, + watched_nodes: Arc::new(RwLock::new(std::collections::HashSet::new())), logger, } } @@ -214,6 +218,9 @@ impl NetworkingEventLoop { connections.insert((remote_node_id, alpn_bytes), conn.clone()); } + // Spawn a task to watch for connection closure for instant reactivity + self.spawn_connection_watcher(conn.clone(), remote_node_id).await; + // For now, we'll need to detect ALPN from the first stream // TODO: Find the correct way to get ALPN from iroh Connection let alpn = PAIRING_ALPN; // Default to pairing, will be overridden based on stream detection @@ -647,6 +654,9 @@ impl NetworkingEventLoop { connections.insert((node_id, alpn_bytes.clone()), conn.clone()); } + // Spawn a task to watch for connection closure for instant reactivity + self.spawn_connection_watcher(conn.clone(), node_id).await; + self.logger .info(&format!( "Tracking outbound connection to {} (ALPN: {:?}), spawning stream handler", @@ -756,6 +766,9 @@ impl NetworkingEventLoop { connections.insert((node_id, alpn_bytes), conn.clone()); } + // Spawn a task to watch for connection closure for instant reactivity + self.spawn_connection_watcher(conn.clone(), node_id).await; + // Open appropriate stream based on protocol match protocol { "pairing" | "messaging" => { @@ -969,4 +982,79 @@ impl NetworkingEventLoop { } } } + + /// Spawn a background task to watch for connection closure + /// + /// This provides instant reactivity when connections drop, instead of waiting + /// for the 10-second polling interval in update_connection_states(). + async fn spawn_connection_watcher(&self, conn: Connection, node_id: NodeId) { + // Check if we already have a watcher for this node + { + let mut watched = self.watched_nodes.write().await; + if watched.contains(&node_id) { + // Already watching this node, skip to prevent duplicates + return; + } + watched.insert(node_id); + } + + let device_registry = self.device_registry.clone(); + let active_connections = self.active_connections.clone(); + let watched_nodes = self.watched_nodes.clone(); + let logger = self.logger.clone(); + + tokio::spawn(async move { + // Wait for the connection to close + let close_reason = conn.closed().await; + + logger + .info(&format!( + "Connection to {} closed instantly: {:?}", + node_id, close_reason + )) + .await; + + // Remove from active connections + { + let mut connections = active_connections.write().await; + connections.retain(|(nid, _alpn), _conn| *nid != node_id); + } + + // Remove from watched nodes set so future reconnections can spawn a new watcher + { + let mut watched = watched_nodes.write().await; + watched.remove(&node_id); + } + + // Find the device ID for this node and update state + let mut registry = device_registry.write().await; + if let Some(device_id) = registry.get_device_by_node_id(node_id) { + // Use update_device_from_connection with ConnectionType::None + // This handles any current state and transitions appropriately + if let Err(e) = registry + .update_device_from_connection( + device_id, + node_id, + iroh::endpoint::ConnectionType::None, + None, + ) + .await + { + logger + .warn(&format!( + "Failed to update device {} after connection closed: {}", + device_id, e + )) + .await; + } else { + logger + .info(&format!( + "Device {} instantly marked as offline after connection closed", + device_id + )) + .await; + } + } + }); + } } diff --git a/core/src/service/network/core/mod.rs b/core/src/service/network/core/mod.rs index 4216e37cc..5269e3d4c 100644 --- a/core/src/service/network/core/mod.rs +++ b/core/src/service/network/core/mod.rs @@ -107,6 +107,9 @@ pub struct NetworkingService { /// Each ALPN protocol requires its own connection since ALPN is negotiated at connection establishment active_connections: Arc), Connection>>>, + /// Nodes that already have connection watchers spawned (to prevent duplicates) + watched_nodes: Arc>>, + /// Sync multiplexer for routing sync messages to correct library sync_multiplexer: Arc, @@ -159,6 +162,7 @@ impl NetworkingService { device_registry, event_sender, active_connections: Arc::new(RwLock::new(std::collections::HashMap::new())), + watched_nodes: Arc::new(RwLock::new(std::collections::HashSet::new())), sync_multiplexer, logger, }) @@ -173,6 +177,15 @@ impl NetworkingService { registry.set_event_bus(event_bus); } + /// Set the library manager for querying complete device data + /// + /// This enables the device registry to emit complete device data with hardware_model + /// by querying the library database instead of just using network DeviceInfo. + pub async fn set_library_manager(&self, library_manager: std::sync::Weak) { + let mut registry = self.device_registry.write().await; + registry.set_library_manager(library_manager); + } + /// Start the networking service pub async fn start(&mut self) -> Result<()> { // Check if already started @@ -965,6 +978,82 @@ impl NetworkingService { ) } + /// Spawn a background task to watch for connection closure + /// + /// This provides instant reactivity when connections drop by waiting on + /// Iroh's Connection::closed() future, instead of relying on the 10-second + /// polling interval in update_connection_states(). + async fn spawn_connection_watcher(&self, conn: Connection, node_id: NodeId) { + // Check if we already have a watcher for this node + { + let mut watched = self.watched_nodes.write().await; + if watched.contains(&node_id) { + // Already watching this node, skip to prevent duplicates + return; + } + watched.insert(node_id); + } + + let device_registry = self.device_registry.clone(); + let active_connections = self.active_connections.clone(); + let watched_nodes = self.watched_nodes.clone(); + let logger = self.logger.clone(); + + tokio::spawn(async move { + // Wait for the connection to close + let close_reason = conn.closed().await; + + logger + .info(&format!( + "Connection to {} closed instantly: {:?}", + node_id, close_reason + )) + .await; + + // Remove from active connections + { + let mut connections = active_connections.write().await; + connections.retain(|(nid, _alpn), _conn| *nid != node_id); + } + + // Remove from watched nodes set so future reconnections can spawn a new watcher + { + let mut watched = watched_nodes.write().await; + watched.remove(&node_id); + } + + // Find the device ID for this node and update state + let mut registry = device_registry.write().await; + if let Some(device_id) = registry.get_device_by_node_id(node_id) { + // Use update_device_from_connection with ConnectionType::None + // This handles any current state and transitions appropriately + if let Err(e) = registry + .update_device_from_connection( + device_id, + node_id, + iroh::endpoint::ConnectionType::None, + None, + ) + .await + { + logger + .warn(&format!( + "Failed to update device {} after connection closed: {}", + device_id, e + )) + .await; + } else { + logger + .info(&format!( + "Device {} instantly marked as offline after connection closed", + device_id + )) + .await; + } + } + }); + } + /// Connect to a node at a specific address /// /// # Parameters @@ -989,7 +1078,7 @@ impl NetworkingService { let node_id = node_addr.node_id; { let mut connections = self.active_connections.write().await; - connections.insert((node_id, PAIRING_ALPN.to_vec()), conn); + connections.insert((node_id, PAIRING_ALPN.to_vec()), conn.clone()); self.logger .info(&format!( "Tracked outbound pairing connection to {}", @@ -998,6 +1087,9 @@ impl NetworkingService { .await; } + // Spawn a task to watch for connection closure for instant reactivity + self.spawn_connection_watcher(conn, node_id).await; + Ok(()) } else { Err(NetworkingError::ConnectionFailed( @@ -1160,9 +1252,12 @@ impl NetworkingService { // Track the connection for the pairing protocol { let mut connections = self.active_connections.write().await; - connections.insert((node_id, PAIRING_ALPN.to_vec()), conn); + connections.insert((node_id, PAIRING_ALPN.to_vec()), conn.clone()); } + // Spawn a task to watch for connection closure for instant reactivity + self.spawn_connection_watcher(conn, node_id).await; + Ok(()) } Ok(Err(e)) => { diff --git a/core/src/service/network/device/registry.rs b/core/src/service/network/device/registry.rs index 158e1cc56..2bb4e0682 100644 --- a/core/src/service/network/device/registry.rs +++ b/core/src/service/network/device/registry.rs @@ -36,6 +36,9 @@ pub struct DeviceRegistry { /// Event bus for emitting resource change events event_bus: Option>, + + /// Library manager for querying device data from database + library_manager: Option>, } impl DeviceRegistry { @@ -55,6 +58,7 @@ impl DeviceRegistry { persistence, logger, event_bus: None, + library_manager: None, } } @@ -63,14 +67,126 @@ impl DeviceRegistry { self.event_bus = Some(event_bus); } - /// Emit a ResourceChanged event for a device + /// Set the library manager for querying device data + pub fn set_library_manager(&mut self, library_manager: std::sync::Weak) { + self.library_manager = Some(library_manager); + } + + /// Update device online status in library database + async fn update_device_online_status(&self, device_id: Uuid, is_online: bool) { + let Some(library_manager_weak) = &self.library_manager else { + return; + }; + + let Some(library_manager) = library_manager_weak.upgrade() else { + return; + }; + + // Update in all libraries (device data is synced across libraries) + let all_libraries = library_manager.list().await; + + for lib in all_libraries { + let db = lib.db().conn(); + + // Update device is_online status in database + use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; + + match crate::infra::db::entities::device::Entity::find() + .filter(crate::infra::db::entities::device::Column::Uuid.eq(device_id.as_bytes().to_vec())) + .one(db) + .await + { + Ok(Some(model)) => { + let mut active_model: crate::infra::db::entities::device::ActiveModel = model.into(); + active_model.is_online = Set(is_online); + active_model.last_seen_at = Set(chrono::Utc::now()); + + if let Err(e) = active_model.update(db).await { + tracing::warn!( + device_id = %device_id, + error = %e, + "Failed to update device online status in database" + ); + } + } + Ok(None) => { + // Device not in this library's database, skip + } + Err(e) => { + tracing::warn!( + device_id = %device_id, + error = %e, + "Failed to query device from database" + ); + } + } + } + } + + /// Emit a ResourceChanged event for a device with complete database data fn emit_device_changed(&self, device_id: Uuid, info: &DeviceInfo, is_connected: bool) { let Some(event_bus) = &self.event_bus else { return; }; - // Convert network DeviceInfo to domain Device - let device = crate::domain::Device::from_network_info(info, is_connected); + // Try to query the full device from database to get hardware_model + let device = if let Some(library_manager_weak) = &self.library_manager { + if let Some(library_manager) = library_manager_weak.upgrade() { + // Try to get device from first available library + // (device data should be consistent across libraries since it's synced) + let rt = tokio::runtime::Handle::try_current(); + if let Ok(handle) = rt { + let device_result = handle.block_on(async { + // Get all libraries + let all_libraries = library_manager.list().await; + + for lib in all_libraries { + let db = lib.db().conn(); + + // Query device from database by UUID + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; + if let Ok(Some(model)) = crate::infra::db::entities::device::Entity::find() + .filter(crate::infra::db::entities::device::Column::Uuid.eq(device_id.as_bytes().to_vec())) + .one(db) + .await + { + // Convert to domain Device + if let Ok(mut device) = crate::domain::Device::try_from(model) { + // Merge with network state + device.is_connected = is_connected; + device.is_online = is_connected; + device.is_paired = true; + + // Note: We don't have access to endpoint here to get connection_method + // but that's okay - the query will populate it with the correct data + // from Iroh's RemoteInfo + device.connection_method = None; + + return Some(device); + } + } + } + None + }); + + if let Some(device) = device_result { + device + } else { + // Fallback to network data if database query fails + crate::domain::Device::from_network_info(info, is_connected, None) + } + } else { + // No runtime, fallback to network data + crate::domain::Device::from_network_info(info, is_connected, None) + } + } else { + // Libraries dropped, fallback to network data + crate::domain::Device::from_network_info(info, is_connected, None) + } + } else { + // No libraries set, fallback to network data + crate::domain::Device::from_network_info(info, is_connected, None) + }; use crate::domain::resource::EventEmitter; if let Err(e) = device.emit_changed(event_bus) { @@ -329,6 +445,9 @@ impl DeviceRegistry { .await; } + // Update device online status in library database + self.update_device_online_status(device_id, true).await; + // Emit ResourceChanged event for UI reactivity self.emit_device_changed(device_id, &info, true); @@ -383,6 +502,9 @@ impl DeviceRegistry { .await; } + // Update device online status in library database + self.update_device_online_status(device_id, false).await; + // Emit ResourceChanged event for UI reactivity self.emit_device_changed(device_id, &info, false); @@ -564,6 +686,9 @@ impl DeviceRegistry { .await .ok(); + // Update device online status in library database + self.update_device_online_status(device_id, true).await; + // Emit ResourceChanged event for UI reactivity self.emit_device_changed(device_id, &info, true); } @@ -601,6 +726,9 @@ impl DeviceRegistry { .await .ok(); + // Update device online status in library database + self.update_device_online_status(device_id, false).await; + // Emit ResourceChanged event for UI reactivity self.emit_device_changed(device_id, &info, false); } diff --git a/packages/interface/src/components/JobManager/components/SpeedGraph.tsx b/packages/interface/src/components/JobManager/components/SpeedGraph.tsx index aa42d23f9..35283dfb5 100644 --- a/packages/interface/src/components/JobManager/components/SpeedGraph.tsx +++ b/packages/interface/src/components/JobManager/components/SpeedGraph.tsx @@ -85,11 +85,23 @@ function SpeedGraphVisualization({ // Add 10% headroom to max for better visualization const yMax = maxRate * 1.1; - // Generate points for the line - const points = speedHistory.map((sample, index) => { + // Apply exponential smoothing to debounce rapid changes while retaining shape + const smoothingFactor = 0.3; // Lower = smoother (0.1-0.4 range works well) + const smoothedRates = speedHistory.reduce((acc, sample, index) => { + if (index === 0) { + acc.push(sample.bytesPerSecond); + } else { + const smoothed = acc[index - 1] + smoothingFactor * (sample.bytesPerSecond - acc[index - 1]); + acc.push(smoothed); + } + return acc; + }, []); + + // Generate points for the line using smoothed rates + const points = smoothedRates.map((smoothedRate, index) => { const x = padding.left + (index / Math.max(speedHistory.length - 1, 1)) * graphWidth; - const y = padding.top + graphHeight - (sample.bytesPerSecond / yMax) * graphHeight; - return { x, y, rate: sample.bytesPerSecond }; + const y = padding.top + graphHeight - (smoothedRate / yMax) * graphHeight; + return { x, y, rate: smoothedRate }; }); // Generate SVG path for smooth curve using quadratic bezier diff --git a/packages/interface/src/components/JobManager/renderers/FileCopyRenderer.tsx b/packages/interface/src/components/JobManager/renderers/FileCopyRenderer.tsx index 7a62e2ed4..a5f72be4d 100644 --- a/packages/interface/src/components/JobManager/renderers/FileCopyRenderer.tsx +++ b/packages/interface/src/components/JobManager/renderers/FileCopyRenderer.tsx @@ -2,6 +2,8 @@ import { Pause, Play, X, CaretDown } from "@phosphor-icons/react"; import { motion } from "framer-motion"; import type { JobRenderer, JobRendererProps, JobDetailsRendererProps } from "./index"; import { CopyJobDetails } from "../components/CopyJobDetails"; +import { useNormalizedQuery } from "../../../contexts/SpacedriveContext"; +import type { Device } from "@sd/ts-client"; /** * Map strategy name to display label (enables i18n in future) @@ -11,13 +13,13 @@ function getStrategyLabel(strategyName: string | undefined, isMove: boolean): st switch (strategyName) { case "RemoteTransfer": - return isMove ? "Network move" : "Network copy"; + return "Network"; case "LocalMove": - return "Atomic move"; + return "Atomic"; case "FastCopy": - return "Fast copy"; + return "Fast"; case "LocalStream": - return isMove ? "Streaming move" : "Streaming copy"; + return "Streaming"; default: return strategyName; } @@ -119,16 +121,38 @@ function FileCopyCardContent({ const strategyName = metadata?.strategy?.strategy_name; const strategyLabel = getStrategyLabel(strategyName, job.action_context?.action_type === "files.move"); + // Fetch devices to determine if destination is remote + const { data: devices } = useNormalizedQuery({ + wireMethod: "query:devices.list", + input: { include_offline: true, include_details: false }, + resourceType: "device", + }); + // Determine if this is a move operation const isMove = job.action_context?.action_type === "files.move"; + // Check if this is a cross-device transfer from metadata + const isCrossDevice = metadata?.strategy?.is_cross_device === true; + + // Find current device and infer destination device + const currentDevice = devices?.find(d => d.is_current); + + // For cross-device transfers, the destination is the device that's NOT current + // (assuming only 2 devices in the transfer scenario) + const destinationDevice = isCrossDevice && currentDevice + ? devices?.find(d => !d.is_current) + : null; + // Calculate title const fileCount = generic?.completion?.total || 0; const fileName = extractFirstFileName(job); - const title = - fileCount > 1 - ? `${isMove ? "Moving" : "Copying"} ${fileCount} items` - : `${isMove ? "Moving" : "Copying"} '${fileName}'`; + const baseTitle = fileCount > 1 + ? `${isMove ? "Moving" : "Copying"} ${fileCount} items` + : `${isMove ? "Moving" : "Copying"} '${fileName}'`; + + const title = destinationDevice + ? `${baseTitle} to ${destinationDevice.name}` + : baseTitle; // Calculate rich subtext with progress, speed, and ETA const completed = generic?.completion?.completed || 0; diff --git a/packages/interface/src/routes/overview/DevicePanel.tsx b/packages/interface/src/routes/overview/DevicePanel.tsx index c82c5ed60..5aa0eb003 100644 --- a/packages/interface/src/routes/overview/DevicePanel.tsx +++ b/packages/interface/src/routes/overview/DevicePanel.tsx @@ -1,47 +1,54 @@ -import { useState, useRef, useEffect } from "react"; -import { motion } from "framer-motion"; import { - HardDrive, - Plus, - Database, CaretLeft, CaretRight, -} from "@phosphor-icons/react"; -import Masonry from "react-masonry-css"; -import DriveIcon from "@sd/assets/icons/Drive.png"; -import HDDIcon from "@sd/assets/icons/HDD.png"; -import ServerIcon from "@sd/assets/icons/Server.png"; -import DatabaseIcon from "@sd/assets/icons/Database.png"; -import DriveAmazonS3Icon from "@sd/assets/icons/Drive-AmazonS3.png"; -import DriveGoogleDriveIcon from "@sd/assets/icons/Drive-GoogleDrive.png"; -import DriveDropboxIcon from "@sd/assets/icons/Drive-Dropbox.png"; -import LocationIcon from "@sd/assets/icons/Location.png"; -import { TopBarButton } from "@sd/ui"; -import { - useNormalizedQuery, - useLibraryMutation, - getDeviceIcon, - useCoreQuery, -} from "../../contexts/SpacedriveContext"; + Cpu, + Database, + HardDrive, + Memory, + Plus +} from '@phosphor-icons/react'; +import DatabaseIcon from '@sd/assets/icons/Database.png'; +import DriveAmazonS3Icon from '@sd/assets/icons/Drive-AmazonS3.png'; +import DriveDropboxIcon from '@sd/assets/icons/Drive-Dropbox.png'; +import DriveGoogleDriveIcon from '@sd/assets/icons/Drive-GoogleDrive.png'; +import DriveIcon from '@sd/assets/icons/Drive.png'; +import HDDIcon from '@sd/assets/icons/HDD.png'; +import LocationIcon from '@sd/assets/icons/Location.png'; +import ServerIcon from '@sd/assets/icons/Server.png'; import type { - VolumeListOutput, - VolumeListQueryInput, - VolumeItem, Device, - ListLibraryDevicesInput, JobListItem, + ListLibraryDevicesInput, + Location, LocationsListOutput, LocationsListQueryInput, - Location, -} from "@sd/ts-client"; -import { useJobs } from "../../components/JobManager/hooks/useJobs"; -import { JobCard } from "../../components/JobManager/components/JobCard"; -import clsx from "clsx"; + VolumeItem, + VolumeListOutput, + VolumeListQueryInput +} from '@sd/ts-client'; +import {TopBarButton} from '@sd/ui'; +import clsx from 'clsx'; +import {motion} from 'framer-motion'; +import {useEffect, useRef, useState} from 'react'; +import Masonry from 'react-masonry-css'; +import {JobCard} from '../../components/JobManager/components/JobCard'; +import {useJobs} from '../../components/JobManager/hooks/useJobs'; +import { + getDeviceIcon, + useCoreQuery, + useLibraryMutation, + useNormalizedQuery +} from '../../contexts/SpacedriveContext'; + +// Temporary type extension until types are regenerated +type DeviceWithConnection = Device & { + connection_method?: 'Direct' | 'Relay' | 'Mixed' | null; +}; function formatBytes(bytes: number): string { - if (bytes === 0) return "0 B"; + if (bytes === 0) return '0 B'; const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; + 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]}`; } @@ -49,71 +56,71 @@ function formatBytes(bytes: number): string { function getVolumeIcon(volumeType: any, name?: string): string { // Convert volume type to string if it's an enum variant object const volumeTypeStr = - typeof volumeType === "string" + typeof volumeType === 'string' ? volumeType : volumeType?.Other || JSON.stringify(volumeType); // Check for cloud providers by name - if (name?.includes("S3")) return DriveAmazonS3Icon; - if (name?.includes("Google")) return DriveGoogleDriveIcon; - if (name?.includes("Dropbox")) return DriveDropboxIcon; + if (name?.includes('S3')) return DriveAmazonS3Icon; + if (name?.includes('Google')) return DriveGoogleDriveIcon; + if (name?.includes('Dropbox')) return DriveDropboxIcon; // By type - if (volumeTypeStr === "Cloud") return DriveIcon; - if (volumeTypeStr === "Network") return ServerIcon; - if (volumeTypeStr === "Virtual") return DatabaseIcon; + if (volumeTypeStr === 'Cloud') return DriveIcon; + if (volumeTypeStr === 'Network') return ServerIcon; + if (volumeTypeStr === 'Virtual') return DatabaseIcon; return HDDIcon; } function getDiskTypeLabel(diskType: string): string { - return diskType === "SSD" ? "SSD" : diskType === "HDD" ? "HDD" : diskType; + return diskType === 'SSD' ? 'SSD' : diskType === 'HDD' ? 'HDD' : diskType; } interface DevicePanelProps { onLocationSelect?: (location: Location | null) => void; } -export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { +export function DevicePanel({onLocationSelect}: DevicePanelProps = {}) { const [selectedLocationId, setSelectedLocationId] = useState( - null, + null ); // Fetch all volumes using normalized cache - const { data: volumesData, isLoading: volumesLoading } = useNormalizedQuery< + const {data: volumesData, isLoading: volumesLoading} = useNormalizedQuery< VolumeListQueryInput, VolumeListOutput >({ - wireMethod: "query:volumes.list", - input: { filter: "All" }, - resourceType: "volume", + wireMethod: 'query:volumes.list', + input: {filter: 'All'}, + resourceType: 'volume' }); // Fetch all devices using normalized cache - const { data: devicesData, isLoading: devicesLoading } = useNormalizedQuery< + const {data: devicesData, isLoading: devicesLoading} = useNormalizedQuery< ListLibraryDevicesInput, - Device[] + DeviceWithConnection[] >({ - wireMethod: "query:devices.list", - input: { include_offline: true, include_details: false }, - resourceType: "device", + wireMethod: 'query:devices.list', + input: {include_offline: true, include_details: false}, + resourceType: 'device' }); // Fetch all locations using normalized cache - const { data: locationsData, isLoading: locationsLoading } = + const {data: locationsData, isLoading: locationsLoading} = useNormalizedQuery({ - wireMethod: "query:locations.list", + wireMethod: 'query:locations.list', input: null, - resourceType: "location", + resourceType: 'location' }); // Get all jobs with real-time updates (local jobs) - const { jobs: localJobs } = useJobs(); + const {jobs: localJobs} = useJobs(); // Get remote device jobs // TODO: This should have its own hook like useJobs, this will not work reactively - const { data: remoteJobsData } = useCoreQuery({ - type: "jobs.remote.all_devices", - input: {}, + const {data: remoteJobsData} = useCoreQuery({ + type: 'jobs.remote.all_devices', + input: {} }); // Merge local and remote jobs @@ -129,19 +136,19 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { status: remoteJob.status, progress: remoteJob.progress || 0, action_type: null, - action_context: null, + action_context: null })) - : []), + : []) ] as JobListItem[]; if (volumesLoading || devicesLoading || locationsLoading) { return ( -
-
-

+
+
+

Storage Volumes

-

+

Loading volumes...

@@ -155,7 +162,7 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { // Filter to only show user-visible volumes const userVisibleVolumes = volumes.filter( - (volume) => volume.is_user_visible !== false, + (volume) => volume.is_user_visible !== false ); // Group volumes by device_id @@ -168,7 +175,7 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { acc[deviceId].push(volume); return acc; }, - {} as Record, + {} as Record ); // Group locations by device slug @@ -176,8 +183,8 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { (acc, location) => { // Extract device_slug from sd_path if ( - typeof location.sd_path === "object" && - "Physical" in location.sd_path + typeof location.sd_path === 'object' && + 'Physical' in location.sd_path ) { const deviceSlug = location.sd_path.Physical.device_slug; if (!acc[deviceSlug]) { @@ -187,7 +194,7 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { } return acc; }, - {} as Record, + {} as Record ); // Create device map for quick lookup @@ -196,7 +203,7 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { acc[device.id] = device; return acc; }, - {} as Record, + {} as Record ); // Group jobs by device_id @@ -209,20 +216,20 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { acc[deviceId].push(job); return acc; }, - {} as Record, + {} as Record ); const breakpointColumns = { default: 3, 1600: 2, - 1000: 1, + 1000: 1 }; return (
{devices.map((device) => { @@ -252,11 +259,11 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { })} {devices.length === 0 && ( -
-
- +
+
+

No devices detected

-

+

Pair a device to get started

@@ -267,8 +274,41 @@ export function DevicePanel({ onLocationSelect }: DevicePanelProps = {}) { ); } +interface ConnectionBadgeProps { + method: 'Direct' | 'Relay' | 'Mixed'; +} + +function ConnectionBadge({method}: ConnectionBadgeProps) { + const colors = { + Direct: { + dot: 'bg-green-500', + text: 'text-green-500', + label: 'Local' + }, + Relay: { + dot: 'bg-yellow-500', + text: 'text-yellow-500', + label: 'Relay' + }, + Mixed: { + dot: 'bg-blue-500', + text: 'text-blue-500', + label: 'Mixed' + } + }; + + const {dot, text, label} = colors[method]; + + return ( +
+
+ {label} +
+ ); +} + interface DeviceCardProps { - device?: Device; + device?: DeviceWithConnection; volumes: VolumeItem[]; jobs: JobListItem[]; locations: Location[]; @@ -282,22 +322,21 @@ function DeviceCard({ jobs, locations, selectedLocationId, - onLocationSelect, + onLocationSelect }: DeviceCardProps) { - const deviceName = device?.name || "Unknown Device"; + const deviceName = device?.name || 'Unknown Device'; const deviceIconSrc = device ? getDeviceIcon(device) : null; - const { pause, resume, getSpeedHistory } = useJobs(); - + const {pause, resume, getSpeedHistory} = useJobs(); // Format hardware specs const cpuInfo = device?.cpu_model - ? `${device.cpu_model}${device.cpu_physical_cores ? ` � ${device.cpu_physical_cores}C` : ""}` + ? `${device.cpu_model}${device.cpu_cores_physical ? ` � ${device.cpu_cores_physical}C` : ''}` : null; - const ramInfo = device?.memory_total - ? formatBytes(device.memory_total) + const ramInfo = device?.memory_total_bytes + ? formatBytes(device.memory_total_bytes) : null; // Convert form_factor enum to string const formFactor = device?.form_factor - ? typeof device.form_factor === "string" + ? typeof device.form_factor === 'string' ? device.form_factor : (device.form_factor as any)?.Other || JSON.stringify(device.form_factor) @@ -306,72 +345,82 @@ function DeviceCard({ // Filter active jobs const activeJobs = jobs.filter( - (j) => j.status === "running" || j.status === "paused", + (j) => j.status === 'running' || j.status === 'paused' ); return ( -
+
{/* Device Header */} -
+
{/* Left: Device icon and name */} -
+
{deviceIconSrc ? ( {deviceName} ) : ( )}
-

- {deviceName} -

-

- {volumes.length}{" "} - {volumes.length === 1 ? "volume" : "volumes"} - {device?.is_online === false && " � Offline"} +

+

+ {deviceName} +

+ {device?.connection_method && ( + + )} +
+

+ {volumes.length}{' '} + {volumes.length === 1 ? 'volume' : 'volumes'} + {device?.is_online === false && ' � Offline'}

{/* Right: Hardware specs */} -
- {manufacturer && formFactor && ( -
-
- {manufacturer} -
-
{formFactor}
-
- )} +
+ {/* CPU Model */} {cpuInfo && ( -
+
+ {device?.cpu_model || 'CPU'} +
+ )} + + {/* Stats row */} +
+ {device?.cpu_cores_physical && (
- {device?.cpu_model || "CPU"} + + + {Math.max(device.cpu_cores_physical || 0, device.cpu_cores_logical || 0)} +
-
- {device?.cpu_physical_cores}C /{" "} - {device?.cpu_cores_logical}T + )} + {ramInfo && ( +
+ + {ramInfo}
-
- )} - {ramInfo && ( -
-
- {ramInfo} -
-
RAM
-
- )} + )} +
@@ -379,7 +428,7 @@ function DeviceCard({
{/* Active Jobs Section */} {activeJobs.length > 0 && ( -
+
{activeJobs.map((job) => ( +
{volumes.length > 0 ? ( volumes.map((volume, idx) => (
- +

No volumes

@@ -434,7 +483,7 @@ interface LocationsScrollerProps { function LocationsScroller({ locations, selectedLocationId, - onLocationSelect, + onLocationSelect }: LocationsScrollerProps) { const scrollRef = useRef(null); const [canScrollLeft, setCanScrollLeft] = useState(false); @@ -442,37 +491,37 @@ function LocationsScroller({ const updateScrollState = () => { if (!scrollRef.current) return; - const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current; + const {scrollLeft, scrollWidth, clientWidth} = scrollRef.current; setCanScrollLeft(scrollLeft > 0); setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1); }; useEffect(() => { updateScrollState(); - window.addEventListener("resize", updateScrollState); - return () => window.removeEventListener("resize", updateScrollState); + window.addEventListener('resize', updateScrollState); + return () => window.removeEventListener('resize', updateScrollState); }, [locations]); - const scroll = (direction: "left" | "right") => { + const scroll = (direction: 'left' | 'right') => { if (!scrollRef.current) return; const scrollAmount = 200; scrollRef.current.scrollBy({ - left: direction === "left" ? -scrollAmount : scrollAmount, - behavior: "smooth", + left: direction === 'left' ? -scrollAmount : scrollAmount, + behavior: 'smooth' }); }; return ( -
+
{/* Left fade and button */} {canScrollLeft && ( <> -
-
+
+
scroll("left")} + onClick={() => scroll('left')} />
@@ -482,8 +531,8 @@ function LocationsScroller({
{locations.map((location) => { const isSelected = selectedLocationId === location.id; @@ -497,14 +546,14 @@ function LocationsScroller({ onLocationSelect?.(location); } }} - className="flex flex-col items-center gap-2 p-1 rounded-lg transition-all min-w-[80px] flex-shrink-0" + className="flex min-w-[80px] flex-shrink-0 flex-col items-center gap-2 rounded-lg p-1 transition-all" >
-
+
{location.name} @@ -533,11 +582,11 @@ function LocationsScroller({ {/* Right fade and button */} {canScrollRight && ( <> -
-
+
+
scroll("right")} + onClick={() => scroll('right')} />
@@ -552,23 +601,23 @@ interface VolumeBarProps { index: number; } -function VolumeBar({ volume, index }: VolumeBarProps) { - const trackVolume = useLibraryMutation("volumes.track"); - const indexVolume = useLibraryMutation("volumes.index"); +function VolumeBar({volume, index}: VolumeBarProps) { + const trackVolume = useLibraryMutation('volumes.track'); + const indexVolume = useLibraryMutation('volumes.index'); // Get current device to check if this volume is local - const { data: currentDevice } = useCoreQuery({ - type: "devices.current", - input: null, + const {data: currentDevice} = useCoreQuery({ + type: 'devices.current', + input: null }); const handleTrack = async () => { try { await trackVolume.mutateAsync({ - fingerprint: volume.fingerprint, + fingerprint: volume.fingerprint }); } catch (error) { - console.error("Failed to track volume:", error); + console.error('Failed to track volume:', error); } }; @@ -576,11 +625,11 @@ function VolumeBar({ volume, index }: VolumeBarProps) { try { const result = await indexVolume.mutateAsync({ fingerprint: volume.fingerprint, - scope: "Recursive", + scope: 'Recursive' }); - console.log("Volume indexed:", result.message); + console.log('Volume indexed:', result.message); } catch (error) { - console.error("Failed to index volume:", error); + console.error('Failed to index volume:', error); } }; @@ -601,32 +650,32 @@ function VolumeBar({ volume, index }: VolumeBarProps) { // Convert enum values to strings for safe rendering const fileSystem = volume.file_system - ? typeof volume.file_system === "string" + ? typeof volume.file_system === 'string' ? volume.file_system : (volume.file_system as any)?.Other || JSON.stringify(volume.file_system) - : "Unknown"; + : 'Unknown'; const diskType = volume.disk_type - ? typeof volume.disk_type === "string" + ? typeof volume.disk_type === 'string' ? volume.disk_type : (volume.disk_type as any)?.Other || JSON.stringify(volume.disk_type) - : "Unknown"; + : 'Unknown'; const readSpeed = volume.read_speed_mbps; const iconSrc = getVolumeIcon(volume.volume_type, volume.name); const volumeTypeStr = - typeof volume.volume_type === "string" + typeof volume.volume_type === 'string' ? volume.volume_type : (volume.volume_type as any)?.Other || JSON.stringify(volume.volume_type); return ( {/* Top row: Info */}
@@ -634,17 +683,17 @@ function VolumeBar({ volume, index }: VolumeBarProps) { {volumeTypeStr} {/* Name, actions, and badges */}
-
- +
+ {volume.display_name || volume.name} {!volume.is_online && ( - + Offline )} @@ -652,13 +701,13 @@ function VolumeBar({ volume, index }: VolumeBarProps) { )} {currentDevice && @@ -666,7 +715,7 @@ function VolumeBar({ volume, index }: VolumeBarProps) { )}
{/* Badges under name */} -
- +
+ {fileSystem} - + {getDiskTypeLabel(diskType)} - + {volumeTypeStr} {volume.total_file_count != null && ( - + {volume.total_file_count.toLocaleString()} files )} @@ -700,11 +749,11 @@ function VolumeBar({ volume, index }: VolumeBarProps) {
{/* Capacity info */} -
-
+
+
{formatBytes(totalCapacity)}
-
+
{formatBytes(availableBytes)} free
@@ -712,31 +761,31 @@ function VolumeBar({ volume, index }: VolumeBarProps) { {/* Bottom: Full-width capacity bar with padding */}
-
-
+
+
@@ -745,4 +794,4 @@ function VolumeBar({ volume, index }: VolumeBarProps) {
); -} \ No newline at end of file +}