From bab82f51486e0ea78946fd250e37a4bff7d05d5f Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 1 Sep 2026 12:32:28 -0500 Subject: [PATCH] Persist last publish report across restarts --- frontend/src/lib/types.ts | 6 +++++ frontend/src/state/AppProvider.tsx | 3 +++ frontend/src/test/apiMock.ts | 1 + src/app.rs | 10 ++++++++- src/ipc.rs | 7 ++++++ src/publish.rs | 4 ++-- src/vault.rs | 35 ++++++++++++++++++++++++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 5030525..0713cc7 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -140,6 +140,12 @@ export interface AppState { settings: Settings; /** Recently deleted profiles, newest last, for undo. */ undo_history?: ProfileSummary[]; + /** The most recent publish report, persisted across restarts. */ + last_publish?: { + event_id: string; + succeeded: string[]; + failed: RelayFailure[]; + } | null; } /** A secret key revealed after the vault is unlocked. */ diff --git a/frontend/src/state/AppProvider.tsx b/frontend/src/state/AppProvider.tsx index 9703932..0086fe9 100644 --- a/frontend/src/state/AppProvider.tsx +++ b/frontend/src/state/AppProvider.tsx @@ -98,6 +98,9 @@ export function AppProvider({ children }: { children: ReactNode }) { const initial = await api.init(); if (!cancelled) { setState(initial); + if (initial.last_publish) { + setLastPublish({ report: initial.last_publish, error: null, details: null, at: Date.now() }); + } } } catch (error) { if (!cancelled) { diff --git a/frontend/src/test/apiMock.ts b/frontend/src/test/apiMock.ts index 21b89ee..0114b3b 100644 --- a/frontend/src/test/apiMock.ts +++ b/frontend/src/test/apiMock.ts @@ -43,6 +43,7 @@ export function makeState(overrides?: Partial): AppState { active_profile: alice, profiles: [alice, bob], settings, + last_publish: null, ...overrides, }; } diff --git a/src/app.rs b/src/app.rs index 88b569a..f80c939 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,7 @@ use crate::crypto::{self, VaultKey}; use crate::errors::AppError; use crate::profiles::{self, ProfileSummary}; use crate::settings::Settings; -use crate::vault::{self, KdfParams, StoredProfile, Vault, VaultCrypto}; +use crate::vault::{self, KdfParams, StoredProfile, StoredPublishReport, Vault, VaultCrypto}; /// Minimum password length accepted when encrypting the vault. pub const MIN_PASSWORD_LEN: usize = 8; @@ -20,6 +20,8 @@ pub struct App { unlock_key: Option, /// Stack of deleted profiles for undo functionality. pub undo_history: Vec, + /// The most recent publish report, persisted across restarts. + pub last_publish: Option, } /// Snapshot of everything the UI needs, containing no secret keys. @@ -38,6 +40,9 @@ pub struct AppStateView { /// Recently deleted profiles, newest last, for undo. #[serde(skip_serializing_if = "Vec::is_empty")] pub undo_history: Vec, + /// The most recent publish report, persisted across restarts. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_publish: Option, } impl App { @@ -48,6 +53,7 @@ impl App { settings: vault::load_settings()?, unlock_key: None, undo_history: Vec::new(), + last_publish: vault::load_last_publish(), }) } @@ -240,6 +246,7 @@ impl App { profiles: profiles::summaries(&self.vault), settings: self.settings.clone(), undo_history: self.undo_history.clone(), + last_publish: self.last_publish.clone(), } } } @@ -302,6 +309,7 @@ mod tests { settings: offline_settings(), unlock_key: None, undo_history: Vec::new(), + last_publish: None, } } diff --git a/src/ipc.rs b/src/ipc.rs index 744fd6f..d9f6bd1 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -464,6 +464,13 @@ async fn run_with_app(app: &mut App, request: Request) -> Result PathBuf { data_dir().join(SETTINGS_FILE_NAME) } +pub fn last_publish_path() -> PathBuf { + data_dir().join(LAST_PUBLISH_FILE_NAME) +} + +/// A minimal publish report persisted across restarts so the Home screen can +/// show the most recent publication result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredPublishReport { + pub event_id: String, + pub succeeded: Vec, + pub failed: Vec, +} + +/// Load the last publish report, returning `None` when absent or unreadable. +pub fn load_last_publish() -> Option { + let path = last_publish_path(); + if !path.exists() { + return None; + } + let content = fs::read_to_string(&path).ok()?; + if content.trim().is_empty() { + return None; + } + serde_json::from_str(&content).ok() +} + +/// Persist the last publish report with restrictive permissions. +pub fn save_last_publish(report: &StoredPublishReport) -> Result<(), AppError> { + let content = serde_json::to_string_pretty(report) + .map_err(|e| AppError::json("Could not prepare the last publish report for saving", e))?; + write_restricted(&last_publish_path(), &content) +} + /// Candidate locations for a legacy vault created by the old CLI version. /// /// The old application wrote `profiles_vault.json` in its working directory.