app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 20028a38415acd04e83cd55cb2cee12f8dd89142
parent 9f5939a42dcfea7c854b3efd48c491f9f3044ae2
Author: triesap <tyson@radroots.org>
Date:   Mon, 20 Apr 2026 21:03:29 +0000

ui: replace about placeholder with runtime sync status

Diffstat:
Mcrates/launchers/desktop/src/app.rs | 6+++++-
Mcrates/launchers/desktop/src/runtime.rs | 157++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/launchers/desktop/src/source_guards.rs | 23+++++++++++++++++++++++
Mcrates/launchers/desktop/src/window.rs | 350+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/shared/i18n/src/keys.rs | 18+++++++++++++-----
Mcrates/shared/i18n/src/lib.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Mcrates/shared/ui/src/lib.rs | 2+-
Mcrates/shared/ui/src/text.rs | 31++-----------------------------
Mi18n/locales/en/messages.json | 18+++++++++++++-----
9 files changed, 566 insertions(+), 83 deletions(-)

diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs @@ -30,7 +30,10 @@ pub fn launch() -> Result<(), AppLaunchError> { bootstrap_logging(&snapshot, runtime_config.local_log_root.as_path())?; install_panic_hook(); - let runtime = DesktopAppRuntime::bootstrap(runtime_config.default_nostr_relay_url.clone()); + let runtime = DesktopAppRuntime::bootstrap( + runtime_config.default_nostr_relay_url.clone(), + snapshot.clone(), + ); let runtime_summary = runtime.summary(); emit_runtime_events(&snapshot, &runtime_summary); let launch_target = primary_window_target(&runtime_summary); @@ -275,6 +278,7 @@ mod tests { products_projection: Default::default(), orders_projection: Default::default(), pack_day_projection: Default::default(), + runtime_metadata: crate::runtime::DesktopAppRuntimeMetadataSummary::default(), logged_out_startup: LoggedOutStartupProjection::default(), sync_status: crate::runtime::DesktopAppSyncStatusSummary::default(), startup_issue: startup_issue.map(str::to_owned), diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -1,8 +1,12 @@ use std::collections::BTreeSet; use std::fmt; +use std::path::PathBuf; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; -use radroots_app_core::{AppDesktopRuntimePaths, AppRuntimePathsError, AppSharedAccountsPaths}; +use radroots_app_core::{ + AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode, + AppRuntimePathsError, AppRuntimeSnapshot, AppSharedAccountsPaths, +}; use radroots_app_models::{ ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate, BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection, @@ -55,10 +59,16 @@ pub struct DesktopAppRuntime { } impl DesktopAppRuntime { - pub fn bootstrap(default_nostr_relay_url: String) -> Self { - let state = match DesktopAppRuntimeState::try_bootstrap(default_nostr_relay_url) { + pub fn bootstrap( + default_nostr_relay_url: String, + runtime_snapshot: AppRuntimeSnapshot, + ) -> Self { + let state = match DesktopAppRuntimeState::try_bootstrap( + default_nostr_relay_url, + runtime_snapshot.clone(), + ) { Ok(state) => state, - Err(error) => DesktopAppRuntimeState::degraded(error), + Err(error) => DesktopAppRuntimeState::degraded_with_snapshot(error, runtime_snapshot), }; Self::from_state(state) @@ -90,6 +100,7 @@ impl DesktopAppRuntime { products_projection: state.state_store.products_projection().clone(), orders_projection: state.state_store.orders_projection().clone(), pack_day_projection: state.state_store.pack_day_projection().clone(), + runtime_metadata: state.runtime_metadata.clone(), sync_status, startup_issue: state.startup_issue.clone(), } @@ -503,6 +514,25 @@ impl DesktopAppRuntime { } } +fn default_runtime_snapshot() -> AppRuntimeSnapshot { + AppRuntimeSnapshot::from_capture( + AppBuildIdentity { + package_name: env!("CARGO_PKG_NAME").to_owned(), + package_version: env!("CARGO_PKG_VERSION").to_owned(), + build_profile: option_env!("PROFILE").unwrap_or("debug").to_owned(), + target_triple: option_env!("TARGET").unwrap_or("unknown-target").to_owned(), + projection_source: "rust".to_owned(), + git_commit: None, + }, + AppRuntimeMode::Development, + AppRuntimeCapture { + host_locale: "en_US.UTF-8".to_owned(), + operating_system: "macos".to_owned(), + run_id: "runtime-summary-test-run".to_owned(), + }, + ) +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct DesktopAppSyncStatusSummary { pub account_id: Option<String>, @@ -516,6 +546,48 @@ impl DesktopAppSyncStatusSummary { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DesktopAppRuntimeMetadataSummary { + pub snapshot: AppRuntimeSnapshot, + pub data_root: Option<PathBuf>, + pub logs_root: Option<PathBuf>, + pub database_path: Option<PathBuf>, + pub database_schema_version: Option<u32>, +} + +impl DesktopAppRuntimeMetadataSummary { + fn available( + snapshot: AppRuntimeSnapshot, + paths: &AppDesktopRuntimePaths, + database_path: PathBuf, + database_schema_version: u32, + ) -> Self { + Self { + snapshot, + data_root: Some(paths.app.data.clone()), + logs_root: Some(paths.app.logs.clone()), + database_path: Some(database_path), + database_schema_version: Some(database_schema_version), + } + } + + fn unavailable(snapshot: AppRuntimeSnapshot) -> Self { + Self { + snapshot, + data_root: None, + logs_root: None, + database_path: None, + database_schema_version: None, + } + } +} + +impl Default for DesktopAppRuntimeMetadataSummary { + fn default() -> Self { + Self::unavailable(default_runtime_snapshot()) + } +} + #[derive(Clone, Debug)] pub struct DesktopAppRuntimeSummary { pub shell_projection: AppShellProjection, @@ -530,6 +602,7 @@ pub struct DesktopAppRuntimeSummary { pub products_projection: ProductsScreenProjection, pub orders_projection: OrdersScreenProjection, pub pack_day_projection: PackDayScreenProjection, + pub runtime_metadata: DesktopAppRuntimeMetadataSummary, pub sync_status: DesktopAppSyncStatusSummary, pub startup_issue: Option<String>, } @@ -567,6 +640,7 @@ struct DesktopAppRuntimeState { remote_signer_paths: Option<DesktopRemoteSignerPaths>, accounts_manager: Option<RadrootsNostrAccountsManager>, sqlite_store: Option<AppSqliteStore>, + runtime_metadata: DesktopAppRuntimeMetadataSummary, selected_account_pending_sync_write_count: usize, startup_issue: Option<String>, } @@ -592,6 +666,7 @@ impl fmt::Debug for DesktopAppRuntimeState { "sqlite_store", &self.sqlite_store.as_ref().map(|_| "available"), ) + .field("runtime_metadata", &self.runtime_metadata) .field( "selected_account_pending_sync_write_count", &self.selected_account_pending_sync_write_count, @@ -604,17 +679,20 @@ impl fmt::Debug for DesktopAppRuntimeState { impl DesktopAppRuntimeState { fn try_bootstrap( default_nostr_relay_url: String, + runtime_snapshot: AppRuntimeSnapshot, ) -> Result<Self, DesktopAppRuntimeBootstrapError> { let paths = AppDesktopRuntimePaths::current_desktop()?; - Self::bootstrap_from_paths(paths, default_nostr_relay_url) + Self::bootstrap_from_paths(paths, default_nostr_relay_url, runtime_snapshot) } fn bootstrap_from_paths( paths: AppDesktopRuntimePaths, default_nostr_relay_url: String, + runtime_snapshot: AppRuntimeSnapshot, ) -> Result<Self, DesktopAppRuntimeBootstrapError> { let database_path = paths.app.data.join(APP_DATABASE_FILE_NAME); let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?; + let database_schema_version = sqlite_store.schema_version()?; let mut state_store = AppStateStore::load(InMemoryAppStateRepository::default())?; let remote_signer_paths = DesktopRemoteSignerPaths::from_runtime_paths(&paths); let accounts_bootstrap = bootstrap_desktop_accounts(&paths.shared_accounts, &sqlite_store)?; @@ -689,12 +767,25 @@ impl DesktopAppRuntimeState { remote_signer_paths: Some(remote_signer_paths), accounts_manager: accounts_bootstrap.accounts_manager, sqlite_store: Some(sqlite_store), + runtime_metadata: DesktopAppRuntimeMetadataSummary::available( + runtime_snapshot, + &paths, + database_path, + database_schema_version, + ), selected_account_pending_sync_write_count: pending_sync_write_count, startup_issue: None, }) } fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self { + Self::degraded_with_snapshot(error, default_runtime_snapshot()) + } + + fn degraded_with_snapshot( + error: DesktopAppRuntimeBootstrapError, + runtime_snapshot: AppRuntimeSnapshot, + ) -> Self { Self { state_store: AppStateStore::in_memory(AppShellProjection::default()), default_nostr_relay_url: String::new(), @@ -702,6 +793,7 @@ impl DesktopAppRuntimeState { remote_signer_paths: None, accounts_manager: None, sqlite_store: None, + runtime_metadata: DesktopAppRuntimeMetadataSummary::unavailable(runtime_snapshot), selected_account_pending_sync_write_count: 0, startup_issue: Some(error.to_string()), } @@ -2577,7 +2669,7 @@ mod tests { use radroots_app_remote_signer::{ RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, }; - use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget}; + use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget, latest_schema_version}; use radroots_app_state::{ AppStateRepositoryError, AppStateStore, AppStateStoreError, HomeRoute, InMemoryAppStateRepository, @@ -2597,8 +2689,8 @@ mod tests { use super::{ APP_DATABASE_FILE_NAME, DesktopAppRuntime, DesktopAppRuntimeActivityContextError, - DesktopAppRuntimeCommandError, DesktopAppRuntimeState, DesktopAppSyncStatusSummary, - DesktopRemoteSignerPaths, + DesktopAppRuntimeCommandError, DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeState, + DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths, }; #[test] @@ -2657,6 +2749,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -2708,6 +2801,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -2834,6 +2928,35 @@ mod tests { } #[test] + fn runtime_summary_surfaces_runtime_metadata_from_bootstrap() { + let (runtime, paths) = bootstrapped_runtime("runtime_metadata"); + let summary = runtime.summary(); + + assert_eq!( + summary.runtime_metadata.snapshot.host.app_name, + radroots_app_core::APP_NAME + ); + assert_eq!( + summary.runtime_metadata.data_root.as_ref(), + Some(&paths.app.data) + ); + assert_eq!( + summary.runtime_metadata.logs_root.as_ref(), + Some(&paths.app.logs) + ); + assert_eq!( + summary.runtime_metadata.database_path.as_ref(), + Some(&paths.app.data.join(APP_DATABASE_FILE_NAME)) + ); + assert_eq!( + summary.runtime_metadata.database_schema_version, + Some(latest_schema_version()) + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] fn clearing_startup_pending_remote_signer_session_is_idempotent_without_record() { let paths = temp_remote_signer_paths("clear_pending_none"); let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState { @@ -2847,6 +2970,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -2875,6 +2999,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -2972,6 +3097,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -3069,6 +3195,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -3119,6 +3246,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -3153,6 +3281,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -3185,6 +3314,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -5388,6 +5518,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }); @@ -5420,6 +5551,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }) @@ -5450,6 +5582,7 @@ mod tests { AppSqliteStore::open(DatabaseTarget::InMemory) .expect("in-memory sqlite store should open"), ), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), selected_account_pending_sync_write_count: 0, startup_issue: None, }), @@ -5465,8 +5598,12 @@ mod tests { fn restart_runtime(paths: AppDesktopRuntimePaths) -> DesktopAppRuntime { DesktopAppRuntime::from_state( - DesktopAppRuntimeState::bootstrap_from_paths(paths, "ws://127.0.0.1:8080".to_owned()) - .expect("runtime bootstrap should succeed"), + DesktopAppRuntimeState::bootstrap_from_paths( + paths, + "ws://127.0.0.1:8080".to_owned(), + super::default_runtime_snapshot(), + ) + .expect("runtime bootstrap should succeed"), ) } diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -28,8 +28,11 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "Salad mix", "USD", "Untitled draft", + "/tmp/radroots/data/apps/app", + "/tmp/radroots/logs/apps/app", "{}.{:02}", "abc", + "app.sqlite3", "account-add", "account-open-workspace", "account-log-out", @@ -488,6 +491,16 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::SettingsReadinessFieldBlackoutPeriodEndsBeforeStart", "AppTextKey::SettingsReadinessFieldBlackoutOverlapsFulfillmentWindow", "AppTextKey::SettingsReadinessReady", + "AppTextKey::SettingsAboutStatusSectionLabel", + "AppTextKey::SettingsAboutConflictReviewSectionLabel", + "AppTextKey::SettingsAboutRuntimeSectionLabel", + "AppTextKey::SettingsAboutConflictReviewUnavailable", + "AppTextKey::SettingsAboutConflictReviewClear", + "AppTextKey::SettingsAboutConflictReviewNeedsAttention", + "AppTextKey::SettingsAboutConflictReviewBlocking", + "AppTextKey::MetadataSelectedAccount", + "AppTextKey::MetadataSyncPendingWriteCount", + "AppTextKey::MetadataSyncBlockingConflictCount", ]; const FORBIDDEN_LAUNCHER_UI_BYPASS_PATTERNS: &[(&str, &str)] = &[ @@ -587,6 +600,16 @@ fn desktop_window_source_does_not_reintroduce_removed_ui_helper_families() { } } +#[test] +fn desktop_window_source_does_not_use_about_placeholder_copy() { + let source = include_str!("window.rs"); + + assert!( + !source.contains("SettingsAboutPlaceholder"), + "window.rs still references retired about placeholder copy" + ); +} + fn extract_string_literals(source: &str) -> BTreeSet<&str> { let mut literals = BTreeSet::new(); let bytes = source.as_bytes(); diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -8,7 +8,7 @@ use gpui_component::{ IconName, Root, input::{InputEvent, InputState}, }; -use radroots_app_i18n::AppTextKey; +use radroots_app_i18n::{AppTextKey, app_text}; pub use radroots_app_models::SettingsSection as SettingsPanelViewKey; use radroots_app_models::{ AppStartupGate, BlackoutPeriodId, BlackoutPeriodRecord, BuyerCartProjection, @@ -35,6 +35,7 @@ use radroots_app_sqlite::derive_farm_rules_readiness; use radroots_app_state::{ FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, derive_product_publish_blockers, }; +use radroots_app_sync::{AppSyncRunStatus, SyncCheckpointState}; use radroots_app_ui::{ APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, AppSegmentButtonIconSpec as IconSegmentButtonSpec, LabelValueRow, app_button_card, @@ -52,13 +53,13 @@ use radroots_app_ui::{ app_surface_sidebar, app_surface_window as app_window_shell, app_text_badge as settings_badge_text, app_text_body_subtle as home_body_text, app_text_label, app_text_label as home_farm_setup_field_label, app_text_value, label_value_list, - utility_title_row, + runtime_metadata_rows, utility_title_row, }; use radroots_nostr::prelude::RadrootsNostrClient; -use std::{collections::BTreeSet, sync::Arc, time::Duration}; +use std::{collections::BTreeSet, path::PathBuf, sync::Arc, time::Duration}; use tracing::error; -use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary}; +use crate::runtime::{DesktopAppRuntime, DesktopAppRuntimeSummary, DesktopAppSyncStatusSummary}; const HOME_WINDOW_MIN_WIDTH_PX: f32 = 1080.0; const HOME_WINDOW_MIN_HEIGHT_PX: f32 = 720.0; @@ -5491,6 +5492,11 @@ impl SettingsWindowView { } fn about_panel(&self) -> impl IntoElement { + let runtime = self.runtime.summary(); + let status_rows = about_status_rows(&runtime); + let conflict_rows = about_conflict_review_rows(&runtime.sync_status); + let runtime_rows = about_runtime_rows(&runtime); + app_scroll_panel( "settings-panel-scroll", APP_UI_THEME.shells.settings_content_padding_px, @@ -5498,28 +5504,33 @@ impl SettingsWindowView { app_stack_v(APP_UI_THEME.shells.settings_account_main_stack_gap_px) .size_full() .py_12() - .child( - app_stack_v(APP_UI_THEME.shells.settings_account_main_stack_gap_px) + .child(app_surface_card( + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) .w_full() - .justify_between() - .child(home_body_text(app_shared_text( - AppTextKey::SettingsAboutPlaceholderTopPrimary, + .child(app_heading_section(app_shared_text( + AppTextKey::SettingsAboutStatusSectionLabel, + ))) + .child(label_value_list(status_rows)), + )) + .child(app_surface_card( + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(app_heading_section(app_shared_text( + AppTextKey::SettingsAboutConflictReviewSectionLabel, ))) - .child(home_body_text(app_shared_text( - AppTextKey::SettingsAboutPlaceholderTopSecondary, + .child(home_body_text(app_text(about_conflict_review_body_key( + &runtime.sync_status, + )))) + .child(label_value_list(conflict_rows)), + )) + .child(app_surface_card( + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(app_heading_section(app_shared_text( + AppTextKey::SettingsAboutRuntimeSectionLabel, ))) - .child(home_body_text(app_shared_text( - AppTextKey::SettingsAboutPlaceholderTopTertiary, - ))), - ) - .child(section_divider()) - .child(div().w_full().py_12().child(home_body_text(app_shared_text( - AppTextKey::SettingsAboutPlaceholderMiddle, - )))) - .child(section_divider()) - .child(div().w_full().py_12().child(home_body_text(app_shared_text( - AppTextKey::SettingsAboutPlaceholderBottom, - )))), + .child(label_value_list(runtime_rows)), + )), ) } @@ -5537,6 +5548,175 @@ impl SettingsWindowView { } } +fn about_status_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> { + let mut rows = vec![ + LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSelectedAccount), + selected_account_label(runtime.sync_status.account_id.as_deref()), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncRunStatus), + about_sync_run_status_text(&runtime.sync_status), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncCheckpointState), + about_sync_checkpoint_state_text(&runtime.sync_status), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncPendingWriteCount), + runtime.sync_status.pending_write_count.to_string(), + ), + LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncConflictCount), + runtime + .sync_status + .projection + .conflict_status + .unresolved_count + .to_string(), + ), + ]; + + if runtime + .sync_status + .projection + .conflict_status + .blocking_count + > 0 + { + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncBlockingConflictCount), + runtime + .sync_status + .projection + .conflict_status + .blocking_count + .to_string(), + )); + } + + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataStartupIssue), + runtime + .startup_issue + .clone() + .unwrap_or_else(|| app_text(AppTextKey::ValueNone)), + )); + + rows +} + +fn about_conflict_review_body_key(sync_status: &DesktopAppSyncStatusSummary) -> AppTextKey { + if !sync_status.is_enabled() { + AppTextKey::SettingsAboutConflictReviewUnavailable + } else if sync_status + .projection + .conflict_status + .has_blocking_conflicts() + { + AppTextKey::SettingsAboutConflictReviewBlocking + } else if sync_status.projection.conflict_status.requires_attention() { + AppTextKey::SettingsAboutConflictReviewNeedsAttention + } else { + AppTextKey::SettingsAboutConflictReviewClear + } +} + +fn about_conflict_review_rows(sync_status: &DesktopAppSyncStatusSummary) -> Vec<LabelValueRow> { + let mut rows = vec![LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncConflictCount), + sync_status + .projection + .conflict_status + .unresolved_count + .to_string(), + )]; + + if sync_status.projection.conflict_status.blocking_count > 0 { + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataSyncBlockingConflictCount), + sync_status + .projection + .conflict_status + .blocking_count + .to_string(), + )); + } + + rows +} + +fn about_runtime_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> { + let mut rows = runtime_metadata_rows(&runtime.runtime_metadata.snapshot); + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataDataRoot), + path_or_none(runtime.runtime_metadata.data_root.as_ref()), + )); + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataLogsRoot), + path_or_none(runtime.runtime_metadata.logs_root.as_ref()), + )); + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataDatabasePath), + path_or_none(runtime.runtime_metadata.database_path.as_ref()), + )); + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataDatabaseSchemaVersion), + runtime + .runtime_metadata + .database_schema_version + .map(|version| version.to_string()) + .unwrap_or_else(|| app_text(AppTextKey::ValueNone)), + )); + rows.push(LabelValueRow::new( + app_shared_text(AppTextKey::MetadataShellSection), + runtime + .shell_projection + .selected_section + .storage_key() + .to_owned(), + )); + rows +} + +fn selected_account_label(account_id: Option<&str>) -> String { + account_id + .map(ToOwned::to_owned) + .unwrap_or_else(|| app_text(AppTextKey::ValueNone)) +} + +fn about_sync_run_status_text(sync_status: &DesktopAppSyncStatusSummary) -> String { + if !sync_status.is_enabled() { + return app_text(AppTextKey::ValueDisabled); + } + + match sync_status.projection.run_status { + AppSyncRunStatus::Idle => app_text(AppTextKey::ValueSyncRunStatusIdle), + AppSyncRunStatus::Syncing => app_text(AppTextKey::ValueSyncRunStatusSyncing), + AppSyncRunStatus::Succeeded => app_text(AppTextKey::ValueSyncRunStatusSucceeded), + AppSyncRunStatus::Conflicted => app_text(AppTextKey::ValueSyncRunStatusConflicted), + AppSyncRunStatus::Failed => app_text(AppTextKey::ValueSyncRunStatusFailed), + } +} + +fn about_sync_checkpoint_state_text(sync_status: &DesktopAppSyncStatusSummary) -> String { + if !sync_status.is_enabled() { + return app_text(AppTextKey::ValueNone); + } + + match sync_status.projection.checkpoint.state { + SyncCheckpointState::NeverSynced => app_text(AppTextKey::ValueSyncCheckpointNeverSynced), + SyncCheckpointState::Syncing => app_text(AppTextKey::ValueSyncCheckpointSyncing), + SyncCheckpointState::Current => app_text(AppTextKey::ValueSyncCheckpointCurrent), + SyncCheckpointState::Failed => app_text(AppTextKey::ValueSyncCheckpointFailed), + } +} + +fn path_or_none(path: Option<&PathBuf>) -> String { + path.map(|value| value.display().to_string()) + .unwrap_or_else(|| app_text(AppTextKey::ValueNone)) +} + impl Render for SettingsWindowView { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let navigation_buttons = SETTINGS_NAVIGATION_ORDER @@ -9537,16 +9717,19 @@ mod tests { AppTextKey, FarmerHomeFarmState, HomeStage, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface, - StartupSignerConnectState, buyer_orders_status_key, farm_setup_onboarding_card_spec, - farmer_home_farm_state, farmer_pack_day_available, home_content_scroll_id, home_saved_farm, - home_sidebar_navigation_sections, home_stage, home_window_launch_size_px, - home_window_minimum_size_px, parse_optional_product_editor_stock_input, - parse_product_editor_price_input, product_display_title, startup_home_surface, - startup_signer_preview_summary, startup_signer_preview_summary_for_connect_state, - startup_signer_source_input_is_editable, startup_signer_status_spec, - startup_signer_transport_failure_requires_notice, + StartupSignerConnectState, about_conflict_review_body_key, about_conflict_review_rows, + about_runtime_rows, about_status_rows, app_text, buyer_orders_status_key, + farm_setup_onboarding_card_spec, farmer_home_farm_state, farmer_pack_day_available, + home_content_scroll_id, home_saved_farm, home_sidebar_navigation_sections, home_stage, + home_window_launch_size_px, home_window_minimum_size_px, + parse_optional_product_editor_stock_input, parse_product_editor_price_input, + product_display_title, startup_home_surface, startup_signer_preview_summary, + startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable, + startup_signer_status_spec, startup_signer_transport_failure_requires_notice, + }; + use crate::runtime::{ + DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeSummary, DesktopAppSyncStatusSummary, }; - use crate::runtime::DesktopAppRuntimeSummary; use radroots_app_models::SettingsAccountProjection; use radroots_app_models::{ ActiveSurface, AppStartupGate, BuyerOrderStatus, FarmId, FarmOrderMethod, FarmReadiness, @@ -9562,7 +9745,11 @@ mod tests { use radroots_app_state::{ AppShellProjection, FarmWorkspaceReadinessProjection, FarmWorkspaceStatus, HomeRoute, }; + use radroots_app_sync::{ + AppSyncProjection, AppSyncRunStatus, SyncCheckpointStatus, SyncConflictStatus, + }; use radroots_identity::RadrootsIdentity; + use std::path::PathBuf; #[test] fn farm_setup_onboarding_uses_frozen_copy_and_primary_action() { @@ -10054,6 +10241,104 @@ mod tests { )); } + #[test] + fn about_status_rows_disable_sync_without_a_selected_account() { + let rows = about_status_rows(&summary( + HomeRoute::SetupRequired, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + )); + + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataSelectedAccount) + && row.value == app_text(AppTextKey::ValueNone) + })); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataSyncRunStatus) + && row.value == app_text(AppTextKey::ValueDisabled) + })); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataSyncCheckpointState) + && row.value == app_text(AppTextKey::ValueNone) + })); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataStartupIssue) + && row.value == app_text(AppTextKey::ValueNone) + })); + } + + #[test] + fn about_conflict_review_helpers_surface_blocking_attention_truthfully() { + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + runtime.sync_status = DesktopAppSyncStatusSummary { + account_id: Some(app_text(AppTextKey::AppName)), + projection: AppSyncProjection { + run_status: AppSyncRunStatus::Conflicted, + checkpoint: SyncCheckpointStatus::never_synced(), + conflict_status: SyncConflictStatus { + unresolved_count: 2, + blocking_count: 1, + }, + }, + pending_write_count: 3, + }; + + let rows = about_conflict_review_rows(&runtime.sync_status); + + assert_eq!( + about_conflict_review_body_key(&runtime.sync_status), + AppTextKey::SettingsAboutConflictReviewBlocking + ); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataSyncConflictCount) + && row.value == 2.to_string() + })); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataSyncBlockingConflictCount) + && row.value == 1.to_string() + })); + } + + #[test] + fn about_runtime_rows_append_paths_schema_and_shell_section() { + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + let data_root = PathBuf::from("/tmp/radroots/data/apps/app"); + let logs_root = PathBuf::from("/tmp/radroots/logs/apps/app"); + let database_path = data_root.join("app.sqlite3"); + runtime.shell_projection.selected_section = + ShellSection::Settings(SettingsPanelViewKey::About); + runtime.runtime_metadata = DesktopAppRuntimeMetadataSummary { + data_root: Some(data_root.clone()), + logs_root: Some(logs_root), + database_path: Some(database_path), + database_schema_version: Some(7), + ..DesktopAppRuntimeMetadataSummary::default() + }; + + let rows = about_runtime_rows(&runtime); + + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataDataRoot) + && row.value == data_root.display().to_string() + })); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataDatabaseSchemaVersion) + && row.value == 7.to_string() + })); + assert!(rows.iter().any(|row| { + row.label == app_text(AppTextKey::MetadataShellSection) + && row.value == ShellSection::Settings(SettingsPanelViewKey::About).storage_key() + })); + } + fn summary( home_route: HomeRoute, today_projection: TodayAgendaProjection, @@ -10091,6 +10376,7 @@ mod tests { products_projection: Default::default(), orders_projection: Default::default(), pack_day_projection: Default::default(), + runtime_metadata: DesktopAppRuntimeMetadataSummary::default(), sync_status: crate::runtime::DesktopAppSyncStatusSummary::default(), startup_issue: None, } diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -352,11 +352,16 @@ define_app_text_keys! { SettingsGeneralLaunchAtLogin => "settings.general.launch_at_login", SettingsGeneralManageAction => "settings.general.action.manage", SettingsGeneralUseNip05Note => "settings.general.use_nip05.note", - SettingsAboutPlaceholderTopPrimary => "settings.about.placeholder.top_primary", - SettingsAboutPlaceholderTopSecondary => "settings.about.placeholder.top_secondary", - SettingsAboutPlaceholderTopTertiary => "settings.about.placeholder.top_tertiary", - SettingsAboutPlaceholderMiddle => "settings.about.placeholder.middle", - SettingsAboutPlaceholderBottom => "settings.about.placeholder.bottom", + SettingsAboutStatusSectionLabel => "settings.about.status.section.label", + SettingsAboutConflictReviewSectionLabel => "settings.about.conflict_review.section.label", + SettingsAboutRuntimeSectionLabel => "settings.about.runtime.section.label", + SettingsAboutConflictReviewUnavailable => "settings.about.conflict_review.unavailable", + SettingsAboutConflictReviewClear => "settings.about.conflict_review.clear", + SettingsAboutConflictReviewNeedsAttention => "settings.about.conflict_review.needs_attention", + SettingsAboutConflictReviewBlocking => "settings.about.conflict_review.blocking", + SettingsAboutConflictAcceptLocalAction => "settings.about.conflict_review.action.accept_local", + SettingsAboutConflictAcceptRemoteAction => "settings.about.conflict_review.action.accept_remote", + SettingsAboutConflictDismissAction => "settings.about.conflict_review.action.dismiss", MetadataCorePackage => "metadata.core_package", MetadataCoreVersion => "metadata.core_version", MetadataCoreAuthors => "metadata.core_authors", @@ -381,9 +386,12 @@ define_app_text_keys! { MetadataDatabasePath => "metadata.database_path", MetadataDatabaseSchemaVersion => "metadata.database_schema_version", MetadataShellSection => "metadata.shell_section", + MetadataSelectedAccount => "metadata.selected_account", MetadataSyncRunStatus => "metadata.sync_run_status", MetadataSyncCheckpointState => "metadata.sync_checkpoint_state", + MetadataSyncPendingWriteCount => "metadata.sync_pending_write_count", MetadataSyncConflictCount => "metadata.sync_conflict_count", + MetadataSyncBlockingConflictCount => "metadata.sync_blocking_conflict_count", MetadataStartupIssue => "metadata.startup_issue", ValueNone => "value.none", ValueEnabled => "value.enabled", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -168,6 +168,50 @@ mod tests { } #[test] + fn english_about_copy_matches_the_runtime_status_contract() { + assert_eq!( + app_text(AppTextKey::SettingsAboutStatusSectionLabel), + "Status" + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutConflictReviewSectionLabel), + "Conflict review" + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutRuntimeSectionLabel), + "Runtime" + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutConflictReviewUnavailable), + "Conflict review becomes available after you select an account." + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutConflictReviewBlocking), + "Blocking conflicts pause sync until you resolve them." + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutConflictAcceptLocalAction), + "Accept local" + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutConflictAcceptRemoteAction), + "Accept remote" + ); + assert_eq!( + app_text(AppTextKey::SettingsAboutConflictDismissAction), + "Dismiss" + ); + assert_eq!( + app_text(AppTextKey::MetadataSyncPendingWriteCount), + "pending writes" + ); + assert_eq!( + app_text(AppTextKey::MetadataSyncBlockingConflictCount), + "blocking conflict count" + ); + } + + #[test] fn english_orders_copy_matches_the_queue_contract() { assert_eq!(app_text(AppTextKey::HomeNavOrders), "Orders"); assert_eq!( diff --git a/crates/shared/ui/src/lib.rs b/crates/shared/ui/src/lib.rs @@ -16,7 +16,7 @@ pub use primitives::{ app_text_label, app_text_value, label_value_list, utility_title_row, }; pub use text::{ - app_shared_label_text, app_shared_text, runtime_metadata_rows, settings_about_status_rows, + app_shared_label_text, app_shared_text, runtime_metadata_rows, settings_preferences_general_rows, }; pub use theme::{ diff --git a/crates/shared/ui/src/text.rs b/crates/shared/ui/src/text.rs @@ -112,23 +112,6 @@ pub fn settings_preferences_general_rows() -> Vec<LabelValueRow> { ] } -pub fn settings_about_status_rows() -> Vec<LabelValueRow> { - vec![ - text_row( - AppTextKey::SettingsViewAbout, - AppTextKey::SettingsAboutPlaceholderTopPrimary, - ), - text_row( - AppTextKey::SettingsGeneralSectionLabel, - AppTextKey::SettingsAboutPlaceholderMiddle, - ), - text_row( - AppTextKey::SettingsAccountProfileLabel, - AppTextKey::SettingsAboutPlaceholderBottom, - ), - ] -} - fn metadata_row(label: AppTextKey, value: impl Into<String>) -> LabelValueRow { LabelValueRow::new(app_shared_text(label), value.into()) } @@ -152,9 +135,7 @@ mod tests { }; use radroots_app_i18n::{AppTextKey, app_text}; - use super::{ - runtime_metadata_rows, settings_about_status_rows, settings_preferences_general_rows, - }; + use super::{runtime_metadata_rows, settings_preferences_general_rows}; #[test] fn runtime_metadata_rows_use_localized_labels() { @@ -188,24 +169,16 @@ mod tests { } #[test] - fn settings_placeholder_rows_use_localized_copy() { + fn settings_preferences_rows_use_localized_copy() { let general_rows = settings_preferences_general_rows(); - let about_rows = settings_about_status_rows(); let allow_relay_label = app_text(AppTextKey::SettingsGeneralAllowRelayConnections); let enabled_value = app_text(AppTextKey::ValueEnabled); - let about_label = app_text(AppTextKey::SettingsViewAbout); - let about_primary = app_text(AppTextKey::SettingsAboutPlaceholderTopPrimary); assert!( general_rows .iter() .any(|row| row.label == allow_relay_label && row.value == enabled_value) ); - assert!( - about_rows - .iter() - .any(|row| row.label == about_label && row.value == about_primary) - ); } } diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -331,11 +331,16 @@ "settings.general.launch_at_login": "Launch Radroots at login", "settings.general.action.manage": "Manage...", "settings.general.use_nip05.note": "A kind-0 metadata event will be posted including your NIP-05 profile name and a request will be sent to radroots over NIP-96 to reserve your configured username", - "settings.about.placeholder.top_primary": "About placeholder primary", - "settings.about.placeholder.top_secondary": "About placeholder secondary", - "settings.about.placeholder.top_tertiary": "About placeholder tertiary", - "settings.about.placeholder.middle": "About placeholder middle section", - "settings.about.placeholder.bottom": "About placeholder bottom section", + "settings.about.status.section.label": "Status", + "settings.about.conflict_review.section.label": "Conflict review", + "settings.about.runtime.section.label": "Runtime", + "settings.about.conflict_review.unavailable": "Conflict review becomes available after you select an account.", + "settings.about.conflict_review.clear": "No conflicts need review right now.", + "settings.about.conflict_review.needs_attention": "Conflicts need review before you trust the remote state.", + "settings.about.conflict_review.blocking": "Blocking conflicts pause sync until you resolve them.", + "settings.about.conflict_review.action.accept_local": "Accept local", + "settings.about.conflict_review.action.accept_remote": "Accept remote", + "settings.about.conflict_review.action.dismiss": "Dismiss", "metadata.core_package": "core package", "metadata.core_version": "core version", "metadata.core_authors": "core authors", @@ -360,9 +365,12 @@ "metadata.database_path": "database path", "metadata.database_schema_version": "database schema version", "metadata.shell_section": "shell section", + "metadata.selected_account": "selected account", "metadata.sync_run_status": "sync run status", "metadata.sync_checkpoint_state": "sync checkpoint state", + "metadata.sync_pending_write_count": "pending writes", "metadata.sync_conflict_count": "sync conflict count", + "metadata.sync_blocking_conflict_count": "blocking conflict count", "metadata.startup_issue": "startup issue", "value.none": "none", "value.enabled": "enabled",