Files
bal-electrum-plugin/CHANGELOG_REFACTOR.md
donkey-ai c963f61424 docs(bal): translate refactoring docs to English and rename two files
- CHANGELOG_REFACTOR.md: translated sections 1-16 (header + §1-§16) from
  Italian to English; sections 17-18 were already English and left untouched.
  All code blocks, commit hashes, tables, names, versions and structure kept.
- DIAGNOSI_GUI.md -> GUI_DIAGNOSIS.md: renamed and fully translated to
  English (title included), preserving code, line refs, emojis and tables.
- REPORT_NETWORKING_PARALLELO.md -> PARALLEL_NETWORKING_REPORT.md: renamed
  only (content was already in English).
- CHANGELOG.md: added entry #12 documenting this task.

Documentation-only change; no plugin code touched (zip-first not applicable).
2026-06-28 23:01:47 -04:00

36 KiB
Raw Blame History

BAL — Refactoring report (for the original author)

This document lists all the changes made to the BAL plugin (Bitcoin After Life) with respect to the original version 0.2.8.

Guiding principle: conservative, behaviour-preserving refactoring (Approach A). The business logic was kept byte-identical where possible; what changed are mainly the file layout and the imports. No algorithmic rewrite.

Verification environment: Electrum 4.7.2 + PyQt6 (the latest stable release that exposes json_db.register_dict).


1. Structure reorganization (logic / GUI separation)

The main reported problem was that logic and graphics were mixed together, in particular in a single qt.py file of 4131 lines.

Structure BEFORE (flat, 7 files)

BAL/
├── __init__.py        (empty, 0 lines)
├── bal.py             (243)  logic + plugin base
├── util.py            (533)  helpers
├── heirs.py           (791)  heirs model + tx building
├── will.py            (927)  will/WillItem model
├── willexecutors.py   (374)  will-executor networking
├── qt.py              (4131) ALL the GUI + the Plugin in a single file
└── bal_resources.py   (14)

Structure AFTER (core/ vs gui/)

bal/
├── manifest.json            standard-compliant metadata
├── qt.py                    loading shim (re-export of Plugin)
├── __init__.py              architecture docstring + __version__
├── core/                    LOGIC with no Qt dependencies
│   ├── util.py              (was util.py)
│   ├── plugin_base.py       (was bal.py)
│   ├── heirs.py             (was heirs.py)
│   ├── will.py              (was will.py)
│   └── willexecutors.py     (was willexecutors.py)
└── gui/qt/                  PyQt6 PRESENTATION
    ├── theme.py      (59)   status → colour mapping
    ├── common.py     (155)  shared imports + GUI helpers
    ├── widgets.py    (782)  "leaf" widgets
    ├── calendar.py   (80)   BalCalendar
    ├── dialogs.py    (1127) dialog windows
    ├── lists.py      (957)  tree views (heirs/preview/executor)
    ├── window.py     (952)  per-wallet GUI controller (BalWindow)
    └── plugin.py     (273)  Plugin class (@hook Electrum → GUI)

The 4131-line qt.py file was split by responsibility. The class bodies were copied verbatim (line by line) so as not to touch the delicate inheritance-transaction logic.

Map: where the 40 classes/functions of qt.py ended up

Class/function (orig. line) New module
Plugin (67) gui/qt/plugin.py
shown_cv (317) gui/qt/common.py
BalWindow (330) gui/qt/window.py
add_widget (1257) gui/qt/common.py
ClickableLabel (1263) gui/qt/widgets.py
BalTxFeesWidget (1271) gui/qt/widgets.py
_LockTimeEditor (1340) gui/qt/widgets.py
BalTimeEditWidget (1374) gui/qt/widgets.py
TimeRawEditWidget (1508) gui/qt/widgets.py
LockTimeRawEdit (1527) gui/qt/widgets.py
LockTimeDateEdit (1605) gui/qt/widgets.py
ThresholdTimeWidget (1644) gui/qt/widgets.py
LockTimeWidget (1664) gui/qt/widgets.py
WillSettingsWidget (1683) gui/qt/widgets.py
PercAmountEdit (1818) gui/qt/widgets.py
BalDialog (1883) gui/qt/dialogs.py
BalWizardDialog (1913) gui/qt/dialogs.py
BalWizardWidget (2002) gui/qt/dialogs.py
BalWizardHeirsWidget (2068) gui/qt/dialogs.py
BalWizardWEDownloadWidget (2103) gui/qt/dialogs.py
BalWizardWEWidget (2190) gui/qt/dialogs.py
BalWizardLocktimeAndFeeWidget(2207) gui/qt/dialogs.py
BalWaitingDialog (2224) gui/qt/dialogs.py
BalBlockingWaitingDialog (2285) gui/qt/dialogs.py
BalLineEdit (2304) gui/qt/widgets.py
BalTextEdit (2312) gui/qt/widgets.py
BalCheckBox (2320) gui/qt/widgets.py
BalBuildWillDialog (2335) gui/qt/dialogs.py
HeirListWidget (2858) gui/qt/lists.py
PreviewList (3059) gui/qt/lists.py
WillDetailDialog (3445) gui/qt/dialogs.py
WillWidget (3545) gui/qt/widgets.py
WillExecutorListWidget (3637) gui/qt/lists.py
WillExecutorWidget (3873) gui/qt/lists.py
WillExecutorDialog (3982) gui/qt/dialogs.py
CheckAliveError (4018) gui/qt/common.py
log_error (4028) gui/qt/common.py
export_meta_gui (4043) gui/qt/common.py
BalCalendar (4066) gui/qt/calendar.py

2. Removals (dead / debug code) — behaviour unchanged

All the following removals were verified as unused or purely debug, so they do not alter the plugin's behaviour.

  1. util.pycore/util.py: removed three debug helpers used only for console printing:

    • print_var() (orig. line 439)
    • print_utxo() (orig. line 474)
    • print_prevout() (orig. line 486)
  2. bal.pycore/plugin_base.py: removed the empty stub function get_will_settings(x) (orig. lines 12-14):

    def get_will_settings(x):
        # print(x)
        pass
    

    ⚠️ Verified: it was not referenced by any register_dict — the three register_dict calls use tuple, dict, lambda x: x. So it was dead code. The used function get_will(x) was kept identical.

  3. will.py (WillItem) → moved to gui/qt/theme.py: the method WillItem.get_color() (orig. line 852) returned hexadecimal colours — it is presentation logic, not domain logic. It was moved out of the model and turned into the function status_color(will_item) in gui/qt/theme.py. Verified byte-identical across all status combinations (same get_status(...) chain, same colour codes).


3. Import changes (required by the new structure)

The imports were updated from "flat" to "package" style. Examples:

Before After
from .bal import BalPlugin from .plugin_base import BalPlugin (in willexecutors)
from .util import Util from .util import Util (unchanged, now inside core/)
(in qt.py) from .bal import ... the GUI modules import from ...core.X
  • Added from .common import _, _logger in the GUI modules, because import * does not export names starting with an underscore.
  • Added 3 "lazy" imports (inside the functions) in dialogs.py to break the dialogs ↔ lists cycle (lists imports BalBuildWillDialog from dialogs).

The internal logic of the methods was not touched: prepare_transactions(), buildTransactions(), etc. are verbatim.


4. Electrum-standard packaging

manifest.json made compliant with https://plugins.electrum.org/developers.html :

Field Before After
name "BAL" "bal" (lowercase = dir name)
version (absent, was only in the description) "0.2.8"
description with HTML <br> clean text
licence (absent) "MIT"
fullname/author/available_for/icon present unchanged
  • __init__.py (was empty): now contains the architecture docstring and __version__ = "0.2.8".
  • Brought into the package: LICENSE, VERSION, README.md, bal_resources.py, and the wallet_util/ folder (unchanged).

5. BUG FIX: loading as an external plugin (.zip)

During testing on Electrum 4.7.2 portable for Windows, two real problems emerged when loading the plugin as an external plugin from .zip:

Bug 5a — ModuleNotFoundError: No module named 'electrum_external_plugins'

  • Cause: Electrum loads external plugins from zip under the synthetic package electrum_external_plugins.bal, and runs only the package __init__ and the qt module. It does not register the synthetic root package nor the nested sub-packages (gui, gui.qt). A simple from .gui.qt.plugin import Plugin fails when walking up to the missing parents.
  • Fix: qt.py is now a resilient shim that (1) detects its own package name at runtime (__package__), (2) rebuilds in sys.modules any missing parent packages, (3) imports Plugin with importlib.import_module. It works both as an internal plugin (electrum.plugins.bal) and as an external one (electrum_external_plugins.bal).

Bug 5b — zlib.error: Error -5 ... incomplete or truncated stream

  • Cause: some Electrum portable builds on Windows fail to decompress with zipimport archives that contain directory entries or non-standard compression.
  • Fix: added build_zip.py, which generates a "zipimport-friendly" archive: files only (no directory entries), standard DEFLATE, deterministic ordering (reproducible SHA-256), excluding __pycache__/*.pyc. It also prints the SHA-256 hash to verify the integrity of the download.

6. Tests added

  • tests/smoke_test.py — checks imports + basic behaviour (BalTimestamp, Util helpers, HEIR_* constants, WillItem states, Plugin hooks).
  • tests/external_zip_test.pyfaithfully reproduces Electrum's loading sequence for an external plugin from zip (regression test for Bug 5a/5b).

All tests pass under Electrum 4.7.2 + PyQt6.


7. Summary: what did NOT change

  • The transaction-building logic (heirs.py, will.py).
  • The values and types of json_db.register_dict(...).
  • The status colour codes (only moved to theme.py).
  • The algorithm of all the GUI classes (copied verbatim).
  • The format of the data saved in the wallet.

8. Notes / recommendations

  • The plugin requires Electrum 4.7.2: json_db.register_dict was removed in later versions (master), where it should be replaced with stored_dict.register_name. Consider an update if you want to support a more recent Electrum.
  • Before release, an end-to-end test in a real Electrum session is recommended (preferably on testnet), in addition to the smoke tests.

9. GUI FIXES — windows and lifecycle (B1-B10)

After the structural refactoring, ten graphical and lifecycle defects of the windows were fixed, all already present in the original code. The business logic remained byte-identical (no changes to bal/core/*): only the presentation, parent, modality, z-order, lifecycle and cleanup of the Qt windows changed.

Symptoms reported by the user, now resolved:

  • (S1) the plugin windows disappeared behind the Electrum window;
  • (S2) some mechanisms worked only after closing and restarting Electrum.
ID Problem (present in the original) Applied fix
B1 self.parent = parent overrode Qt's parent() method, breaking the window hierarchy renamed to self._bal_parent (in dialogs.py, lists.py, widgets.py); the real parent comes from top_level_of(parent)
B2 dialogs opened with non-modal .show() → ended up under the main window replaced with show_on_top() / show_modal() and the correct parent
B3 message "Please restart Electrum to activate the BAL plugin": the plugin activated only after a restart hot initialization with _setup_window() that replicates load_wallet — no more restart
B4 the windows-dictionary key used the winId method instead of its value stable _window_key() key based on id(window)
B5 on_close swallowed all errors with except: pass rewritten: no except:pass, logging at every step, clean state reset
B6 BalBlockingWaitingDialog blocked the GUI thread (processEvents commented out) restored processEvents() → responsive GUI during the wait
B7 closeEvent/hideEvent with the thread cleanup commented out explicit handling of closeEvent/hideEvent + call to super()
B8 incomplete closeEvent in some dialogs uniform handling of the closing state
B9 show()+raise_() without activateWindow() nor modality → window not in the foreground bring_to_front() = raise_() + activateWindow()
B10 fragile multi-wallet / multi-window handling; menu looked up by the &Tools title use of the official window.tools_menu API

New module: gui/qt/window_utils.py (119 lines)

The window-management helpers were centralized in a single module, so that the same logic is not duplicated across the various dialogs:

  • top_level_of(widget) — walks up to the correct top-level window to use as parent;
  • bring_to_front(window)raise_() + activateWindow() to bring to the foreground;
  • stop_thread(thread) — safe stop+wait of a TaskThread;
  • show_modal(dialog) — correct modal opening (exec());
  • show_on_top(window) — non-modal opening but above the other windows.

gui/qt/common.py imports these helpers and makes them available to the rest of the GUI.


10. BUG FIX: will-executor list download

After installing the package with the GUI fixes, the user reported that the will-executor "download list" command no longer downloaded the list.

Investigation

The network code (core/willexecutors.py: send_request, handle_response, download_list, initialize_willexecutor) was compared line by line with the original Gitea version and turned out to be byte-identical (the only difference is the extra welist_server parameter in download_list, which is backward-compatible).

During the investigation, two real defects introduced by the GUI fixes were nevertheless found and corrected, which could "lose" the download result:

  1. BalDialog.closeEvent/hideEvent stopped the TaskThread. In Electrum, TaskThread.on_done runs cb_done (i.e. self.accept, which closes the dialog) before cb_result (i.e. on_success, which updates the list). Stopping the thread when the dialog closed therefore discarded the result that had just been downloaded. → The two methods were restored so they do not stop the thread (with an explanatory comment in the code).
  2. BalWaitingDialog.exe() used the wrong modality (show_modal / WindowModal). → Restored the original self.exec(), first adding bring_to_front(self) to guarantee the foreground.

In addition, the button and wizard paths (which previously downloaded in different ways and with different messages) were unified into a single helper fetch_will_executors_list, run inside the TaskThread worker.

Real cause of the failed download: environmental, NOT the plugin

A control probe with urllib that completely bypassed Electrum also failed with WinError 10054 ("connection forcibly closed by remote host"): a sign that the user's network/ISP was resetting the HTTPS connection to welist.bitcoin-after.life. The definitive confirmation: with a VPN enabled the download succeeded.

The original "seemed" to work because it ships anyway a built-in default will-executor (https://we.bitcoin-after.life), so the list never appeared completely empty even without a successful download.

Final cleanup (chosen by the user — "Option 1")

  • Non-blocking waiting window kept (BalWaitingDialog), so the GUI does not freeze during the download.

  • URL fallback: first the configured URL (WELIST_SERVER), then the hardcoded https://welist.bitcoin-after.life/.

  • Detailed diagnostics moved to the logs only (removed the urllib probe from the interface).

  • Simple error message for the user, in English (DOWNLOAD_FAILED_MESSAGE):

    "Could not download the will-executors list. This is usually caused by your internet connection or a firewall, not by the plugin. Please check your connection (a VPN often helps) and try again."

Files touched (presentation/GUI only, logic unchanged)

  • gui/qt/window.py — shared helper fetch_will_executors_list, download_list with TaskThread + BalWaitingDialog, constant DOWNLOAD_FAILED_MESSAGE.
  • gui/qt/lists.pyWillExecutorWidget.download_list routed onto the shared path with an on_success that updates/saves the list.
  • gui/qt/dialogs.pyBalDialog.closeEvent/hideEvent no longer stop the thread; BalWaitingDialog.exe() goes back to self.exec() + bring_to_front.
  • tests/gui_fixes_test.pyregression assertion: verifies that closeEvent/hideEvent do not contain stop_thread (so as not to reintroduce the bug that discarded the download).

11. Final structural comparison (original Gitea → refactor)

.py file count (excluding generated folders):

Original (Gitea) lines Refactor (bal/) lines
__init__.py 1 __init__.py 37
bal.py 161 core/plugin_base.py 351
util.py 1051 core/util.py 614
heirs.py 792 core/heirs.py 806
will.py 903 core/will.py 938
willexecutors.py 547 core/willexecutors.py 390
qt.py (GUI monolith) 3777 split into gui/qt/* (see below)
bal_resources.py 14 bal_resources.py 14
wallet_util/*.py 275 wallet_util/*.py (unchanged) 280

Split of the old qt.py (3777 lines) into the GUI modules:

Refactor module lines Content
gui/qt/plugin.py 303 Plugin class (@hook Electrum → GUI)
gui/qt/window.py 1048 BalWindow (per-wallet controller)
gui/qt/dialogs.py 1155 dialog windows + wizard
gui/qt/lists.py 964 tree views (heirs/preview/executor)
gui/qt/widgets.py 782 "leaf" widgets
gui/qt/common.py 157 shared imports + helpers
gui/qt/window_utils.py 119 window helpers (NEW — see §9)
gui/qt/calendar.py 80 BalCalendar
gui/qt/theme.py 59 status → colour mapping
gui/qt/__init__.py 17 GUI package init

The differences in the line counts compared to the original come from: reformatting/comments, separation of imports per module, and the movement of functions between util.py/bal.py and the new modules. The algorithms were not modified.


12. Change history on GitHub

  • 4198a51 — initial import of the structural refactor (v0.2.8): separation of core/ (logic) vs gui/qt/ (presentation), compliant packaging, external-zip load fix, smoke test (sections §1-§8).
  • d56fa36 — this refactoring changelog (in Italian).
  • 4806997GUI_DIAGNOSIS.md (originally DIAGNOSI_GUI.md): diagnosis of the z-order and lifecycle GUI bugs (Phase A).
  • dd6f677 (PR #2, squash) — GUI fixes B1-B10 + will-executor list download fix + window_utils.py + regression test (sections §9-§10).
  • PR #3 — fix for the OverflowError on Windows (year 2038) that broke the Will/Heirs tabs and the menu entry (section §13).

13. BUG FIX: OverflowError on Windows (year-2038 limit)

Symptom (Windows 11)

After restarting Electrum or switching wallet, the Will and Heirs tabs disappeared and a condensed/illegible menu entry (overlapping icon + text) appeared under the Electrum logo, next to Wallets. On Linux the problem did not occur.

Real cause (from the user's Electrum log)

OverflowError: Python int too large to convert to C int
  window.py __init__ -> create_heirs_tab -> WillSettingsWidget
  -> on_locktime_change -> BalTimestamp.to_date
  -> datetime.fromtimestamp(NLOCKTIME_MAX)
  • NLOCKTIME_MAX = 2**32 - 1 = 4294967295 is used as the default/sentinel locktime.
  • On Windows time_t is 32-bit, so datetime.fromtimestamp(ts) raises OverflowError for any timestamp beyond 2038.
  • On 64-bit Linux the same call works: that is why the bug was visible only on Windows and the Linux tests did not catch it.
  • The exception interrupted BalWindow.__init__ during init_menubar / load_wallet, leaving the Will/Heirs tabs and the menu entry half-built → the condensed/illegible graphical element under the logo.

Note: the first two correction attempts (a no-op status bar and the idempotency of init_menubar_tools) did not hit the cause; they were kept anyway because they are harmless and slightly improving, but the real culprit was this upstream crash.

Fix (behaviour unchanged for all normal values)

  • BalTimestamp._safe_fromtimestamp(): datetime.fromtimestamp with a clamp to INT32_MAX (year 2038) on OverflowError/OSError/ ValueError, exactly like the original's get_max_allowed_timestamp() function (workaround for Electrum issue #6170).
  • Used in to_date / to_timestamp / __str__ / __repr__ of BalTimestamp.
  • gui/qt/widgets.py (set_value): uses the safe converter.
  • core/util.py (timestamp_minus): same inline protection with a clamp to INT32_MAX.

Values within 2038 (normal absolute dates, relative durations such as 90d/5y) produce exactly the same result as before.

Test

  • tests/windows_overflow_test.py reproduces the Windows 32-bit limit (monkeypatch of datetime.fromtimestamp) and proves that without the fix you get the same OverflowError as in the log, while with the fix it passes. It was also verified that the test fails without the fix.

Confirmed by the user: "yes, it works now".

14. NEW FEATURE: automatic invalidation when postponing the inheritance

Problem

An inheritance transaction is signed with a fixed, immutable locktime and sent to the will-executors, who are economically incentivized to broadcast it (they collect the fees). If the user, after signing/sending, postpones the delivery date (e.g. by one year), the old already-signed transaction remains valid on the will-executors' servers. Since it has the lower locktime, a will-executor could broadcast it as soon as it expires, executing the inheritance earlier than the user's new intent. The previous version did not handle this case: postponing produced no action at all.

Solution (Strategy B — explicit on-chain invalidation)

When postponing an inheritance that is already signed and/or sent (state COMPLETE or PUSHED), the plugin asks to invalidate the funds on-chain before rebuilding the new inheritance. The invalidation spends the same UTXOs to a new change address with locktime = current height (RBF), so it is broadcastable immediately: once confirmed, the old pre-signed transaction becomes permanently unusable, winning the race against any will-executor.

Technical details

  • core/will.py:
    • new exception WillPostponedException (subclass of NotCompleteWillException);
    • check_willexecutors_and_heirs: the locktime comparison no longer uses the stored heir entry (their[2]), which is updated in memory together with the new value at the moment of postponement and would therefore always look equal. It now compares the requested locktime with w.tx.locktime, i.e. the locktime frozen in the signed transaction (immutable, and the one the will-executors hold). Three cases: unchanged → coherent; new > tx on a signed/sent will → WillPostponedException; new > tx on a will never sent → simple rebuild (no on-chain fee).
  • gui/qt/dialogs.py (BalBuildWillDialog.task_phase1, the real path used by Tools → Prepare): added the except WillPostponedException branch before NotCompleteWillException; it behaves like the "expired will" case and returns (None, tx) to trigger signing + broadcasting of the invalidation. The user presses Prepare again to rebuild, re-sign and re-send the new inheritance (two explicit steps, for greater control).
  • gui/qt/window.py (build_inheritance_transaction): added the same branch for completeness of the alternative path, with an explanatory message.
  • gui/qt/common.py: WillPostponedException exported.

NEW "Server" COLUMN in the transactions list

To give the user constant visibility on the online status of their inheritance transactions, a dedicated "Server" column was added in PreviewList (gui/qt/lists.py), with an always-readable label (Confirmed on server, Sent (not checked), Send failed, Not on server, Signed (not sent), Not sent) and a tooltip with the will-executor URL and status. The functions server_status_text() and server_status_tooltip() are in gui/qt/theme.py and reuse the same already-existing status flags.

Test

  • The 182 official tests keep passing; smoke test and external-zip test OK; ruff with no new real warnings.
  • Verified against the real data from the user's log: postponing a signed inheritance now correctly detects the condition and starts the invalidation.

Confirmed by the user: "it seems to work".

15. ATTEMPT AND REVERT: fix for double invalidation on postpone (v0.3.1 -> v0.3.2)

v0.3.1 (WITHDRAWN)

To solve the double signing of the invalidation on postpone, Will.mark_invalidated_by_tx() had been introduced, called in loop_broadcast_invalidating after broadcasting the invalidation, to mark as INVALIDATED the wills that spent the same UTXOs as the invalidation tx and to persist the state with save_willitems.

Why it was withdrawn

The change introduced a serious regression reported by the user: the inheritance list still showed the old inheritances and the update of heirs/dates was inconsistent.

Cause: loop_broadcast_invalidating is the broadcast point used for ALL types of invalidation (postpone, CheckAlive, expired/anticipated will), not only for postpone. In addition, the method marked and persisted the INVALIDATED state on all the will items that shared the wallet's UTXOs (typically all of them). These invalidated will items then stayed in memory and on disk, polluting the rebuild of heirs/dates and leaving old entries in the list.

v0.3.2 (this version): full REVERT

  • Removed Will.mark_invalidated_by_tx() from core/will.py.
  • Removed the call in gui/qt/dialogs.py (loop_broadcast_invalidating): the method goes back to being identical to v0.3.0.
  • Removed the two related tests; kept only the hierarchy assertion on WillPostponedException (correct and independent).
  • core/will.py and gui/qt/dialogs.py are now byte-identical to the working v0.3.0 (verified with git diff a394cde).

The double-invalidation-on-postpone bug therefore remains open and will have to be tackled in a more targeted way (without touching the common broadcast path and without persisting states on wills that share the UTXOs), subject to the user's confirmation. The priority was to restore the correct behaviour of the list/heirs/dates.

16. Missed updates, consistent Check/Close, and UI polish (v0.3.2)

FIX 1 - Removal of an heir detected on Check / Electrum close

core/will.py (check_willexecutors_and_heirs): previously the plugin detected only the addition of an heir (raising HeirNotFoundException when a current heir was no longer in the will). The opposite case was missing: the removal of an heir. Added the else branch that raises HeirNotFoundException also when the will still carries an heir that is no longer present in the current heir set. This way the inheritance rebuild is triggered on Check and on Electrum close (both use the same BalBuildWillDialog.build_will_task() path), as decided by the user: no automatic update after the change, only a manual one via Check / on close.

FIX 2 - Check also queries the servers for already-sent wills

core/will.py (new Will.needs_server_check(w)) and gui/qt/lists.py (PreviewList.check): previously Check queried the servers only for wills in the PUSHED state. Wills already sent but left at "New / Not sent" were not re-checked ("nothing to do"). Now needs_server_check includes every VALID will with a will-executor that is not yet CHECKED, even if not in the PUSHED state. The same check is used both by the Check button and by on_close.

FIX 3 - Hide invalidated/replaced from the Settings window updated the list

core/plugin_base.py (new sync_hide_filters()) and gui/qt/window.py (update_all): the "Hide Replaced" / "Hide Invalidated" checkboxes in the Settings window write the config directly (BalConfig.set) without touching the cached flags _hide_invalidated / _hide_replaced that the list uses to filter. Result: the list kept filtering with the old value until Electrum was restarted. Now update_all() calls sync_hide_filters(), which re-reads the flags from the config, so whatever the source of the change (toolbar or Settings window) the list updates immediately.

UI polish - Bold results in the "Building Will" dialog

gui/qt/dialogs.py (BalBuildWillDialog): the results shown to the right of each status line (e.g. Ok, Ko, Nothing to do, Skipped, Wait, Timeout) are now rendered in bold, keeping their colours (green/red/yellow). The status labels on the left stay in normal weight. The change is centralized in the helpers msg_ok, msg_error, msg_warning, msg_set_status, plus the will-executor lines (push and check) that now show Ok/Ko and True/False in bold + colour (green/red).

Test

  • 186 official tests pass; smoke test, external-zip test and the update-flow simulation (tests/sim_update_flows.py) OK; ruff with no new real warnings (only pre-existing false positives from star-imports).
  • Added tests in tests/test_core_will.py: test_check_heirs_unchanged_is_coherent, test_check_heir_removed_triggers_rebuild, test_check_heir_added_triggers_rebuild, test_needs_server_check.

Confirmed by the user against real data: after Sign -> Broadcast -> Check the already-sent transactions turned green again ("confirmed on server"); the list goes back clean; the bold rendering and the hide-flag update work.

17. UI polish and bug fix — signed-tx colour, wizard, Building Will dialog (v0.3.3)

This session groups several behaviour-invariant UI refinements plus one colour bug fix. All comments and code remain in English; only the chat with the author was in Italian.

FIX — Signed-but-not-sent transaction shown RED instead of blue

core/will.py (needs_server_check): a previous-session change (section 16, "FIX 2") had removed the PUSHED requirement from needs_server_check, so a will that was signed but never broadcast was still server-queried. The query returned CHECK_FAIL, and because status_color() checks CHECK_FAIL (red, #e83845) before COMPLETE (blue, #2bc8ed), the row turned red. Restored the original Gitea check() condition by adding back and w.get_status("PUSHED"), so only already-broadcast wills are server-checked. A signed-but-not-sent will now stays blue (COMPLETE) as in the original.

  • tests/test_core_will.py (test_needs_server_check): a freshly-built item (VALID, not PUSHED) now correctly expects False.

Wizard "Will Settings" — equal-width, left-aligned rows

gui/qt/widgets.py (WillSettingsWidget, vertical layout): the calendar button and the fee field used to stretch to the dialog's right edge, far wider than the date rows. Now every row is capped to the widest date-row width (row_w) and left-aligned, so they form a tidy column. The leading icons keep their original HelpButton width (icon_w is used only as a spacer in front of the calendar, never to widen the icons themselves).

Wizard button — icon + text

gui/qt/lists.py (create_toolbar): the "build your will" toolbar button is now more inviting: a 28×28 wizard icon plus a bold "Create your will" caption, setMinimumHeight(40). gui/qt/common.py gained QSize in the QtCore import.

Building Will dialog — clearer final report + manual Close

gui/qt/dialogs.py (BalBuildWillDialog):

  • The closing summary line is no longer a bare "Ok": it now has an explicit left-side label, "All done: Ok", like the other result rows.
  • A blank separator row is inserted above "All done" so the overall outcome is visually detached from the per-step rows.
  • The four "checking variables" status strings are capitalised to "Checking variables" to match the rows below; the redundant trailing colon on the final one was dropped (msg_set_status already adds ":\t").
  • The final auto-closing countdown (self.wait(5)) was replaced by an explicit right-aligned "Close" button (_add_close_button / _on_close_clicked). The dialog now stays open until the user dismisses it, so the full report can be read at leisure. The intermediate technical pauses (wait(10), wait(5), wait(3)) are kept. Closing still shows the persistent "next steps" (Sign / Broadcast) popup when self._next_steps_hint is set.

Preview helpers (dev-only, not shipped logic)

tests/preview_wizard_settings_align.py, tests/preview_wizard_button.py, tests/preview_building_will_close_btn.py: small offscreen scripts used to render before/after mock-ups for visual approval.

Test

  • 186 official tests pass; smoke test, external-zip test OK.
  • ruff reports only the pre-existing baseline false positives (F401/F403/F405 star-import re-exports, one F841, one F541) — no new issues.
  • Version bumped to 0.3.3 (bal/VERSION, bal/__init__.py, bal/manifest.json).

18. Documentation site (docs/) — no behaviour change

Added a docs/ tree that renders directly on GitHub and GitHub Pages (no PDF):

  • docs/inheritance-options.md + .html: a codeaccurate Inheritance Options Guide covering every change a user can make (date earlier/later, add/remove heir, change percentages, fees, willexecutors), each transaction status flag and its colour, and what happens on the willexecutor servers. Includes a decision flow chart (GitHubnative Mermaid block in the .md, plus a static SVG fallback docs/images/inheritance-flow.svg, plus a live Mermaid render in the .html). Behaviour is derived directly from core/will.py::is_will_valid / check_willexecutors_and_heirs and gui/qt/window.py::build_inheritance_transaction.
  • docs/manual/README.md + manual.html + images/: the official BAL User Manual (revB) converted from the upstream Gitea PDF into GitHubfriendly Markdown/HTML with the original screenshots rerendered at high resolution (docs/manual/images/fig*.png, logo.png).
  • docs/README.md: documentation index linking both documents.

No plugin code changed; 186 tests still pass.