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:
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",