commit 8df4791632b9a206d2a28fa4fc6d1bd718c68b01
parent e7324be536ff98cd4c7022c74d506f1cc962ad87
Author: triesap <tyson@radroots.org>
Date: Thu, 18 Jun 2026 14:29:21 -0700
app: surface sdk diagnostics in about status
- add lightweight SDK lifecycle status to the desktop runtime summary
- expose app-owned SDK diagnostics rows for ready, degraded, and lifecycle-busy states
- localize SDK status, integrity, relay, issue, and recovery labels
- cover ready, degraded, busy, and runtime path presentation with focused tests
Diffstat:
5 files changed, 805 insertions(+), 20 deletions(-)
diff --git a/crates/desktop/src/app.rs b/crates/desktop/src/app.rs
@@ -269,6 +269,7 @@ mod tests {
logged_out_startup: LoggedOutStartupProjection::default(),
sync_status: crate::runtime::DesktopAppSyncStatusSummary::default(),
startup_issue: startup_issue.map(str::to_owned),
+ sdk_status: None,
}
}
diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs
@@ -8,9 +8,10 @@ use std::time::{Duration as StdDuration, SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Duration, Utc};
use radroots_app_core::{
AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode,
- AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics, AppSdkRuntime,
- AppSdkRuntimeError, AppSdkRuntimeStatus, AppSharedAccountsPaths, PackDayExportWriteError,
- prepare_pack_day_export_bundle_at_data_root,
+ AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics,
+ AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy, AppSdkRuntime,
+ AppSdkRuntimeError, AppSdkRuntimeIssue, AppSdkRuntimeStatus, AppSdkStoragePaths,
+ AppSharedAccountsPaths, PackDayExportWriteError, prepare_pack_day_export_bundle_at_data_root,
shared_local_events_database_path_from_shared_accounts, write_prepared_pack_day_export_bundle,
};
use radroots_app_remote_signer::{
@@ -533,6 +534,7 @@ impl DesktopAppRuntime {
}
pub fn summary(&self) -> DesktopAppRuntimeSummary {
+ let sdk_status = self.sdk_status_summary();
let state = self.lock_state();
let sync_status = DesktopAppSyncStatusSummary {
account_id: state
@@ -564,6 +566,7 @@ impl DesktopAppRuntime {
runtime_metadata: state.runtime_metadata.clone(),
sync_status,
startup_issue: state.startup_issue.clone(),
+ sdk_status,
}
}
@@ -579,6 +582,12 @@ impl DesktopAppRuntime {
.map(AppSdkRuntime::status)
}
+ pub fn sdk_status_summary(&self) -> Option<DesktopAppSdkStatusSummary> {
+ self.sdk_status()
+ .as_ref()
+ .map(DesktopAppSdkStatusSummary::from_status)
+ }
+
pub fn wait_for_sdk_startup(&self, timeout: StdDuration) -> Option<AppSdkRuntimeStatus> {
self.sdk_runtime
.lock()
@@ -610,6 +619,32 @@ impl DesktopAppRuntime {
runtime.diagnostics().map(Some)
}
+ pub fn sdk_diagnostics_summary(&self) -> Option<DesktopAppSdkDiagnosticsSummary> {
+ let sdk_runtime = self
+ .sdk_runtime
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
+ let runtime = sdk_runtime.as_ref()?;
+ let status = runtime.status();
+ match runtime.diagnostics() {
+ Ok(diagnostics) => Some(DesktopAppSdkDiagnosticsSummary {
+ status: DesktopAppSdkStatusSummary::from_status(&diagnostics.runtime),
+ state: DesktopAppSdkDiagnosticsState::Ready(
+ DesktopAppSdkReadyDiagnosticsSummary::from_diagnostics(&diagnostics),
+ ),
+ }),
+ Err(error) => {
+ let issue = desktop_app_sdk_issue_from_runtime_error(&error);
+ let mut status = DesktopAppSdkStatusSummary::from_status(&status);
+ status.last_issue = Some(issue.clone());
+ Some(DesktopAppSdkDiagnosticsSummary {
+ status,
+ state: DesktopAppSdkDiagnosticsState::Blocked(issue),
+ })
+ }
+ }
+ }
+
pub fn selected_settings_section(&self) -> SettingsSection {
self.lock_state()
.state_store
@@ -1284,6 +1319,52 @@ fn start_desktop_sdk_runtime(
AppSdkRuntime::start(AppSdkConfig::from_desktop_paths(paths, nostr_relay_urls))
}
+fn sdk_storage_path_pair(paths: Option<&AppSdkStoragePaths>) -> (Option<PathBuf>, Option<PathBuf>) {
+ paths
+ .map(|paths| {
+ (
+ Some(paths.event_store_path.clone()),
+ Some(paths.outbox_path.clone()),
+ )
+ })
+ .unwrap_or((None, None))
+}
+
+fn desktop_app_sdk_issue_from_runtime_error(
+ error: &AppSdkRuntimeError,
+) -> DesktopAppSdkIssueSummary {
+ match error {
+ AppSdkRuntimeError::CommandFailed(issue) => DesktopAppSdkIssueSummary::from_issue(issue),
+ AppSdkRuntimeError::CommandQueueCapacityZero => DesktopAppSdkIssueSummary::runtime(
+ "sdk_command_queue_capacity_zero",
+ false,
+ ["review_runtime_configuration"],
+ ),
+ AppSdkRuntimeError::WorkerSpawn(_) => {
+ DesktopAppSdkIssueSummary::runtime("sdk_worker_spawn_failed", true, ["retry_startup"])
+ }
+ AppSdkRuntimeError::CommandQueueFull => DesktopAppSdkIssueSummary::runtime(
+ "sdk_command_queue_full",
+ true,
+ ["retry_status_refresh"],
+ ),
+ AppSdkRuntimeError::CommandQueueClosed => {
+ DesktopAppSdkIssueSummary::runtime("sdk_command_queue_closed", true, ["retry_startup"])
+ }
+ AppSdkRuntimeError::CommandResponseClosed => DesktopAppSdkIssueSummary::runtime(
+ "sdk_command_response_closed",
+ true,
+ ["retry_status_refresh"],
+ ),
+ AppSdkRuntimeError::ShutdownAck => {
+ DesktopAppSdkIssueSummary::runtime("sdk_shutdown_ack_failed", true, ["retry_startup"])
+ }
+ AppSdkRuntimeError::WorkerJoin => {
+ DesktopAppSdkIssueSummary::runtime("sdk_worker_join_failed", true, ["retry_startup"])
+ }
+ }
+}
+
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DesktopAppSyncStatusSummary {
pub account_id: Option<String>,
@@ -1305,6 +1386,118 @@ pub struct DesktopAppSyncConflictSummary {
}
#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DesktopAppSdkStatusSummary {
+ pub lifecycle_state: AppSdkLifecycleState,
+ pub projection_lifecycle_state: AppSdkProjectionLifecycleState,
+ pub projection_lifecycle_reason: Option<String>,
+ pub storage_root: PathBuf,
+ pub event_store_path: Option<PathBuf>,
+ pub outbox_path: Option<PathBuf>,
+ pub relay_target_count: usize,
+ pub relay_url_policy: AppSdkRelayUrlPolicy,
+ pub last_issue: Option<DesktopAppSdkIssueSummary>,
+}
+
+impl DesktopAppSdkStatusSummary {
+ fn from_status(status: &AppSdkRuntimeStatus) -> Self {
+ let (event_store_path, outbox_path) = sdk_storage_path_pair(status.storage_paths.as_ref());
+ Self {
+ lifecycle_state: status.state,
+ projection_lifecycle_state: status.projection_lifecycle.state,
+ projection_lifecycle_reason: status.projection_lifecycle.reason.clone(),
+ storage_root: status.storage_root.clone(),
+ event_store_path,
+ outbox_path,
+ relay_target_count: status.relay_urls.len(),
+ relay_url_policy: status.relay_url_policy,
+ last_issue: status
+ .last_issue
+ .as_ref()
+ .map(DesktopAppSdkIssueSummary::from_issue),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DesktopAppSdkDiagnosticsSummary {
+ pub status: DesktopAppSdkStatusSummary,
+ pub state: DesktopAppSdkDiagnosticsState,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DesktopAppSdkDiagnosticsState {
+ Ready(DesktopAppSdkReadyDiagnosticsSummary),
+ Blocked(DesktopAppSdkIssueSummary),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DesktopAppSdkReadyDiagnosticsSummary {
+ pub storage_kind: String,
+ pub event_store_total_events: i64,
+ pub outbox_total_events: i64,
+ pub outbox_pending_events: i64,
+ pub outbox_failed_terminal_events: i64,
+ pub integrity_event_store_ok: bool,
+ pub integrity_outbox_ok: bool,
+ pub sync_source: String,
+ pub sync_observed_at_ms: i64,
+ pub sync_relay_target_count: usize,
+}
+
+impl DesktopAppSdkReadyDiagnosticsSummary {
+ fn from_diagnostics(diagnostics: &AppSdkDiagnostics) -> Self {
+ Self {
+ storage_kind: diagnostics.storage.storage_kind.clone(),
+ event_store_total_events: diagnostics.storage.event_store.total_events,
+ outbox_total_events: diagnostics.storage.outbox.total_events,
+ outbox_pending_events: diagnostics.storage.outbox.pending_events,
+ outbox_failed_terminal_events: diagnostics.storage.outbox.failed_terminal_events,
+ integrity_event_store_ok: diagnostics.integrity.event_store_ok,
+ integrity_outbox_ok: diagnostics.integrity.outbox_ok,
+ sync_source: diagnostics.sync.source.clone(),
+ sync_observed_at_ms: diagnostics.sync.observed_at_ms,
+ sync_relay_target_count: diagnostics.sync.relay_targets.configured_count,
+ }
+ }
+
+ pub const fn integrity_ok(&self) -> bool {
+ self.integrity_event_store_ok && self.integrity_outbox_ok
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DesktopAppSdkIssueSummary {
+ pub code: String,
+ pub class: String,
+ pub retryable: bool,
+ pub recovery_actions: Vec<String>,
+}
+
+impl DesktopAppSdkIssueSummary {
+ fn from_issue(issue: &AppSdkRuntimeIssue) -> Self {
+ Self {
+ code: issue.code.clone(),
+ class: issue.class.clone(),
+ retryable: issue.retryable,
+ recovery_actions: issue.recovery_actions.clone(),
+ }
+ }
+
+ fn runtime(
+ code: impl Into<String>,
+ retryable: bool,
+ recovery_actions: impl IntoIterator<Item = &'static str>,
+ ) -> Self {
+ Self {
+ code: code.into(),
+ class: "runtime".to_owned(),
+ retryable,
+ recovery_actions: recovery_actions.into_iter().map(str::to_owned).collect(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DesktopAppRuntimeMetadataSummary {
pub snapshot: AppRuntimeSnapshot,
pub data_root: Option<PathBuf>,
@@ -1365,6 +1558,7 @@ pub struct DesktopAppRuntimeSummary {
pub runtime_metadata: DesktopAppRuntimeMetadataSummary,
pub sync_status: DesktopAppSyncStatusSummary,
pub startup_issue: Option<String>,
+ pub sdk_status: Option<DesktopAppSdkStatusSummary>,
}
#[derive(Debug, Error)]
@@ -9886,8 +10080,8 @@ mod tests {
use futures_util::{SinkExt, StreamExt};
use radroots_app_core::{
AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform,
- AppSdkLifecycleState, AppSharedAccountsPaths, SHARED_ACCOUNTS_STORE_FILE_NAME,
- SHARED_IDENTITY_FILE_NAME,
+ AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSharedAccountsPaths,
+ SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITY_FILE_NAME,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord,
@@ -9981,9 +10175,9 @@ mod tests {
use super::{
APP_DATABASE_FILE_NAME, DesktopAppRuntime, DesktopAppRuntimeActivityContextError,
DesktopAppRuntimeCommandError, DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeState,
- DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths, SYNC_TRANSPORT_UNAVAILABLE_MESSAGE,
- SdkDirectRelayAppSyncTransport, TokioRuntimeBuilder, default_sync_transport,
- direct_relay_event_source_runtime, farm_sync_payload, is_hex_64,
+ DesktopAppSdkDiagnosticsState, DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths,
+ SYNC_TRANSPORT_UNAVAILABLE_MESSAGE, SdkDirectRelayAppSyncTransport, TokioRuntimeBuilder,
+ default_sync_transport, direct_relay_event_source_runtime, farm_sync_payload, is_hex_64,
order_decision_publish_payload_to_sdk_decision, pending_sync_upsert,
signed_event_from_local_record,
};
@@ -13444,7 +13638,12 @@ mod tests {
#[test]
fn runtime_bootstrap_starts_sdk_runtime_under_app_data_root() {
- let (runtime, paths) = bootstrapped_runtime("sdk_runtime");
+ let paths = temp_desktop_runtime_paths("sdk_runtime");
+ let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
+ paths.clone(),
+ vec!["ws://127.0.0.1:8080".to_owned()],
+ super::default_runtime_snapshot(),
+ );
let status = runtime
.wait_for_sdk_startup(StdDuration::from_secs(5))
.expect("sdk runtime should be present");
@@ -13476,6 +13675,145 @@ mod tests {
}
#[test]
+ fn runtime_summary_surfaces_lightweight_sdk_status() {
+ let paths = temp_desktop_runtime_paths("sdk_summary_status");
+ let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
+ paths.clone(),
+ vec!["ws://127.0.0.1:8080".to_owned()],
+ super::default_runtime_snapshot(),
+ );
+ runtime
+ .wait_for_sdk_startup(StdDuration::from_secs(5))
+ .expect("sdk runtime should be present");
+
+ let summary = runtime.summary();
+ let sdk_status = summary.sdk_status.expect("sdk status summary");
+
+ assert_eq!(sdk_status.lifecycle_state, AppSdkLifecycleState::Ready);
+ assert_eq!(
+ sdk_status.projection_lifecycle_state,
+ AppSdkProjectionLifecycleState::Current
+ );
+ assert_eq!(sdk_status.storage_root, paths.app.data.join("sdk"));
+ assert_eq!(
+ sdk_status.event_store_path.as_ref(),
+ Some(&paths.app.data.join("sdk").join("event_store.sqlite"))
+ );
+ assert_eq!(sdk_status.relay_target_count, 1);
+ assert!(
+ runtime
+ .shutdown_sdk_runtime()
+ .expect("sdk runtime should shut down")
+ );
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_sdk_diagnostics_summary_preserves_degraded_issue_metadata() {
+ let paths = temp_desktop_runtime_paths("sdk_summary_degraded");
+ let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
+ paths.clone(),
+ vec!["ws://relay.example".to_owned()],
+ super::default_runtime_snapshot(),
+ );
+ let status = runtime
+ .wait_for_sdk_startup(StdDuration::from_secs(5))
+ .expect("sdk runtime should be present");
+ assert_eq!(status.state, AppSdkLifecycleState::Degraded);
+
+ let diagnostics = runtime
+ .sdk_diagnostics_summary()
+ .expect("sdk diagnostics summary");
+
+ assert_eq!(
+ diagnostics.status.lifecycle_state,
+ AppSdkLifecycleState::Degraded
+ );
+ match diagnostics.state {
+ DesktopAppSdkDiagnosticsState::Blocked(issue) => {
+ assert_eq!(issue.code, "invalid_relay_url");
+ assert_eq!(issue.class, "configuration");
+ assert!(!issue.retryable);
+ assert!(
+ issue
+ .recovery_actions
+ .contains(&"configure_relay_targets".to_owned())
+ );
+ }
+ unexpected => panic!("unexpected diagnostics state: {unexpected:?}"),
+ }
+ assert!(
+ runtime
+ .shutdown_sdk_runtime()
+ .expect("sdk runtime should shut down")
+ );
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_sdk_diagnostics_summary_keeps_lifecycle_busy_visible() {
+ let paths = temp_desktop_runtime_paths("sdk_summary_busy");
+ let runtime = DesktopAppRuntime::bootstrap_from_paths_with_snapshot(
+ paths.clone(),
+ vec!["ws://127.0.0.1:8080".to_owned()],
+ super::default_runtime_snapshot(),
+ );
+ runtime
+ .wait_for_sdk_startup(StdDuration::from_secs(5))
+ .expect("sdk runtime should be present");
+ {
+ let sdk_runtime = runtime.sdk_runtime.lock().expect("sdk runtime lock");
+ sdk_runtime
+ .as_ref()
+ .expect("sdk runtime")
+ .begin_projection_rebuild()
+ .expect("projection rebuild should begin");
+ }
+
+ let diagnostics = runtime
+ .sdk_diagnostics_summary()
+ .expect("sdk diagnostics summary");
+
+ assert_eq!(
+ diagnostics.status.lifecycle_state,
+ AppSdkLifecycleState::RebuildingProjections
+ );
+ assert_eq!(
+ diagnostics.status.projection_lifecycle_state,
+ AppSdkProjectionLifecycleState::Rebuilding
+ );
+ match diagnostics.state {
+ DesktopAppSdkDiagnosticsState::Blocked(issue) => {
+ assert_eq!(issue.code, "sdk_lifecycle_busy");
+ assert!(issue.retryable);
+ assert!(
+ issue
+ .recovery_actions
+ .contains(&"wait_for_sdk_lifecycle".to_owned())
+ );
+ }
+ unexpected => panic!("unexpected diagnostics state: {unexpected:?}"),
+ }
+ {
+ let sdk_runtime = runtime.sdk_runtime.lock().expect("sdk runtime lock");
+ sdk_runtime
+ .as_ref()
+ .expect("sdk runtime")
+ .complete_projection_rebuild()
+ .expect("projection rebuild should complete");
+ }
+ assert!(
+ runtime
+ .shutdown_sdk_runtime()
+ .expect("sdk runtime should shut down")
+ );
+
+ 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 {
diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs
@@ -11,6 +11,9 @@ use gpui_component::{
menu::PopupMenuItem,
select::{SearchableVec, Select, SelectDelegate, SelectEvent, SelectState},
};
+use radroots_app_core::{
+ AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy,
+};
use radroots_app_i18n::{AppTextKey, app_text};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
@@ -99,8 +102,10 @@ use crate::pack_day_print::{
PackDayPrintError, execute_pack_day_batch_print_plan, execute_pack_day_print_plan,
};
use crate::runtime::{
- DesktopAppRuntime, DesktopAppRuntimeSummary, DesktopAppSyncConflictSummary,
- DesktopAppSyncStatusSummary,
+ DesktopAppRuntime, DesktopAppRuntimeSummary, DesktopAppSdkDiagnosticsState,
+ DesktopAppSdkDiagnosticsSummary, DesktopAppSdkIssueSummary,
+ DesktopAppSdkReadyDiagnosticsSummary, DesktopAppSdkStatusSummary,
+ DesktopAppSyncConflictSummary, DesktopAppSyncStatusSummary,
};
const HOME_WINDOW_MIN_WIDTH_PX: f32 = 1080.0;
@@ -8362,7 +8367,8 @@ impl SettingsWindowView {
fn about_panel(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let runtime = self.runtime.summary();
- let status_rows = about_status_rows(&runtime);
+ let sdk_diagnostics = self.runtime.sdk_diagnostics_summary();
+ let status_rows = about_status_rows(&runtime, sdk_diagnostics.as_ref());
let runtime_rows = about_runtime_rows(&runtime);
let manual_refresh_enabled = about_manual_refresh_enabled(&runtime.sync_status);
let conflict_cards = runtime
@@ -8735,7 +8741,10 @@ fn settings_account_activation_key(
}
}
-fn about_status_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> {
+fn about_status_rows(
+ runtime: &DesktopAppRuntimeSummary,
+ sdk_diagnostics: Option<&DesktopAppSdkDiagnosticsSummary>,
+) -> Vec<LabelValueRow> {
let mut rows = vec![
LabelValueRow::new(
app_shared_text(AppTextKey::MetadataSelectedAccount),
@@ -8791,9 +8800,127 @@ fn about_status_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> {
.unwrap_or_else(|| app_text(AppTextKey::ValueNone)),
));
+ append_sdk_status_rows(&mut rows, runtime.sdk_status.as_ref(), sdk_diagnostics);
+
rows
}
+fn append_sdk_status_rows(
+ rows: &mut Vec<LabelValueRow>,
+ sdk_status: Option<&DesktopAppSdkStatusSummary>,
+ sdk_diagnostics: Option<&DesktopAppSdkDiagnosticsSummary>,
+) {
+ let status = sdk_diagnostics
+ .map(|diagnostics| &diagnostics.status)
+ .or(sdk_status);
+ let Some(status) = status else {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkLifecycleState),
+ app_text(AppTextKey::ValueDisabled),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
+ app_text(AppTextKey::ValueSdkUnavailable),
+ ));
+ return;
+ };
+
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkLifecycleState),
+ sdk_lifecycle_state_text(status.lifecycle_state),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkProjectionLifecycleState),
+ sdk_projection_lifecycle_state_text(status.projection_lifecycle_state),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkRelayTargetCount),
+ status.relay_target_count.to_string(),
+ ));
+
+ match sdk_diagnostics.map(|diagnostics| &diagnostics.state) {
+ Some(DesktopAppSdkDiagnosticsState::Ready(ready)) => {
+ append_ready_sdk_rows(rows, ready);
+ append_sdk_issue_rows(rows, status.last_issue.as_ref());
+ }
+ Some(DesktopAppSdkDiagnosticsState::Blocked(issue)) => {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
+ app_text(AppTextKey::ValueSdkDiagnosticsBlocked),
+ ));
+ append_sdk_issue_rows(rows, Some(issue));
+ }
+ None => {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
+ app_text(AppTextKey::ValueNone),
+ ));
+ append_sdk_issue_rows(rows, status.last_issue.as_ref());
+ }
+ }
+}
+
+fn append_ready_sdk_rows(
+ rows: &mut Vec<LabelValueRow>,
+ ready: &DesktopAppSdkReadyDiagnosticsSummary,
+) {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkDiagnosticState),
+ app_text(AppTextKey::ValueSdkDiagnosticsReady),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkStorageKind),
+ sdk_storage_kind_text(ready.storage_kind.as_str()),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkEventCount),
+ ready.event_store_total_events.to_string(),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkOutboxCount),
+ ready.outbox_total_events.to_string(),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkOutboxPendingCount),
+ ready.outbox_pending_events.to_string(),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkOutboxFailedCount),
+ ready.outbox_failed_terminal_events.to_string(),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkIntegrityStatus),
+ sdk_integrity_status_text(ready.integrity_ok()),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkSyncStatus),
+ app_text(AppTextKey::ValueSdkDiagnosticsReady),
+ ));
+}
+
+fn append_sdk_issue_rows(rows: &mut Vec<LabelValueRow>, issue: Option<&DesktopAppSdkIssueSummary>) {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkLastIssueCode),
+ issue
+ .map(|issue| issue.code.clone())
+ .unwrap_or_else(|| app_text(AppTextKey::ValueNone)),
+ ));
+ if let Some(issue) = issue {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkLastIssueClass),
+ issue.class.clone(),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkIssueRetryable),
+ yes_no_text(issue.retryable),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkRecoveryAction),
+ sdk_recovery_actions_text(&issue.recovery_actions),
+ ));
+ }
+}
+
fn about_conflict_review_body_key(sync_status: &DesktopAppSyncStatusSummary) -> AppTextKey {
if !sync_status.is_enabled() {
AppTextKey::SettingsAboutConflictReviewUnavailable
@@ -8962,6 +9089,24 @@ fn about_runtime_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow>
.storage_key()
.to_owned(),
));
+ if let Some(sdk_status) = runtime.sdk_status.as_ref() {
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkStorageRoot),
+ sdk_status.storage_root.display().to_string(),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkEventStorePath),
+ path_or_none(sdk_status.event_store_path.as_ref()),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkOutboxPath),
+ path_or_none(sdk_status.outbox_path.as_ref()),
+ ));
+ rows.push(LabelValueRow::new(
+ app_shared_text(AppTextKey::MetadataSdkRelayUrlPolicy),
+ sdk_relay_url_policy_text(sdk_status.relay_url_policy),
+ ));
+ }
rows
}
@@ -9003,6 +9148,83 @@ fn path_or_none(path: Option<&PathBuf>) -> String {
.unwrap_or_else(|| app_text(AppTextKey::ValueNone))
}
+fn sdk_lifecycle_state_text(state: AppSdkLifecycleState) -> String {
+ app_text(match state {
+ AppSdkLifecycleState::Starting => AppTextKey::ValueSdkLifecycleStarting,
+ AppSdkLifecycleState::Ready => AppTextKey::ValueSdkLifecycleReady,
+ AppSdkLifecycleState::Degraded => AppTextKey::ValueSdkLifecycleDegraded,
+ AppSdkLifecycleState::Pausing => AppTextKey::ValueSdkLifecyclePausing,
+ AppSdkLifecycleState::Paused => AppTextKey::ValueSdkLifecyclePaused,
+ AppSdkLifecycleState::Restoring => AppTextKey::ValueSdkLifecycleRestoring,
+ AppSdkLifecycleState::RebuildingProjections => {
+ AppTextKey::ValueSdkLifecycleRebuildingProjections
+ }
+ AppSdkLifecycleState::ShuttingDown => AppTextKey::ValueSdkLifecycleShuttingDown,
+ AppSdkLifecycleState::Stopped => AppTextKey::ValueSdkLifecycleStopped,
+ })
+}
+
+fn sdk_projection_lifecycle_state_text(state: AppSdkProjectionLifecycleState) -> String {
+ app_text(match state {
+ AppSdkProjectionLifecycleState::Current => AppTextKey::ValueSdkProjectionCurrent,
+ AppSdkProjectionLifecycleState::Stale => AppTextKey::ValueSdkProjectionStale,
+ AppSdkProjectionLifecycleState::Rebuilding => AppTextKey::ValueSdkProjectionRebuilding,
+ })
+}
+
+fn sdk_relay_url_policy_text(policy: AppSdkRelayUrlPolicy) -> String {
+ app_text(match policy {
+ AppSdkRelayUrlPolicy::Public => AppTextKey::ValueSdkRelayPolicyPublic,
+ AppSdkRelayUrlPolicy::Localhost => AppTextKey::ValueSdkRelayPolicyLocalhost,
+ })
+}
+
+fn sdk_storage_kind_text(kind: &str) -> String {
+ match kind {
+ "directory" => app_text(AppTextKey::ValueSdkStorageKindDirectory),
+ _ => app_text(AppTextKey::ValueSdkStorageKindUnknown),
+ }
+}
+
+fn sdk_integrity_status_text(ok: bool) -> String {
+ if ok {
+ app_text(AppTextKey::ValueSdkIntegrityOk)
+ } else {
+ app_text(AppTextKey::ValueSdkIntegrityFailed)
+ }
+}
+
+fn yes_no_text(value: bool) -> String {
+ if value {
+ app_text(AppTextKey::ValueYes)
+ } else {
+ app_text(AppTextKey::ValueNo)
+ }
+}
+
+fn sdk_recovery_actions_text(actions: &[String]) -> String {
+ if actions.is_empty() {
+ return app_text(AppTextKey::ValueNone);
+ }
+
+ actions
+ .iter()
+ .map(|action| app_text(sdk_recovery_action_key(action)))
+ .collect::<Vec<_>>()
+ .join(", ")
+}
+
+fn sdk_recovery_action_key(action: &str) -> AppTextKey {
+ match action {
+ "configure_relay_targets" => AppTextKey::ValueSdkRecoveryConfigureRelayTargets,
+ "retry_startup" => AppTextKey::ValueSdkRecoveryRetryStartup,
+ "wait_for_sdk_lifecycle" => AppTextKey::ValueSdkRecoveryWaitForLifecycle,
+ "retry_status_refresh" => AppTextKey::ValueSdkRecoveryRetryStatusRefresh,
+ "review_runtime_configuration" => AppTextKey::ValueSdkRecoveryReviewRuntimeConfiguration,
+ _ => AppTextKey::ValueSdkRecoveryReviewStatus,
+ }
+}
+
fn focus_button<V>(window: &mut Window, id: impl Into<ElementId>, cx: &mut Context<V>) {
let focus_handle = window
.use_keyed_state(id, cx, |_, cx| cx.focus_handle())
@@ -16944,11 +17166,14 @@ mod tests {
trade_payment_display_status_key, trade_revision_status_key, trade_workflow_source_key,
};
use crate::runtime::{
- DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeSummary, DesktopAppSyncConflictSummary,
- DesktopAppSyncStatusSummary,
+ DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeSummary, DesktopAppSdkDiagnosticsState,
+ DesktopAppSdkDiagnosticsSummary, DesktopAppSdkIssueSummary,
+ DesktopAppSdkReadyDiagnosticsSummary, DesktopAppSdkStatusSummary,
+ DesktopAppSyncConflictSummary, DesktopAppSyncStatusSummary,
};
use radroots_app_core::{
AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform,
+ AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
@@ -19252,11 +19477,14 @@ 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(),
- ));
+ let rows = about_status_rows(
+ &summary(
+ HomeRoute::SetupRequired,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ ),
+ None,
+ );
assert!(rows.iter().any(|row| {
row.label == app_text(AppTextKey::MetadataSelectedAccount)
@@ -19376,6 +19604,103 @@ mod tests {
}
#[test]
+ fn about_status_rows_surface_ready_sdk_diagnostics() {
+ let sdk_status = fixture_sdk_status(AppSdkLifecycleState::Ready);
+ let sdk_diagnostics = DesktopAppSdkDiagnosticsSummary {
+ status: sdk_status.clone(),
+ state: DesktopAppSdkDiagnosticsState::Ready(DesktopAppSdkReadyDiagnosticsSummary {
+ storage_kind: "directory".to_owned(),
+ event_store_total_events: 7,
+ outbox_total_events: 3,
+ outbox_pending_events: 2,
+ outbox_failed_terminal_events: 0,
+ integrity_event_store_ok: true,
+ integrity_outbox_ok: true,
+ sync_source: "sdk_canonical_stores".to_owned(),
+ sync_observed_at_ms: 42,
+ sync_relay_target_count: 2,
+ }),
+ };
+ let mut runtime = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ runtime.sdk_status = Some(sdk_status);
+
+ let rows = about_status_rows(&runtime, Some(&sdk_diagnostics));
+
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkLifecycleState)
+ && row.value == app_text(AppTextKey::ValueSdkLifecycleReady)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkDiagnosticState)
+ && row.value == app_text(AppTextKey::ValueSdkDiagnosticsReady)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkStorageKind)
+ && row.value == app_text(AppTextKey::ValueSdkStorageKindDirectory)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkOutboxPendingCount) && row.value == "2"
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkIntegrityStatus)
+ && row.value == app_text(AppTextKey::ValueSdkIntegrityOk)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkLastIssueCode)
+ && row.value == app_text(AppTextKey::ValueNone)
+ }));
+ }
+
+ #[test]
+ fn about_status_rows_surface_blocked_sdk_issue_metadata() {
+ let issue = DesktopAppSdkIssueSummary {
+ code: "invalid_relay_url".to_owned(),
+ class: "configuration".to_owned(),
+ retryable: false,
+ recovery_actions: vec!["configure_relay_targets".to_owned()],
+ };
+ let mut sdk_status = fixture_sdk_status(AppSdkLifecycleState::Degraded);
+ sdk_status.last_issue = Some(issue.clone());
+ let sdk_diagnostics = DesktopAppSdkDiagnosticsSummary {
+ status: sdk_status.clone(),
+ state: DesktopAppSdkDiagnosticsState::Blocked(issue),
+ };
+ let mut runtime = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ runtime.sdk_status = Some(sdk_status);
+
+ let rows = about_status_rows(&runtime, Some(&sdk_diagnostics));
+
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkLifecycleState)
+ && row.value == app_text(AppTextKey::ValueSdkLifecycleDegraded)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkDiagnosticState)
+ && row.value == app_text(AppTextKey::ValueSdkDiagnosticsBlocked)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkLastIssueCode)
+ && row.value == "invalid_relay_url"
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkIssueRetryable)
+ && row.value == app_text(AppTextKey::ValueNo)
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkRecoveryAction)
+ && row.value == app_text(AppTextKey::ValueSdkRecoveryConfigureRelayTargets)
+ }));
+ }
+
+ #[test]
fn about_runtime_rows_append_paths_schema_and_shell_section() {
let mut runtime = summary(
HomeRoute::Today,
@@ -19394,6 +19719,7 @@ mod tests {
database_schema_version: Some(7),
..DesktopAppRuntimeMetadataSummary::default()
};
+ runtime.sdk_status = Some(fixture_sdk_status(AppSdkLifecycleState::Ready));
let rows = about_runtime_rows(&runtime);
@@ -19409,6 +19735,14 @@ mod tests {
row.label == app_text(AppTextKey::MetadataShellSection)
&& row.value == ShellSection::Settings(SettingsPanelViewKey::About).storage_key()
}));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkStorageRoot)
+ && row.value == "/tmp/radroots/data/apps/app/sdk"
+ }));
+ assert!(rows.iter().any(|row| {
+ row.label == app_text(AppTextKey::MetadataSdkRelayUrlPolicy)
+ && row.value == app_text(AppTextKey::ValueSdkRelayPolicyLocalhost)
+ }));
}
fn summary(
@@ -19453,6 +19787,22 @@ mod tests {
runtime_metadata: DesktopAppRuntimeMetadataSummary::default(),
sync_status: crate::runtime::DesktopAppSyncStatusSummary::default(),
startup_issue: None,
+ sdk_status: None,
+ }
+ }
+
+ fn fixture_sdk_status(lifecycle_state: AppSdkLifecycleState) -> DesktopAppSdkStatusSummary {
+ let storage_root = PathBuf::from("/tmp/radroots/data/apps/app/sdk");
+ DesktopAppSdkStatusSummary {
+ lifecycle_state,
+ projection_lifecycle_state: AppSdkProjectionLifecycleState::Current,
+ projection_lifecycle_reason: None,
+ storage_root: storage_root.clone(),
+ event_store_path: Some(storage_root.join("event_store.sqlite")),
+ outbox_path: Some(storage_root.join("outbox.sqlite")),
+ relay_target_count: 2,
+ relay_url_policy: AppSdkRelayUrlPolicy::Localhost,
+ last_issue: None,
}
}
diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs
@@ -811,9 +811,57 @@ define_app_text_keys! {
MetadataSyncConflictDetectedAt => "metadata.sync_conflict_detected_at",
MetadataSyncConflictResolution => "metadata.sync_conflict_resolution",
MetadataStartupIssue => "metadata.startup_issue",
+ MetadataSdkLifecycleState => "metadata.sdk_lifecycle_state",
+ MetadataSdkProjectionLifecycleState => "metadata.sdk_projection_lifecycle_state",
+ MetadataSdkDiagnosticState => "metadata.sdk_diagnostic_state",
+ MetadataSdkStorageKind => "metadata.sdk_storage_kind",
+ MetadataSdkEventCount => "metadata.sdk_event_count",
+ MetadataSdkOutboxCount => "metadata.sdk_outbox_count",
+ MetadataSdkOutboxPendingCount => "metadata.sdk_outbox_pending_count",
+ MetadataSdkOutboxFailedCount => "metadata.sdk_outbox_failed_count",
+ MetadataSdkIntegrityStatus => "metadata.sdk_integrity_status",
+ MetadataSdkSyncStatus => "metadata.sdk_sync_status",
+ MetadataSdkRelayTargetCount => "metadata.sdk_relay_target_count",
+ MetadataSdkLastIssueCode => "metadata.sdk_last_issue_code",
+ MetadataSdkLastIssueClass => "metadata.sdk_last_issue_class",
+ MetadataSdkIssueRetryable => "metadata.sdk_issue_retryable",
+ MetadataSdkRecoveryAction => "metadata.sdk_recovery_action",
+ MetadataSdkStorageRoot => "metadata.sdk_storage_root",
+ MetadataSdkEventStorePath => "metadata.sdk_event_store_path",
+ MetadataSdkOutboxPath => "metadata.sdk_outbox_path",
+ MetadataSdkRelayUrlPolicy => "metadata.sdk_relay_url_policy",
ValueNone => "value.none",
+ ValueYes => "value.yes",
+ ValueNo => "value.no",
ValueEnabled => "value.enabled",
ValueDisabled => "value.disabled",
+ ValueSdkUnavailable => "value.sdk.unavailable",
+ ValueSdkDiagnosticsReady => "value.sdk.diagnostics.ready",
+ ValueSdkDiagnosticsBlocked => "value.sdk.diagnostics.blocked",
+ ValueSdkLifecycleStarting => "value.sdk.lifecycle.starting",
+ ValueSdkLifecycleReady => "value.sdk.lifecycle.ready",
+ ValueSdkLifecycleDegraded => "value.sdk.lifecycle.degraded",
+ ValueSdkLifecyclePausing => "value.sdk.lifecycle.pausing",
+ ValueSdkLifecyclePaused => "value.sdk.lifecycle.paused",
+ ValueSdkLifecycleRestoring => "value.sdk.lifecycle.restoring",
+ ValueSdkLifecycleRebuildingProjections => "value.sdk.lifecycle.rebuilding_projections",
+ ValueSdkLifecycleShuttingDown => "value.sdk.lifecycle.shutting_down",
+ ValueSdkLifecycleStopped => "value.sdk.lifecycle.stopped",
+ ValueSdkProjectionCurrent => "value.sdk.projection.current",
+ ValueSdkProjectionStale => "value.sdk.projection.stale",
+ ValueSdkProjectionRebuilding => "value.sdk.projection.rebuilding",
+ ValueSdkRelayPolicyPublic => "value.sdk.relay_policy.public",
+ ValueSdkRelayPolicyLocalhost => "value.sdk.relay_policy.localhost",
+ ValueSdkStorageKindDirectory => "value.sdk.storage_kind.directory",
+ ValueSdkStorageKindUnknown => "value.sdk.storage_kind.unknown",
+ ValueSdkIntegrityOk => "value.sdk.integrity.ok",
+ ValueSdkIntegrityFailed => "value.sdk.integrity.failed",
+ ValueSdkRecoveryConfigureRelayTargets => "value.sdk.recovery.configure_relay_targets",
+ ValueSdkRecoveryRetryStartup => "value.sdk.recovery.retry_startup",
+ ValueSdkRecoveryWaitForLifecycle => "value.sdk.recovery.wait_for_lifecycle",
+ ValueSdkRecoveryRetryStatusRefresh => "value.sdk.recovery.retry_status_refresh",
+ ValueSdkRecoveryReviewRuntimeConfiguration => "value.sdk.recovery.review_runtime_configuration",
+ ValueSdkRecoveryReviewStatus => "value.sdk.recovery.review_status",
ValueRuntimeModeDevelopment => "value.runtime_mode.development",
ValueRuntimeModeProduction => "value.runtime_mode.production",
ValueSyncRunStatusIdle => "value.sync_run_status.idle",
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -790,9 +790,57 @@
"metadata.sync_conflict_detected_at": "detected",
"metadata.sync_conflict_resolution": "resolution",
"metadata.startup_issue": "startup issue",
+ "metadata.sdk_lifecycle_state": "SDK lifecycle",
+ "metadata.sdk_projection_lifecycle_state": "SDK projections",
+ "metadata.sdk_diagnostic_state": "SDK diagnostics",
+ "metadata.sdk_storage_kind": "SDK storage",
+ "metadata.sdk_event_count": "SDK events",
+ "metadata.sdk_outbox_count": "SDK outbox events",
+ "metadata.sdk_outbox_pending_count": "SDK outbox pending",
+ "metadata.sdk_outbox_failed_count": "SDK outbox failed",
+ "metadata.sdk_integrity_status": "SDK integrity",
+ "metadata.sdk_sync_status": "SDK sync",
+ "metadata.sdk_relay_target_count": "SDK relay targets",
+ "metadata.sdk_last_issue_code": "SDK issue code",
+ "metadata.sdk_last_issue_class": "SDK issue class",
+ "metadata.sdk_issue_retryable": "SDK issue retryable",
+ "metadata.sdk_recovery_action": "SDK recovery",
+ "metadata.sdk_storage_root": "SDK storage root",
+ "metadata.sdk_event_store_path": "SDK event store",
+ "metadata.sdk_outbox_path": "SDK outbox",
+ "metadata.sdk_relay_url_policy": "SDK relay policy",
"value.none": "none",
+ "value.yes": "yes",
+ "value.no": "no",
"value.enabled": "enabled",
"value.disabled": "disabled",
+ "value.sdk.unavailable": "unavailable",
+ "value.sdk.diagnostics.ready": "ready",
+ "value.sdk.diagnostics.blocked": "blocked",
+ "value.sdk.lifecycle.starting": "starting",
+ "value.sdk.lifecycle.ready": "ready",
+ "value.sdk.lifecycle.degraded": "degraded",
+ "value.sdk.lifecycle.pausing": "pausing",
+ "value.sdk.lifecycle.paused": "paused",
+ "value.sdk.lifecycle.restoring": "restoring",
+ "value.sdk.lifecycle.rebuilding_projections": "rebuilding projections",
+ "value.sdk.lifecycle.shutting_down": "shutting down",
+ "value.sdk.lifecycle.stopped": "stopped",
+ "value.sdk.projection.current": "current",
+ "value.sdk.projection.stale": "stale",
+ "value.sdk.projection.rebuilding": "rebuilding",
+ "value.sdk.relay_policy.public": "public",
+ "value.sdk.relay_policy.localhost": "localhost",
+ "value.sdk.storage_kind.directory": "directory",
+ "value.sdk.storage_kind.unknown": "unknown",
+ "value.sdk.integrity.ok": "ok",
+ "value.sdk.integrity.failed": "failed",
+ "value.sdk.recovery.configure_relay_targets": "Configure relay targets.",
+ "value.sdk.recovery.retry_startup": "Retry startup.",
+ "value.sdk.recovery.wait_for_lifecycle": "Wait for the current SDK operation to finish.",
+ "value.sdk.recovery.retry_status_refresh": "Refresh status again.",
+ "value.sdk.recovery.review_runtime_configuration": "Review runtime configuration.",
+ "value.sdk.recovery.review_status": "Review SDK status.",
"value.runtime_mode.development": "development",
"value.runtime_mode.production": "production",
"value.sync_run_status.idle": "idle",