From 7ec462a589f3ebde401526fa9e36bc6fc4b7b270 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 1 Mar 2026 01:31:08 -0500 Subject: [PATCH] Improve Nostr sync failure messaging for offline mode --- memory-update.md | 12 ++++++++++++ src/main.py | 4 ++++ src/seedpass/core/manager.py | 17 +++++++++++++++++ src/tests/test_background_vault_sync_paths.py | 14 ++++++++++++++ src/tests/test_post_sync_messages.py | 10 ++++++++++ 5 files changed, 57 insertions(+) diff --git a/memory-update.md b/memory-update.md index 342cb1be..d4cad7fa 100644 --- a/memory-update.md +++ b/memory-update.md @@ -25,3 +25,15 @@ - Added `docs/nostr_namespace_reset.md` documenting menu options `8` (reset sync state) and `9` (start fresh namespace), plus validation steps for robust publish/restore. - Linked the new guide from `docs/nostr_setup.md`, `docs/README.md`, and root `README.md`. - Exposed the guide in website docs navigation via `landing/docs.html` (`Nostr Namespace Reset`). + +## Nostr sync failure after namespace increment +- Root-cause pattern: when profile `offline_mode` is enabled, `sync_vault_async()` exits early and previously returned `None` without a helpful message, so UI showed only `❌ Sync failed…`. +- Fixes: + - `PasswordManager.sync_vault_async()` now sets `nostr_client.last_error` for early-return cases (`offline_mode`, missing encrypted index, and missing publish result). + - `main.handle_post_to_nostr()` now explicitly reports offline mode when no detailed error is present. +- Regression tests added: + - `src/tests/test_post_sync_messages.py::test_handle_post_failure_offline_mode_message` + - `src/tests/test_background_vault_sync_paths.py::test_sync_vault_async_sets_offline_error` +- Verified with local venv: + - `./.venv/bin/pytest -q src/tests/test_post_sync_messages.py` + - `./.venv/bin/pytest -q src/tests/test_background_vault_sync_paths.py` diff --git a/src/main.py b/src/main.py index b5a5e5a8..f02e76b0 100644 --- a/src/main.py +++ b/src/main.py @@ -431,6 +431,10 @@ def handle_post_to_nostr( if detail: print(colored(f"\N{CROSS MARK} Sync failed… {detail}", "red")) logging.error("Failed to post encrypted index to Nostr: %s", detail) + elif getattr(password_manager, "offline_mode", False): + detail = "Offline mode is enabled. Disable it in Settings to sync." + print(colored(f"\N{CROSS MARK} Sync failed… {detail}", "red")) + logging.error("Failed to post encrypted index to Nostr: %s", detail) else: print(colored("\N{CROSS MARK} Sync failed…", "red")) logging.error("Failed to post encrypted index to Nostr.") diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 4492915b..854a3206 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -3930,11 +3930,20 @@ class PasswordManager: ) -> dict[str, list[str] | str] | None: """Publish the current vault contents to Nostr and return event IDs.""" try: + client = getattr(self, "nostr_client", None) if getattr(self, "offline_mode", False): + if client is not None and hasattr(client, "last_error"): + client.last_error = ( + "Offline mode is enabled. Disable it in Settings to sync." + ) return None encrypted = self.get_encrypted_data() if not encrypted: + if client is not None and hasattr(client, "last_error"): + client.last_error = "No encrypted index data available to sync." return None + if client is not None and hasattr(client, "last_error"): + client.last_error = None pub_snap = getattr(self.nostr_client, "publish_snapshot", None) manifest = None event_id = None @@ -3948,6 +3957,14 @@ class PasswordManager: event_id = await self.nostr_client.publish_json_to_nostr(encrypted) self.is_dirty = False if event_id is None: + if ( + client is not None + and hasattr(client, "last_error") + and not client.last_error + ): + client.last_error = ( + "Failed to publish encrypted index to configured relays." + ) return None chunk_ids: list[str] = [] if manifest is not None: diff --git a/src/tests/test_background_vault_sync_paths.py b/src/tests/test_background_vault_sync_paths.py index 40464fd7..963cbed0 100644 --- a/src/tests/test_background_vault_sync_paths.py +++ b/src/tests/test_background_vault_sync_paths.py @@ -1,4 +1,5 @@ import asyncio +from types import SimpleNamespace from seedpass.core.manager import PasswordManager from seedpass.core import manager as manager_module @@ -56,3 +57,16 @@ def test_start_background_vault_sync_async_worker_path(monkeypatch): assert ("sync_started",) in events assert ("sync_finished", {"alt_summary": "async-path"}) in events + + +def test_sync_vault_async_sets_offline_error(): + pm = PasswordManager.__new__(PasswordManager) + pm.offline_mode = True + pm.nostr_client = SimpleNamespace(last_error=None) + + result = asyncio.run(pm.sync_vault_async()) + + assert result is None + assert pm.nostr_client.last_error == ( + "Offline mode is enabled. Disable it in Settings to sync." + ) diff --git a/src/tests/test_post_sync_messages.py b/src/tests/test_post_sync_messages.py index 3557cc19..bcaa41b8 100644 --- a/src/tests/test_post_sync_messages.py +++ b/src/tests/test_post_sync_messages.py @@ -41,6 +41,16 @@ def test_handle_post_failure_shows_nostr_error(capsys): assert "❌ Sync failed… Manifest out of date" in out +def test_handle_post_failure_offline_mode_message(capsys): + pm = SimpleNamespace( + sync_vault=lambda alt_summary=None: None, + offline_mode=True, + ) + main.handle_post_to_nostr(pm) + out = capsys.readouterr().out + assert "Offline mode is enabled. Disable it in Settings to sync." in out + + def test_handle_post_prints_all_ids(capsys): pm = SimpleNamespace( sync_vault=lambda alt_summary=None: {