Persist last publish report across restarts

This commit is contained in:
Avi 2026-09-01 12:32:28 -05:00
commit bab82f5148
7 changed files with 63 additions and 3 deletions

View file

@ -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. */

View file

@ -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) {

View file

@ -43,6 +43,7 @@ export function makeState(overrides?: Partial<AppState>): AppState {
active_profile: alice,
profiles: [alice, bob],
settings,
last_publish: null,
...overrides,
};
}

View file

@ -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<VaultKey>,
/// Stack of deleted profiles for undo functionality.
pub undo_history: Vec<ProfileSummary>,
/// The most recent publish report, persisted across restarts.
pub last_publish: Option<StoredPublishReport>,
}
/// 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<ProfileSummary>,
/// The most recent publish report, persisted across restarts.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_publish: Option<StoredPublishReport>,
}
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,
}
}

View file

@ -464,6 +464,13 @@ async fn run_with_app(app: &mut App, request: Request) -> Result<serde_json::Val
let report =
publish::publish_active(&app.vault, &app.settings, &content, app.vault_key())
.await?;
let stored = crate::vault::StoredPublishReport {
event_id: report.event_id.clone(),
succeeded: report.succeeded.clone(),
failed: report.failed.clone(),
};
let _ = crate::vault::save_last_publish(&stored);
app.last_publish = Some(stored);
Ok(json!(report))
}

View file

@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::time::Duration;
use nostr_sdk::prelude::*;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::crypto::VaultKey;
use crate::errors::{AppError, ErrorKind};
@ -16,7 +16,7 @@ use crate::vault::Vault;
const RELAY_SEND_TIMEOUT: Duration = Duration::from_secs(6);
/// A relay that rejected a published note.
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelayFailure {
pub url: String,
/// Concise, user-facing reason.

View file

@ -18,6 +18,8 @@ pub const VAULT_VERSION: u32 = 2;
pub const VAULT_FILE_NAME: &str = "profiles_vault.json";
/// Filename of the settings file.
pub const SETTINGS_FILE_NAME: &str = "settings.json";
/// Filename of the last publish report.
pub const LAST_PUBLISH_FILE_NAME: &str = "last_publish.json";
/// A profile stored on disk.
///
@ -180,6 +182,39 @@ pub fn settings_path() -> 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<String>,
pub failed: Vec<crate::publish::RelayFailure>,
}
/// Load the last publish report, returning `None` when absent or unreadable.
pub fn load_last_publish() -> Option<StoredPublishReport> {
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.