Replace per-callback DocumentStore::new(db_path) calls with a program-wide
Arc<Store> created once at startup. All module setup functions now accept
Arc<Store> and clone the specific sub-stores they need. Add #[derive(Clone)]
to all store types (each holds only PathBuf or Arc<RwLock<...>>). Also update
theme::spawn_system_theme_watcher to take SettingsStore directly.
on_load_documents, on_load_document_detail, and on_load_podcasts were
running DB queries synchronously on the Slint event loop, causing a
visible frame drop when switching to those screens.
Each callback now sets a loading flag immediately on the UI thread,
spawns a worker thread to do the DB work, then pushes results back via
upgrade_in_event_loop. DocumentItemDisplay (which holds a Rc-based
ModelRc) is bridged across the thread boundary using a new RawItemDisplay
intermediate type that carries plain String + Vec fields.
Extract artifact_presence_for_node, blob_presence_for_node, build_peer_name_lookup,
and peer_display_name into lib::node_status so the logic is shared between GUI and CLI.
Update gui-app/src/document.rs to import from node_status instead of defining locally.
Handle docs show directly in the CLI (bypassing the daemon) to get full store access.
cmd_docs_show prints document header, node presence, properties, blobs with per-node
status rows (● Present / ◐ Ref / ○ Absent / ✗ Removed), and artifacts likewise.
Show per-document, artifact, and blob node presence in a reusable Slint
component, with document-level status under Id in the metadata card.
Add lib::help for Present/Ref/Absent tooltips (info icon on hover) and
compact rows: status dot, node name, and full node id on one line.
After doc-sync populates stubs, these two protocols fetch the actual data:
- artifact_sync (/syn/artifact-sync/1): single round-trip to pull SQLite
artifact values by (document_id, role); uses LWW merge on receipt.
- blob_sync (/syn/blob-sync/1): chunked 4 MiB streaming protocol for raw
ciphertext files; verifies BLAKE3 hash before writing to disk.
Supporting store additions:
- ArtifactStore: list_all_artifact_stubs() across all documents
- BlobStore: read_ciphertext() / store_ciphertext() for wire transfer
- DocumentStore: list_all_blob_refs() to find locally missing blobs
fetch_missing_blobs_from_peer() auto-discovers missing blobs by diffing
all BlobRefs against what is present on disk.
- upsert_property now dispatches on MergeStrategy: Immutable properties
use INSERT OR IGNORE (first-write-wins); all others apply LWW
(only update if incoming modified_at is strictly newer)
- Add artifact_stubs table to track artifacts known to exist on remote
nodes before their values have been fetched
- record_artifact_stub never overwrites an actual local artifact value;
only records/updates stubs, and is LWW among stubs themselves
- upsert_artifact clears any stub for the same (document_id, role) once
the actual value arrives
- list_artifact_stubs filters out roles where a local value is present
- apply_chunk step 4 now calls record_artifact_stub for every
ArtifactRefRecord instead of being a no-op
- Add tests for LWW upsert, stub lifecycle, and stub/artifact interaction
- Full PicturesScreen replacing the stub: animated left sidebar (same
overlay pattern as Notes) listing image documents from the store,
zoomable image viewer with pinch-to-zoom and pan, bottom bar with
prev/next navigation and live zoom-percentage display
- ImageLoadState sealed type distinguishes None / Loaded / Error so a
missing blob shows an error message (BrokenImage icon + message) rather
than the generic "no image selected" placeholder
- NativeLib.getDocumentBlobBytes now returns ByteArray? — catches
SyncException.NotFound at the FFI boundary and returns null, preventing
crashes when a blob hasn't synced yet
- ffi.rs: add From<BlobStoreError> for SyncError with proper variant
matching; make with_blob_store generic over E: Into<SyncError> so blob
store callers no longer need to erase errors through Box<dyn Error>;
strip the "not found" string-match arm from From<Box<dyn Error>> since
BlobStoreError::NotFound is now handled correctly by the typed impl
- PdfLibraryScreen: handle null blob bytes gracefully with a toast
- MainActivity: pass nativeLib to PicturesScreen
Android: Import button opens a modal with 'From local file...' and
'From URL...' options. The URL option accepts a URL in a text field
and calls the new importDocumentFromUrl native binding.
GUI: documents screen gains a URL LineEdit + 'From URL...' button
inline in the header, beside the existing 'From local file...' button.
Rust (lib): expose import_document_from_url via uniffi FFI, wrapping
documents::url_import::import_from_url from commit 86319c80.
Calls documents::url_import::import_from_url from the CLI via a small tokio
runtime. Shows a spinner while fetching. Prints the imported title and document
ID on success. Unsupported file types or HTTP errors surface as CLI errors.
- Extract shared HTTP client builder (webpki TLS) into a new crate::http module
- Update agents::podcast to use the shared http module
- Add documents::url_import::import_from_url (async): fetches a URL, detects
the document type from Content-Type then URL extension, and stores it using
the same blob/property/blobref path as the file-based import functions
- Supports PDF, image (JPEG/PNG/WebP/GIF/BMP/TIFF), and music (MP3)
Adds a per-node timezone setting stored as chrono_tz::Tz in the Settings
struct. Serializes to/from its IANA name string (e.g. "America/New_York").
New CLI commands:
syn settings show - display current settings
syn settings timezone <TZ> - set IANA timezone (validated at parse time)
syn settings timezone - clear the timezone
gui-app enables the 'multimedia' feature on synchronicity_lib, which pulls in
rodio/symphonia and their ALSA dependency. Due to Cargo workspace feature
unification, this caused ALSA to be compiled for all workspace members,
including the CLI and daemon, breaking headless builds.
Fix by removing gui-app from the main workspace so its feature requests don't
affect the CLI build. gui-app becomes a standalone workspace root with inlined
dep versions. The just 'gui' recipe now uses the gui-app manifest directly.
symphonia/rodio remain as optional deps in the lib crate (behind the
'multimedia' feature), but are no longer declared in the workspace.
PodcastDetailScreen.kt — fully restructured:
- No longer uses SubAppScreen; builds a custom Surface + Column layout with BackHandler
- Top 1/4 (weight(1f)): podcast image (fills row height, square aspect ratio) on the left; podcast title + truncated description (maxLines = 3) on the right
- Below the info row: an Info icon button that calls the new onInfoSelected callback
- A HorizontalDivider separates the sections
- Bottom 3/4 (weight(3f)): "Episodes (N)" header and episode list filling all remaining space
- Subscribe/Unsubscribe button removed from this screen
PodcastInfoScreen.kt — new screen:
- Uses SubAppScreen (provides back button + title)
- Full podcast description in a verticalScroll column
- Subscribe/Unsubscribe button at the bottom
PodcastScreen.kt — navigation:
- Added PodcastDestination.Info route
- PodcastDetailScreen receives onInfoSelected navigating to the info route
- New composable block handles the info screen
Adds lib/src/agents/pdf.rs and lib/src/agents/pictures.rs with
import_pdf_file and import_image_file, mirroring the existing
import_mp3_file in agents/music.rs.
The 'syn docs add FILE...' command opens the store directly (no daemon
required), detects file type by extension, calls the appropriate import
function, marks each document present on the local node, and reports
per-file success or failure. Valid files are imported even if other
files in the batch fail; exits non-zero only when no files succeed.
docs list now accepts:
-f/--filter TYPE restrict to one document type (pdf, image, etc.)
--sort CRITERIA sort by name (default), date-created, date-modified
-n/--number N|all cap output at N rows (default 100) or show all
The ListDocuments daemon command gains filter/sort/limit fields.
commands.rs uses search_documents for filtering and sorts in-process.
format.rs renders document IDs in bright_black and types in cyan,
and shows 'Showing N of M' when output is truncated.
client.rs now includes the raw response body and a restart hint in
the error message when the daemon returns an unparseable response.
TODO.md: added items for CLI/daemon protocol versioning and for
generating --filter help text from DocumentType at compile time.
Cmd::Data now calls client::ensure_daemon_running before sending the
command, so users don't need to manually start the daemon first.
client::daemon_status() encapsulates the pid-file + socket-exists +
connectivity check that was previously duplicated across
cmd_daemon_status and cmd_daemon_stop in main.rs.
DAEMON_SOCK_FILE and DAEMON_PID_FILE constants are now defined in
syn-daemon::lib and used everywhere, replacing all hard-coded
".daemon.sock" and ".daemon.pid" string literals in syn-daemon
(server, client) and cli-app (client, main, tests).
Two bugs prevented node_document_status from being populated correctly:
- doc_sync (apply_chunk): only the receiving node was being marked
present; now also marks the peer sender. Peer pubkey is parsed from
the target hex in sync_from_peer and threaded through run_client_sync
and apply_chunk.
- ffi + gui-app: locally imported documents never called mark_present
at all. All three Android import FFI functions now call it (via the
new import_document_with_blob helper), and the GUI import thread does
the same via NodeDocumentStore.
Rust FFI (rust/lib/src/ffi.rs): Added sync_documents(node_id_hex) — connects to the peer, drives the sync_from_peer stream to completion, and returns the count of documents received.
Slint UI (rust/gui-app/ui/ensemble_screen.slint): Added sync-pending, sync-ok, sync-error, last-synced fields to EnsembleMemberDisplay; added a "Sync documents" button row in NodeInfo with spinner, result/error text, and "Not synced" / "Last synced: HH:MM:SS" timestamp
display; added sync-node(int) callback.
Slint wiring (app_window.slint, main_screen.slint): Threaded sync-node(int) through all three layers of the component hierarchy.
GUI backend (rust/gui-app/src/ensemble.rs): Populated the four new EnsembleMemberDisplay fields; added on_sync_node callback handler that calls sync_from_peer on a background thread and writes the result (doc count + timestamp, or error) back to the model row.
Android (EnsembleScreen.kt): Added SyncState data class; added syncStates map; added "Sync documents" button row with spinner, result/error text, and "Not synced" / "Last synced" display — mirroring the Ping row pattern.
Android (NativeLib.kt): Added syncDocuments(nodeIdHex) wrapper over the uniffi-generated function.
These agents appear in the main screen list but had no matching case in
MainActivity's when block, leaving users on a blank unescapable screen.
Add minimal SubAppScreen stubs for each and wire them up.
Add three new FFI exports to synchronicity_lib: import_image_document,
import_music_track_document, and get_all_document_mime_types. The
MIME-type list comes from the same documents::import constants the GUI
uses, so Android and GUI share the same source of truth for which files
are supported.
On Android: add DocumentFilePicker (OpenMultipleDocuments) and an
import button to DocumentsScreen. File type is detected via
contentResolver.getType and routed to the correct importer. Add
IMPORT_ICON = Icons.Default.FileOpen as an app-wide constant; update
PdfFilePicker to use it and rename its button text to 'Import PDF'.
- Add DocumentArtifactItem and DocumentBlobRefItem FFI record types with
list_document_artifacts and list_document_blob_refs uniffi exports
- Rework DocumentDetailScreen to show Properties, Artifacts, Blobs, and
Node Status as separate cards with accurate counts
- Show total document size (blobs + artifacts) in the metadata card
- Wrap document ID and blob hashes in SelectionContainer so they are
selectable/copyable; display full node ID instead of truncated form
- Fix size_bytes = 0 for documents imported before size tracking: add
BlobStore::plaintext_size_of (ciphertext size - 16 byte AEAD tag) and
DocumentStore::backfill_blob_ref_sizes, called from initialize_database
- Add size_bytes INTEGER NOT NULL to the blob_refs CREATE TABLE so fresh
databases enforce the constraint without a default value