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).
This commit is contained in:
2026-06-28 23:01:47 -04:00
parent dd2e160a17
commit c963f61424
5 changed files with 745 additions and 719 deletions

View File

@@ -678,3 +678,34 @@ change takes effect immediately (the method is already re-run from
**Outcome:** DONE (delivered together with fix #10 in a single ZIP for user **Outcome:** DONE (delivered together with fix #10 in a single ZIP for user
testing before commit). testing before commit).
## 12. Translated the refactoring docs to English and renamed two of them
**Request:** per rule R1 (all output, including documentation, must be in
English), translate the remaining Italian documentation files. The user also
asked to translate the file *names* of two of them.
**What changed:**
- `CHANGELOG_REFACTOR.md`
- Translated sections 1-16 (the whole report header plus §1-§16) from Italian
to English. Sections 17-18 were already in English and were left untouched.
- All technical content was preserved verbatim: code blocks, commit hashes,
tables, file/class/method names, version numbers and structure.
- Updated the §12 history line to mention the new name of the diagnosis file
(`GUI_DIAGNOSIS.md`, originally `DIAGNOSI_GUI.md`).
- `DIAGNOSI_GUI.md` → renamed to `GUI_DIAGNOSIS.md` (via `git mv`) and fully
translated to English, **title included**. All code blocks, line references,
severity emojis, tables and structure were preserved.
- `REPORT_NETWORKING_PARALLELO.md` → renamed to `PARALLEL_NETWORKING_REPORT.md`
(via `git mv`). Its content was already in English, so only the file name was
translated.
**Verification:**
- Scanned the translated regions for leftover Italian words: none found (only
English false-positives from the regex).
- Confirmed sections 17-18 of `CHANGELOG_REFACTOR.md` are unchanged
(the diff hunks stop before §17).
- Confirmed no other file in the repo still references the old file names.
**Outcome:** DONE (documentation-only change; no plugin code touched, so the
zip-first step does not apply).

View File

@@ -1,66 +1,66 @@
# BAL — Resoconto del refactoring (per l'autore originale) # BAL — Refactoring report (for the original author)
Questo documento elenca **tutte** le modifiche apportate al plugin BAL This document lists **all** the changes made to the BAL plugin
(Bitcoin After Life) rispetto alla versione originale `0.2.8`. (Bitcoin After Life) with respect to the original version `0.2.8`.
**Principio guida:** refactoring **conservativo e a comportamento invariato** **Guiding principle:** **conservative, behaviour-preserving** refactoring
(Approccio A). La logica di business è stata mantenuta **byte-identica** dove (Approach A). The business logic was kept **byte-identical** where possible;
possibile; sono cambiati soprattutto la **disposizione dei file** e gli what changed are mainly the **file layout** and the **imports**. No algorithmic
**import**. Nessuna riscrittura algoritmica. rewrite.
Ambiente di verifica: **Electrum 4.7.2** + **PyQt6** (l'ultima release stabile Verification environment: **Electrum 4.7.2** + **PyQt6** (the latest stable
che espone `json_db.register_dict`). release that exposes `json_db.register_dict`).
--- ---
## 1. Riorganizzazione della struttura (separazione logica / GUI) ## 1. Structure reorganization (logic / GUI separation)
Il problema principale segnalato era che la logica e la grafica erano The main reported problem was that logic and graphics were mixed together, in
mescolate, in particolare in un unico file `qt.py` da **4131 righe**. particular in a single `qt.py` file of **4131 lines**.
### Struttura PRIMA (flat, 7 file) ### Structure BEFORE (flat, 7 files)
``` ```
BAL/ BAL/
├── __init__.py (vuoto, 0 righe) ├── __init__.py (empty, 0 lines)
├── bal.py (243) logica + plugin base ├── bal.py (243) logic + plugin base
├── util.py (533) helper ├── util.py (533) helpers
├── heirs.py (791) modello eredi + costruzione tx ├── heirs.py (791) heirs model + tx building
├── will.py (927) modello will/WillItem ├── will.py (927) will/WillItem model
├── willexecutors.py (374) networking will-executor ├── willexecutors.py (374) will-executor networking
├── qt.py (4131) TUTTA la GUI + il Plugin in un solo file ├── qt.py (4131) ALL the GUI + the Plugin in a single file
└── bal_resources.py (14) └── bal_resources.py (14)
``` ```
### Struttura DOPO (core/ vs gui/) ### Structure AFTER (core/ vs gui/)
``` ```
bal/ bal/
├── manifest.json metadati conformi allo standard ├── manifest.json standard-compliant metadata
├── qt.py shim di caricamento (re-export di Plugin) ├── qt.py loading shim (re-export of Plugin)
├── __init__.py docstring di architettura + __version__ ├── __init__.py architecture docstring + __version__
├── core/ LOGICA senza dipendenze Qt ├── core/ LOGIC with no Qt dependencies
│ ├── util.py (ex util.py) │ ├── util.py (was util.py)
│ ├── plugin_base.py (ex bal.py) │ ├── plugin_base.py (was bal.py)
│ ├── heirs.py (ex heirs.py) │ ├── heirs.py (was heirs.py)
│ ├── will.py (ex will.py) │ ├── will.py (was will.py)
│ └── willexecutors.py (ex willexecutors.py) │ └── willexecutors.py (was willexecutors.py)
└── gui/qt/ PRESENTAZIONE PyQt6 └── gui/qt/ PyQt6 PRESENTATION
├── theme.py (59) mappatura stato → colore ├── theme.py (59) status → colour mapping
├── common.py (155) import condivisi + helper GUI ├── common.py (155) shared imports + GUI helpers
├── widgets.py (782) widget "foglia" ├── widgets.py (782) "leaf" widgets
├── calendar.py (80) BalCalendar ├── calendar.py (80) BalCalendar
├── dialogs.py (1127) finestre di dialogo ├── dialogs.py (1127) dialog windows
├── lists.py (957) viste ad albero (eredi/preview/executor) ├── lists.py (957) tree views (heirs/preview/executor)
├── window.py (952) controller GUI per-wallet (BalWindow) ├── window.py (952) per-wallet GUI controller (BalWindow)
└── plugin.py (273) classe Plugin (@hook Electrum → GUI) └── plugin.py (273) Plugin class (@hook Electrum → GUI)
``` ```
Il file `qt.py` da 4131 righe è stato suddiviso per **responsabilità**. I The 4131-line `qt.py` file was split by **responsibility**. The **class bodies
**corpi delle classi sono stati copiati verbatim** (riga per riga) per non were copied verbatim** (line by line) so as not to touch the delicate
toccare la logica delicata delle transazioni di eredità. inheritance-transaction logic.
### Mappa: dove sono finite le 40 classi/funzioni di `qt.py` ### Map: where the 40 classes/functions of `qt.py` ended up
| Classe/funzione (riga orig.) | Nuovo modulo | | Class/function (orig. line) | New module |
|-------------------------------------|-------------------------| |-------------------------------------|-------------------------|
| `Plugin` (67) | `gui/qt/plugin.py` | | `Plugin` (67) | `gui/qt/plugin.py` |
| `shown_cv` (317) | `gui/qt/common.py` | | `shown_cv` (317) | `gui/qt/common.py` |
@@ -104,257 +104,255 @@ toccare la logica delicata delle transazioni di eredità.
--- ---
## 2. Rimozioni (codice morto / debug) — comportamento invariato ## 2. Removals (dead / debug code) — behaviour unchanged
Tutte le rimozioni seguenti sono state verificate come **non utilizzate** o All the following removals were verified as **unused** or **purely debug**, so
**puramente di debug**, quindi non alterano il comportamento del plugin. they do not alter the plugin's behaviour.
1. **`util.py``core/util.py`**: rimossi tre helper di debug usati solo per 1. **`util.py``core/util.py`**: removed three debug helpers used only for
stampe a console: console printing:
- `print_var()` (orig. riga 439) - `print_var()` (orig. line 439)
- `print_utxo()` (orig. riga 474) - `print_utxo()` (orig. line 474)
- `print_prevout()` (orig. riga 486) - `print_prevout()` (orig. line 486)
2. **`bal.py``core/plugin_base.py`**: rimossa la funzione **stub vuota** 2. **`bal.py``core/plugin_base.py`**: removed the **empty stub** function
`get_will_settings(x)` (orig. righe 12-14): `get_will_settings(x)` (orig. lines 12-14):
```python ```python
def get_will_settings(x): def get_will_settings(x):
# print(x) # print(x)
pass pass
``` ```
⚠️ Verificato: **non era riferita da nessun `register_dict`** — i tre ⚠️ Verified: **it was not referenced by any `register_dict`** — the three
`register_dict` usano `tuple`, `dict`, `lambda x: x`. Quindi era codice `register_dict` calls use `tuple`, `dict`, `lambda x: x`. So it was dead
morto. La funzione **usata** `get_will(x)` è stata mantenuta identica. code. The **used** function `get_will(x)` was kept identical.
3. **`will.py` (`WillItem`) → spostato in `gui/qt/theme.py`**: il metodo 3. **`will.py` (`WillItem`) → moved to `gui/qt/theme.py`**: the method
`WillItem.get_color()` (orig. riga 852) restituiva colori esadecimali `WillItem.get_color()` (orig. line 852) returned hexadecimal colours
è logica di **presentazione**, non di dominio. È stato spostato fuori dal it is **presentation** logic, not domain logic. It was moved out of the
modello e trasformato nella funzione `status_color(will_item)` in model and turned into the function `status_color(will_item)` in
`gui/qt/theme.py`. **Verificato byte-identico** su tutte le combinazioni di `gui/qt/theme.py`. **Verified byte-identical** across all status
stato (stessa catena di `get_status(...)`, stessi codici colore). combinations (same `get_status(...)` chain, same colour codes).
--- ---
## 3. Cambi di import (necessari per la nuova struttura) ## 3. Import changes (required by the new structure)
Gli import sono stati aggiornati da "flat" a "a package". Esempi: The imports were updated from "flat" to "package" style. Examples:
| Prima | Dopo | | Before | After |
|------------------------------------|---------------------------------------| |------------------------------------|---------------------------------------|
| `from .bal import BalPlugin` | `from .plugin_base import BalPlugin` (in willexecutors) | | `from .bal import BalPlugin` | `from .plugin_base import BalPlugin` (in willexecutors) |
| `from .util import Util` | `from .util import Util` (invariato, ora dentro core/) | | `from .util import Util` | `from .util import Util` (unchanged, now inside core/) |
| (in qt.py) `from .bal import ...` | i moduli GUI importano da `...core.X` | | (in qt.py) `from .bal import ...` | the GUI modules import from `...core.X` |
- Aggiunti `from .common import _, _logger` nei moduli GUI, perché `import *` - Added `from .common import _, _logger` in the GUI modules, because `import *`
**non** esporta i nomi che iniziano con underscore. does **not** export names starting with an underscore.
- Aggiunti 3 import "lazy" (dentro le funzioni) in `dialogs.py` per spezzare il - Added 3 "lazy" imports (inside the functions) in `dialogs.py` to break the
ciclo `dialogs ↔ lists` (lists importa `BalBuildWillDialog` da dialogs). `dialogs ↔ lists` cycle (lists imports `BalBuildWillDialog` from dialogs).
La **logica interna dei metodi** non è stata toccata: `prepare_transactions()`, The **internal logic of the methods** was not touched: `prepare_transactions()`,
`buildTransactions()` ecc. sono verbatim. `buildTransactions()`, etc. are verbatim.
--- ---
## 4. Packaging conforme allo standard Electrum ## 4. Electrum-standard packaging
`manifest.json` reso conforme a https://plugins.electrum.org/developers.html : `manifest.json` made compliant with https://plugins.electrum.org/developers.html :
| Campo | Prima | Dopo | | Field | Before | After |
|------------------|--------------------|-------------------------------| |------------------|--------------------|-------------------------------|
| `name` | `"BAL"` | `"bal"` (minuscolo = nome dir) | | `name` | `"BAL"` | `"bal"` (lowercase = dir name) |
| `version` | (assente, era solo nella description) | `"0.2.8"` | | `version` | (absent, was only in the description) | `"0.2.8"` |
| `description` | con `<br>` HTML | testo pulito | | `description` | with HTML `<br>` | clean text |
| `licence` | (assente) | `"MIT"` | | `licence` | (absent) | `"MIT"` |
| `fullname`/`author`/`available_for`/`icon` | presenti | invariati | | `fullname`/`author`/`available_for`/`icon` | present | unchanged |
- `__init__.py` (era **vuoto**): ora contiene la docstring di architettura e - `__init__.py` (was **empty**): now contains the architecture docstring and
`__version__ = "0.2.8"`. `__version__ = "0.2.8"`.
- Portati nel package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`, - Brought into the package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`,
e la cartella `wallet_util/` (invariata). and the `wallet_util/` folder (unchanged).
--- ---
## 5. CORREZIONE BUG: caricamento come plugin esterno (.zip) ## 5. BUG FIX: loading as an external plugin (.zip)
Durante i test su **Electrum 4.7.2 portable per Windows** sono emersi due During testing on **Electrum 4.7.2 portable for Windows**, two real problems
problemi reali nel caricare il plugin come **plugin esterno da .zip**: emerged when loading the plugin as an **external plugin from .zip**:
### Bug 5a — `ModuleNotFoundError: No module named 'electrum_external_plugins'` ### Bug 5a — `ModuleNotFoundError: No module named 'electrum_external_plugins'`
- **Causa:** Electrum carica i plugin esterni da zip sotto il package sintetico - **Cause:** Electrum loads external plugins from zip under the synthetic
`electrum_external_plugins.bal`, ed esegue **solo** l'`__init__` del package e package `electrum_external_plugins.bal`, and runs **only** the package
il modulo `qt`. Non registra il package radice sintetico né i sotto-package `__init__` and the `qt` module. It does not register the synthetic root
annidati (`gui`, `gui.qt`). Un semplice `from .gui.qt.plugin import Plugin` package nor the nested sub-packages (`gui`, `gui.qt`). A simple
fallisce risalendo ai parent mancanti. `from .gui.qt.plugin import Plugin` fails when walking up to the missing
- **Fix:** `qt.py` ora è uno shim resiliente che (1) rileva a runtime il proprio parents.
nome di package (`__package__`), (2) ricostruisce in `sys.modules` gli - **Fix:** `qt.py` is now a resilient shim that (1) detects its own package
eventuali package padre mancanti, (3) importa `Plugin` con name at runtime (`__package__`), (2) rebuilds in `sys.modules` any missing
`importlib.import_module`. Funziona **sia** come plugin interno parent packages, (3) imports `Plugin` with `importlib.import_module`. It
(`electrum.plugins.bal`) **sia** esterno (`electrum_external_plugins.bal`). 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` ### Bug 5b — `zlib.error: Error -5 ... incomplete or truncated stream`
- **Causa:** alcune build portable di Electrum su Windows non riescono a - **Cause:** some Electrum portable builds on Windows fail to decompress with
decomprimere con `zipimport` archivi che contengono **voci di directory** o `zipimport` archives that contain **directory entries** or non-standard
compressione non standard. compression.
- **Fix:** aggiunto `build_zip.py`, che genera un archivio "zipimport-friendly": - **Fix:** added `build_zip.py`, which generates a "zipimport-friendly"
solo file (nessuna voce di directory), DEFLATE standard, ordine deterministico archive: files only (no directory entries), standard DEFLATE, deterministic
(SHA-256 riproducibile), escludendo `__pycache__`/`*.pyc`. Stampa anche ordering (reproducible SHA-256), excluding `__pycache__`/`*.pyc`. It also
l'hash SHA-256 per verificare l'integrità del download. prints the SHA-256 hash to verify the integrity of the download.
--- ---
## 6. Test aggiunti ## 6. Tests added
- `tests/smoke_test.py` — verifica import + comportamento di base - `tests/smoke_test.py` — checks imports + basic behaviour
(`BalTimestamp`, helper di `Util`, costanti `HEIR_*`, stati di `WillItem`, (`BalTimestamp`, `Util` helpers, `HEIR_*` constants, `WillItem` states,
hook del `Plugin`). `Plugin` hooks).
- `tests/external_zip_test.py` — riproduce **fedelmente** la sequenza di - `tests/external_zip_test.py` — **faithfully** reproduces Electrum's loading
caricamento di un plugin esterno da zip di Electrum (regressione per il sequence for an external plugin from zip (regression test for Bug 5a/5b).
Bug 5a/5b).
Tutti i test passano sotto Electrum 4.7.2 + PyQt6. All tests pass under Electrum 4.7.2 + PyQt6.
--- ---
## 7. Riepilogo: cosa NON è cambiato ## 7. Summary: what did NOT change
- La logica di costruzione delle transazioni (`heirs.py`, `will.py`). - The transaction-building logic (`heirs.py`, `will.py`).
- I valori e i tipi di `json_db.register_dict(...)`. - The values and types of `json_db.register_dict(...)`.
- I codici colore degli stati (solo spostati in `theme.py`). - The status colour codes (only moved to `theme.py`).
- L'algoritmo di tutte le classi GUI (copiate verbatim). - The algorithm of all the GUI classes (copied verbatim).
- Il formato dei dati salvati nel wallet. - The format of the data saved in the wallet.
## 8. Note / raccomandazioni ## 8. Notes / recommendations
- Il plugin **richiede Electrum 4.7.2**: `json_db.register_dict` è stato - The plugin **requires Electrum 4.7.2**: `json_db.register_dict` was
**rimosso** nelle versioni successive (master), dove andrebbe sostituito con **removed** in later versions (master), where it should be replaced with
`stored_dict.register_name`. Valutare un adeguamento se si vuole supportare `stored_dict.register_name`. Consider an update if you want to support a
Electrum più recente. more recent Electrum.
- Prima del rilascio è consigliata una prova **end-to-end in una sessione - Before release, an **end-to-end test in a real Electrum session** is
Electrum reale** (preferibilmente su testnet), oltre agli smoke test. recommended (preferably on testnet), in addition to the smoke tests.
--- ---
## 9. CORREZIONI GUIfinestre e ciclo di vita (B1-B10) ## 9. GUI FIXESwindows and lifecycle (B1-B10)
Dopo il refactoring di struttura sono stati corretti **dieci difetti grafici e After the structural refactoring, **ten graphical and lifecycle defects** of
di ciclo di vita** delle finestre, già presenti nel codice originale. La logica the windows were fixed, all already present in the original code. The business
di business è rimasta **byte-identica** (nessuna modifica a `bal/core/*`): sono logic remained **byte-identical** (no changes to `bal/core/*`): only the
cambiati solo **presentazione, parent, modalità, z-order, ciclo di vita e **presentation, parent, modality, z-order, lifecycle and cleanup** of the Qt
cleanup** delle finestre Qt. windows changed.
Sintomi segnalati dall'utente, ora risolti: Symptoms reported by the user, now resolved:
- **(S1)** le finestre del plugin sparivano dietro la finestra di Electrum; - **(S1)** the plugin windows disappeared behind the Electrum window;
- **(S2)** alcuni meccanismi funzionavano solo dopo aver chiuso e riavviato - **(S2)** some mechanisms worked only after closing and restarting Electrum.
Electrum.
| ID | Problema (presente nell'originale) | Correzione applicata | | ID | Problem (present in the original) | Applied fix |
|-----|----------------------------------------------------------------------|----------------------| |-----|----------------------------------------------------------------------|----------------------|
| B1 | `self.parent = parent` sovrascriveva il metodo `parent()` di Qt, rompendo la gerarchia delle finestre | rinominato in `self._bal_parent` (in `dialogs.py`, `lists.py`, `widgets.py`); il parent reale passa da `top_level_of(parent)` | | 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 | dialoghi aperti con `.show()` non modale → finivano sotto la finestra principale | sostituiti con `show_on_top()` / `show_modal()` e parent corretto | | 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 | messaggio "Please restart Electrum to activate the BAL plugin": il plugin si attivava solo dopo riavvio | inizializzazione **a caldo** con `_setup_window()` che replica `load_wallet` — niente più riavvio | | 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 | chiave del dizionario finestre usava il **metodo** `winId` invece del valore | chiave stabile `_window_key()` basata su `id(window)` | | B4 | the windows-dictionary key used the `winId` **method** instead of its value | stable `_window_key()` key based on `id(window)` |
| B5 | `on_close` ingoiava tutti gli errori con `except: pass` | riscritto: niente `except:pass`, log per ogni passo, reset pulito dello stato | | B5 | `on_close` swallowed all errors with `except: pass` | rewritten: no `except:pass`, logging at every step, clean state reset |
| B6 | `BalBlockingWaitingDialog` bloccava il thread della GUI (`processEvents` commentato) | ripristinato `processEvents()` → GUI reattiva durante l'attesa | | B6 | `BalBlockingWaitingDialog` blocked the GUI thread (`processEvents` commented out) | restored `processEvents()` → responsive GUI during the wait |
| B7 | `closeEvent`/`hideEvent` con cleanup del thread commentato | gestione esplicita di `closeEvent`/`hideEvent` + chiamata a `super()` | | B7 | `closeEvent`/`hideEvent` with the thread cleanup commented out | explicit handling of `closeEvent`/`hideEvent` + call to `super()` |
| B8 | `closeEvent` incompleto in alcuni dialog | gestione uniforme dello stato di chiusura | | B8 | incomplete `closeEvent` in some dialogs | uniform handling of the closing state |
| B9 | `show()+raise_()` senza `activateWindow()` né modalitàfinestra non in primo piano | `bring_to_front()` = `raise_()` + `activateWindow()` | | B9 | `show()+raise_()` without `activateWindow()` nor modalitywindow not in the foreground | `bring_to_front()` = `raise_()` + `activateWindow()` |
| B10 | gestione multi-wallet / multi-finestra fragile; menu cercato per titolo `&Tools` | uso dell'API ufficiale `window.tools_menu` | | B10 | fragile multi-wallet / multi-window handling; menu looked up by the `&Tools` title | use of the official `window.tools_menu` API |
### Nuovo modulo: `gui/qt/window_utils.py` (119 righe) ### New module: `gui/qt/window_utils.py` (119 lines)
Gli helper per la gestione delle finestre sono stati **centralizzati** in un The window-management helpers were **centralized** in a single module, so that
unico modulo, così la stessa logica non viene duplicata nei vari dialog: the same logic is not duplicated across the various dialogs:
- `top_level_of(widget)` — risale alla finestra di primo livello corretta da - `top_level_of(widget)` — walks up to the correct top-level window to use as
usare come parent; parent;
- `bring_to_front(window)` — `raise_()` + `activateWindow()` per portare in - `bring_to_front(window)` — `raise_()` + `activateWindow()` to bring to the
primo piano; foreground;
- `stop_thread(thread)` — stop+wait sicuro di un `TaskThread`; - `stop_thread(thread)` — safe stop+wait of a `TaskThread`;
- `show_modal(dialog)` — apertura modale corretta (`exec()`); - `show_modal(dialog)` — correct modal opening (`exec()`);
- `show_on_top(window)` — apertura non modale ma sopra le altre finestre. - `show_on_top(window)` — non-modal opening but above the other windows.
`gui/qt/common.py` importa questi helper e li rende disponibili al resto della `gui/qt/common.py` imports these helpers and makes them available to the rest
GUI. of the GUI.
--- ---
## 10. CORREZIONE BUG: download lista will-executor ## 10. BUG FIX: will-executor list download
Dopo l'installazione del pacchetto con le correzioni GUI, l'utente ha After installing the package with the GUI fixes, the user reported that the
segnalato che il comando **"download list"** dei will-executor non scaricava will-executor **"download list"** command no longer downloaded the list.
più la lista.
### Indagine ### Investigation
Il codice di rete (`core/willexecutors.py`: `send_request`, `handle_response`, The network code (`core/willexecutors.py`: `send_request`, `handle_response`,
`download_list`, `initialize_willexecutor`) è stato confrontato riga per riga `download_list`, `initialize_willexecutor`) was compared line by line with the
con l'originale Gitea ed è risultato **byte-identico** (l'unica differenza è il original Gitea version and turned out to be **byte-identical** (the only
parametro aggiuntivo `welist_server` in `download_list`, retro-compatibile). difference is the extra `welist_server` parameter in `download_list`, which is
backward-compatible).
Durante l'indagine sono comunque emersi e stati corretti **due difetti reali** During the investigation, **two real defects** introduced by the GUI fixes were
introdotti dalle correzioni GUI, che potevano "perdere" il risultato del nevertheless found and corrected, which could "lose" the download result:
download:
1. **`BalDialog.closeEvent`/`hideEvent` fermavano il `TaskThread`.** In Electrum 1. **`BalDialog.closeEvent`/`hideEvent` stopped the `TaskThread`.** In Electrum,
`TaskThread.on_done` esegue `cb_done` (cioè `self.accept`, che **chiude** il `TaskThread.on_done` runs `cb_done` (i.e. `self.accept`, which **closes** the
dialog) **prima** di `cb_result` (cioè `on_success`, che **aggiorna** la dialog) **before** `cb_result` (i.e. `on_success`, which **updates** the
lista). Fermare il thread alla chiusura del dialog **scartava** quindi il list). Stopping the thread when the dialog closed therefore **discarded** the
risultato appena scaricato. → I due metodi sono stati riportati a **non** result that had just been downloaded. → The two methods were restored so they
fermare il thread (con commento esplicativo nel codice). do **not** stop the thread (with an explanatory comment in the code).
2. **`BalWaitingDialog.exe()` usava una modalità sbagliata** (`show_modal` / 2. **`BalWaitingDialog.exe()` used the wrong modality** (`show_modal` /
`WindowModal`). → Ripristinato l'originale `self.exec()`, aggiungendo prima `WindowModal`). → Restored the original `self.exec()`, first adding
`bring_to_front(self)` per garantire il primo piano. `bring_to_front(self)` to guarantee the foreground.
Inoltre i percorsi del **pulsante** e del **wizard** (che prima scaricavano in In addition, the **button** and **wizard** paths (which previously downloaded
modi diversi e con messaggi diversi) sono stati **unificati** in un unico in different ways and with different messages) were **unified** into a single
helper `fetch_will_executors_list`, eseguito dentro il worker del `TaskThread`. helper `fetch_will_executors_list`, run inside the `TaskThread` worker.
### Causa vera del mancato download: ambientale, NON del plugin ### Real cause of the failed download: environmental, NOT the plugin
Una probe di controllo con `urllib` che **bypassava completamente Electrum** A control probe with `urllib` that **completely bypassed Electrum** also failed
falliva ugualmente con `WinError 10054` ("connection forcibly closed by remote with `WinError 10054` ("connection forcibly closed by remote host"): a sign
host"): segno che la **rete/ISP dell'utente resettava la connessione HTTPS** that the **user's network/ISP was resetting the HTTPS connection** to
verso `welist.bitcoin-after.life`. La conferma definitiva: **attivando una VPN `welist.bitcoin-after.life`. The definitive confirmation: **with a VPN enabled
il download è andato a buon fine.** the download succeeded.**
L'originale "sembrava" funzionare perché spedisce comunque un will-executor di The original "seemed" to work because it ships anyway a **built-in default**
**default già incorporato** (`https://we.bitcoin-after.life`), quindi la lista will-executor (`https://we.bitcoin-after.life`), so the list never appeared
non risultava mai del tutto vuota anche senza un download riuscito. completely empty even without a successful download.
### Pulizia finale (scelta dall'utente — "Opzione 1") ### Final cleanup (chosen by the user — "Option 1")
- **Finestra di attesa non bloccante** mantenuta (`BalWaitingDialog`), così la - **Non-blocking waiting window** kept (`BalWaitingDialog`), so the GUI does
GUI non si congela durante il download. not freeze during the download.
- **Fallback dell'URL**: prima l'URL configurato (`WELIST_SERVER`), poi quello - **URL fallback**: first the configured URL (`WELIST_SERVER`), then the
hardcoded `https://welist.bitcoin-after.life/`. hardcoded `https://welist.bitcoin-after.life/`.
- **Diagnostica dettagliata spostata nei soli log** (rimossa la probe `urllib` - **Detailed diagnostics moved to the logs only** (removed the `urllib` probe
dall'interfaccia). from the interface).
- **Messaggio d'errore semplice per l'utente, in inglese** (`DOWNLOAD_FAILED_MESSAGE`): - **Simple error message for the user, in English** (`DOWNLOAD_FAILED_MESSAGE`):
> *"Could not download the will-executors list. This is usually caused by > *"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 internet connection or a firewall, not by the plugin. Please check
> your connection (a VPN often helps) and try again."* > your connection (a VPN often helps) and try again."*
### File toccati (solo presentazione/GUI, logica invariata) ### Files touched (presentation/GUI only, logic unchanged)
- `gui/qt/window.py` — helper condiviso `fetch_will_executors_list`, - `gui/qt/window.py` — shared helper `fetch_will_executors_list`,
`download_list` con `TaskThread` + `BalWaitingDialog`, costante `download_list` with `TaskThread` + `BalWaitingDialog`, constant
`DOWNLOAD_FAILED_MESSAGE`. `DOWNLOAD_FAILED_MESSAGE`.
- `gui/qt/lists.py` — `WillExecutorWidget.download_list` instradato sul - `gui/qt/lists.py` — `WillExecutorWidget.download_list` routed onto the shared
percorso condiviso con `on_success` che aggiorna/salva la lista. path with an `on_success` that updates/saves the list.
- `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` **non** fermano più - `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` no longer stop the
il thread; `BalWaitingDialog.exe()` torna a `self.exec()` + `bring_to_front`. thread; `BalWaitingDialog.exe()` goes back to `self.exec()` + `bring_to_front`.
- `tests/gui_fixes_test.py` — asserzione di **regressione**: verifica che - `tests/gui_fixes_test.py` — **regression** assertion: verifies that
`closeEvent`/`hideEvent` **non** contengano `stop_thread` (per non `closeEvent`/`hideEvent` do **not** contain `stop_thread` (so as not to
reintrodurre il bug che scartava il download). reintroduce the bug that discarded the download).
--- ---
## 11. Confronto strutturale finale (originale Gitea → refactor) ## 11. Final structural comparison (original Gitea → refactor)
Conteggio file `.py` (escluse cartelle generate): `.py` file count (excluding generated folders):
| Originale (Gitea) | righe | → | Refactor (`bal/`) | righe | | Original (Gitea) | lines | → | Refactor (`bal/`) | lines |
|------------------------------|------:|----|-------------------------------------------|------:| |------------------------------|------:|----|-------------------------------------------|------:|
| `__init__.py` | 1 | → | `__init__.py` | 37 | | `__init__.py` | 1 | → | `__init__.py` | 37 |
| `bal.py` | 161 | → | `core/plugin_base.py` | 351 | | `bal.py` | 161 | → | `core/plugin_base.py` | 351 |
@@ -362,56 +360,56 @@ Conteggio file `.py` (escluse cartelle generate):
| `heirs.py` | 792 | → | `core/heirs.py` | 806 | | `heirs.py` | 792 | → | `core/heirs.py` | 806 |
| `will.py` | 903 | → | `core/will.py` | 938 | | `will.py` | 903 | → | `core/will.py` | 938 |
| `willexecutors.py` | 547 | → | `core/willexecutors.py` | 390 | | `willexecutors.py` | 547 | → | `core/willexecutors.py` | 390 |
| `qt.py` (monolite GUI) | 3777 | → | suddiviso in `gui/qt/*` (vedi sotto) | — | | `qt.py` (GUI monolith) | 3777 | → | split into `gui/qt/*` (see below) | — |
| `bal_resources.py` | 14 | → | `bal_resources.py` | 14 | | `bal_resources.py` | 14 | → | `bal_resources.py` | 14 |
| `wallet_util/*.py` | 275 | → | `wallet_util/*.py` (invariati) | 280 | | `wallet_util/*.py` | 275 | → | `wallet_util/*.py` (unchanged) | 280 |
Suddivisione del vecchio `qt.py` (3777 righe) nei moduli GUI: Split of the old `qt.py` (3777 lines) into the GUI modules:
| Modulo refactor | righe | Contenuto | | Refactor module | lines | Content |
|-----------------------------|------:|-----------| |-----------------------------|------:|-----------|
| `gui/qt/plugin.py` | 303 | classe `Plugin` (`@hook` Electrum → GUI) | | `gui/qt/plugin.py` | 303 | `Plugin` class (`@hook` Electrum → GUI) |
| `gui/qt/window.py` | 1048 | `BalWindow` (controller per-wallet) | | `gui/qt/window.py` | 1048 | `BalWindow` (per-wallet controller) |
| `gui/qt/dialogs.py` | 1155 | finestre di dialogo + wizard | | `gui/qt/dialogs.py` | 1155 | dialog windows + wizard |
| `gui/qt/lists.py` | 964 | viste ad albero (eredi/preview/executor) | | `gui/qt/lists.py` | 964 | tree views (heirs/preview/executor) |
| `gui/qt/widgets.py` | 782 | widget "foglia" | | `gui/qt/widgets.py` | 782 | "leaf" widgets |
| `gui/qt/common.py` | 157 | import condivisi + helper | | `gui/qt/common.py` | 157 | shared imports + helpers |
| `gui/qt/window_utils.py` | 119 | helper finestre (NUOVO — vedi §9) | | `gui/qt/window_utils.py` | 119 | window helpers (NEW — see §9) |
| `gui/qt/calendar.py` | 80 | `BalCalendar` | | `gui/qt/calendar.py` | 80 | `BalCalendar` |
| `gui/qt/theme.py` | 59 | mappatura stato → colore | | `gui/qt/theme.py` | 59 | status → colour mapping |
| `gui/qt/__init__.py` | 17 | init package GUI | | `gui/qt/__init__.py` | 17 | GUI package init |
> Le differenze nei conteggi di righe rispetto all'originale derivano da: > The differences in the line counts compared to the original come from:
> riformattazione/commenti, separazione degli import per modulo, e spostamento > reformatting/comments, separation of imports per module, and the movement
> di funzioni tra `util.py`/`bal.py` e i nuovi moduli. **Gli algoritmi non sono > of functions between `util.py`/`bal.py` and the new modules. **The algorithms
> stati modificati.** > were not modified.**
--- ---
## 12. Cronologia delle modifiche su GitHub ## 12. Change history on GitHub
- **`4198a51`** — import iniziale del refactor strutturale (v0.2.8): separazione - **`4198a51`** — initial import of the structural refactor (v0.2.8):
`core/` (logica) vs `gui/qt/` (presentazione), packaging conforme, fix separation of `core/` (logic) vs `gui/qt/` (presentation), compliant
caricamento zip esterno, smoke test (sezioni §1-§8). packaging, external-zip load fix, smoke test (sections §1-§8).
- **`d56fa36`** — questo changelog del refactoring (in italiano). - **`d56fa36`** — this refactoring changelog (in Italian).
- **`4806997`** — `DIAGNOSI_GUI.md`: diagnosi dei bug GUI di z-order e ciclo di - **`4806997`** — `GUI_DIAGNOSIS.md` (originally `DIAGNOSI_GUI.md`): diagnosis
vita (Fase A). of the z-order and lifecycle GUI bugs (Phase A).
- **`dd6f677`** (PR **#2**, squash) — correzioni GUI **B1-B10** + fix download - **`dd6f677`** (PR **#2**, squash) — GUI fixes **B1-B10** + will-executor list
lista will-executor + `window_utils.py` + test di regressione (sezioni §9-§10). download fix + `window_utils.py` + regression test (sections §9-§10).
- **PR #3** — fix **OverflowError su Windows (anno 2038)** che rompeva le schede - **PR #3** — fix for the **OverflowError on Windows (year 2038)** that broke
Will/Heirs e la voce di menu (sezione §13). the Will/Heirs tabs and the menu entry (section §13).
--- ---
## 13. CORREZIONE BUG: OverflowError su Windows (limite anno 2038) ## 13. BUG FIX: OverflowError on Windows (year-2038 limit)
### Sintomo (Windows 11) ### Symptom (Windows 11)
Dopo aver **riavviato Electrum** o **cambiato wallet**, le schede **Will** e After **restarting Electrum** or **switching wallet**, the **Will** and
**Heirs** sparivano e compariva una **voce di menu condensata/illeggibile** **Heirs** tabs disappeared and a **condensed/illegible menu entry**
(icona + testo sovrapposti) sotto il logo di Electrum, accanto a *Portafogli*. (overlapping icon + text) appeared under the Electrum logo, next to *Wallets*.
Su Linux il problema non si manifestava. On Linux the problem did not occur.
### Causa vera (dal log di Electrum dell'utente) ### Real cause (from the user's Electrum log)
``` ```
OverflowError: Python int too large to convert to C int OverflowError: Python int too large to convert to C int
window.py __init__ -> create_heirs_tab -> WillSettingsWidget window.py __init__ -> create_heirs_tab -> WillSettingsWidget
@@ -419,196 +417,192 @@ OverflowError: Python int too large to convert to C int
-> datetime.fromtimestamp(NLOCKTIME_MAX) -> datetime.fromtimestamp(NLOCKTIME_MAX)
``` ```
- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` viene usato come locktime di - `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` is used as the
**default/sentinella**. **default/sentinel** locktime.
- Su **Windows** `time_t` è a **32 bit**, quindi `datetime.fromtimestamp(ts)` - On **Windows** `time_t` is **32-bit**, so `datetime.fromtimestamp(ts)`
solleva **`OverflowError`** per qualsiasi timestamp oltre il **2038**. raises **`OverflowError`** for any timestamp beyond **2038**.
- Su **Linux 64-bit** la stessa chiamata **funziona**: ecco perché il bug si - On **64-bit Linux** the same call **works**: that is why the bug was visible
vedeva solo su Windows e i test su Linux non lo intercettavano. only on Windows and the Linux tests did not catch it.
- L'eccezione interrompeva `BalWindow.__init__` durante `init_menubar` / - The exception interrupted `BalWindow.__init__` during `init_menubar` /
`load_wallet`, lasciando le schede Will/Heirs e la voce di menu **a metà `load_wallet`, leaving the Will/Heirs tabs and the menu entry **half-built**
costruzione** → l'elemento grafico condensato/illeggibile sotto il logo. → the condensed/illegible graphical element under the logo.
> Nota: i due primi tentativi di correzione (status-bar no-op e idempotenza di > Note: the first two correction attempts (a no-op status bar and the
> `init_menubar_tools`) **non** centravano la causa; sono stati comunque > idempotency of `init_menubar_tools`) did **not** hit the cause; they were
> mantenuti perché innocui e leggermente migliorativi, ma il vero colpevole era > kept anyway because they are harmless and slightly improving, but the real
> questo crash a monte. > culprit was this upstream crash.
### Fix (comportamento invariato per tutti i valori normali) ### Fix (behaviour unchanged for all normal values)
- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` con - **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` with
**clamp a INT32_MAX** (anno 2038) in caso di `OverflowError`/`OSError`/ a **clamp to INT32_MAX** (year 2038) on `OverflowError`/`OSError`/
`ValueError`, **esattamente** come la funzione `get_max_allowed_timestamp()` `ValueError`, **exactly** like the original's `get_max_allowed_timestamp()`
dell'originale (workaround per Electrum issue **#6170**). function (workaround for Electrum issue **#6170**).
- Usato in `to_date` / `to_timestamp` / `__str__` / `__repr__` di - Used in `to_date` / `to_timestamp` / `__str__` / `__repr__` of
`BalTimestamp`. `BalTimestamp`.
- `gui/qt/widgets.py` (`set_value`): usa il converter sicuro. - `gui/qt/widgets.py` (`set_value`): uses the safe converter.
- `core/util.py` (`timestamp_minus`): stessa protezione inline con clamp a - `core/util.py` (`timestamp_minus`): same inline protection with a clamp to
INT32_MAX. INT32_MAX.
I valori entro il 2038 (date assolute normali, durate relative come `90d`/`5y`) Values within 2038 (normal absolute dates, relative durations such as
producono **lo stesso identico risultato** di prima. `90d`/`5y`) produce **exactly the same result** as before.
### Test ### Test
- `tests/windows_overflow_test.py` riproduce il limite 32-bit di Windows - `tests/windows_overflow_test.py` reproduces the Windows 32-bit limit
(monkeypatch di `datetime.fromtimestamp`) e dimostra che **senza** il fix si (monkeypatch of `datetime.fromtimestamp`) and proves that **without** the fix
ottiene lo **stesso** `OverflowError` del log, mentre **con** il fix passa. you get the **same** `OverflowError` as in the log, while **with** the fix it
Verificato anche che il test **fallisce** senza il fix. passes. It was also verified that the test **fails** without the fix.
Confermato dall'utente: **"si ora funziona"**. Confirmed by the user: **"yes, it works now"**.
## 14. NUOVA FUNZIONE: invalidazione automatica al posticipo dell'eredità ## 14. NEW FEATURE: automatic invalidation when postponing the inheritance
### Problema ### Problem
Una transazione di eredità viene firmata con un **locktime fisso e immutabile** An inheritance transaction is signed with a **fixed, immutable locktime** and
e inviata ai will-executor, che sono economicamente incentivati a trasmetterla sent to the will-executors, who are economically incentivized to broadcast it
(incassano le fee). Se l'utente, dopo aver firmato/inviato, **posticipa** la (they collect the fees). If the user, after signing/sending, **postpones** the
data di consegna (es. di un anno), la **vecchia** transazione gia firmata resta delivery date (e.g. by one year), the **old** already-signed transaction
valida sui server dei will-executor. Poiche ha il locktime piu basso, un remains valid on the will-executors' servers. Since it has the lower locktime,
will-executor potrebbe trasmetterla appena scade, eseguendo l'eredita **in a will-executor could broadcast it as soon as it expires, executing the
anticipo** rispetto alla nuova volonta dell'utente. La versione precedente inheritance **earlier** than the user's new intent. The previous version did
**non gestiva** questo caso: il posticipo non produceva alcuna azione. **not handle** this case: postponing produced no action at all.
### Soluzione (Strategia B — invalidazione esplicita on-chain) ### Solution (Strategy B — explicit on-chain invalidation)
Al posticipo di un'eredita **gia firmata e/o inviata** (stato `COMPLETE` o When postponing an inheritance that is **already signed and/or sent** (state
`PUSHED`), il plugin chiede di **invalidare on-chain** i fondi prima di `COMPLETE` or `PUSHED`), the plugin asks to **invalidate the funds on-chain**
ricostruire la nuova eredita. L'invalidazione spende gli stessi UTXO verso un before rebuilding the new inheritance. The invalidation spends the same UTXOs
nuovo indirizzo di change con `locktime = altezza corrente` (RBF), quindi e to a new change address with `locktime = current height` (RBF), so it is
trasmettibile subito: una volta confermata, la vecchia transazione pre-firmata broadcastable immediately: once confirmed, the old pre-signed transaction
diventa **definitivamente inutilizzabile**, vincendo la corsa contro qualunque becomes **permanently unusable**, winning the race against any will-executor.
will-executor.
### Dettagli tecnici ### Technical details
- **`core/will.py`**: - **`core/will.py`**:
- nuova eccezione `WillPostponedException` (sottoclasse di - new exception `WillPostponedException` (subclass of
`NotCompleteWillException`); `NotCompleteWillException`);
- `check_willexecutors_and_heirs`: il confronto del locktime non usa piu - `check_willexecutors_and_heirs`: the locktime comparison no longer uses the
l'entry dell'erede memorizzata (`their[2]`), che viene aggiornata in memoria stored heir entry (`their[2]`), which is updated in memory together with the
insieme al nuovo valore al momento del posticipo e quindi risulterebbe new value at the moment of postponement and would therefore always look
sempre uguale. Ora confronta il locktime richiesto con **`w.tx.locktime`**, equal. It now compares the requested locktime with **`w.tx.locktime`**, i.e.
cioe il locktime **congelato** nella transazione firmata (immutabile, e the locktime **frozen** in the signed transaction (immutable, and the one
quello che i will-executor possiedono). Tre casi: invariato → coerente; the will-executors hold). Three cases: unchanged → coherent; new > tx on a
nuovo > tx su will firmato/inviato → `WillPostponedException`; nuovo > tx su signed/sent will → `WillPostponedException`; new > tx on a will never sent
will mai inviato → semplice ricostruzione (nessuna fee on-chain). → simple rebuild (no on-chain fee).
- **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, il percorso reale - **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, the real path used
usato da **Tools → Prepare**): aggiunto il ramo `except WillPostponedException` by **Tools → Prepare**): added the `except WillPostponedException` branch
**prima** di `NotCompleteWillException`; si comporta come il caso "will **before** `NotCompleteWillException`; it behaves like the "expired will"
scaduto" e ritorna `(None, tx)` per innescare firma + broadcast case and returns `(None, tx)` to trigger signing + broadcasting of the
dell'invalidazione. L'utente preme di nuovo **Prepare** per ricostruire, invalidation. The user presses **Prepare** again to rebuild, re-sign and
rifirmare e reinviare la nuova eredita (due passi espliciti, per maggior re-send the new inheritance (two explicit steps, for greater control).
controllo). - **`gui/qt/window.py`** (`build_inheritance_transaction`): added the same
- **`gui/qt/window.py`** (`build_inheritance_transaction`): aggiunto lo stesso branch for completeness of the alternative path, with an explanatory message.
ramo per completezza del percorso alternativo, con messaggio esplicativo. - **`gui/qt/common.py`**: `WillPostponedException` exported.
- **`gui/qt/common.py`**: `WillPostponedException` esportato.
### NUOVA COLONNA "Server" nella lista transazioni ### NEW "Server" COLUMN in the transactions list
Per dare all'utente visibilita costante sullo stato online delle proprie To give the user constant visibility on the online status of their inheritance
transazioni di eredita, e stata aggiunta una colonna dedicata **"Server"** in transactions, a dedicated **"Server"** column was added in `PreviewList`
`PreviewList` (`gui/qt/lists.py`), con etichetta sempre leggibile (`gui/qt/lists.py`), with an always-readable label (`Confirmed on server`,
(`Confirmed on server`, `Sent (not checked)`, `Send failed`, `Not on server`, `Sent (not checked)`, `Send failed`, `Not on server`, `Signed (not sent)`,
`Signed (not sent)`, `Not sent`) e **tooltip** con URL del will-executor e `Not sent`) and a **tooltip** with the will-executor URL and status. The
stato. Le funzioni `server_status_text()` e `server_status_tooltip()` sono in functions `server_status_text()` and `server_status_tooltip()` are in
`gui/qt/theme.py` e riusano gli stessi flag di stato gia esistenti. `gui/qt/theme.py` and reuse the same already-existing status flags.
### Test ### Test
- I 182 test ufficiali continuano a passare; smoke test ed external-zip test - The 182 official tests keep passing; smoke test and external-zip test OK;
OK; `ruff` senza nuove segnalazioni reali. `ruff` with no new real warnings.
- Verificato sui dati reali del log dell'utente: il posticipo di un'eredita - Verified against the real data from the user's log: postponing a signed
firmata ora rileva correttamente la condizione e avvia l'invalidazione. inheritance now correctly detects the condition and starts the invalidation.
Confermato dall'utente: **"mi pare che funziona"**. Confirmed by the user: **"it seems to work"**.
## 15. TENTATIVO E REVERT: fix doppia invalidazione al posticipo (v0.3.1 -> v0.3.2) ## 15. ATTEMPT AND REVERT: fix for double invalidation on postpone (v0.3.1 -> v0.3.2)
### v0.3.1 (RITIRATA) ### v0.3.1 (WITHDRAWN)
Per risolvere la doppia firma dell'invalidazione al posticipo, era stato To solve the double signing of the invalidation on postpone,
introdotto `Will.mark_invalidated_by_tx()`, chiamato in `Will.mark_invalidated_by_tx()` had been introduced, called in
`loop_broadcast_invalidating` dopo il broadcast dell'invalidazione, per marcare `loop_broadcast_invalidating` after broadcasting the invalidation, to mark as
`INVALIDATED` le will che spendevano gli stessi UTXO della tx di invalidazione `INVALIDATED` the wills that spent the same UTXOs as the invalidation tx and to
e persistere lo stato con `save_willitems`. persist the state with `save_willitems`.
### Perche e stata ritirata ### Why it was withdrawn
La modifica ha introdotto una regressione grave segnalata dall'utente: The change introduced a serious regression reported by the user:
**la lista eredita mostrava ancora le vecchie eredita e l'aggiornamento di **the inheritance list still showed the old inheritances and the update of
eredi/date risultava incoerente**. heirs/dates was inconsistent**.
Causa: `loop_broadcast_invalidating` e il punto di broadcast usato per **TUTTI** Cause: `loop_broadcast_invalidating` is the broadcast point used for **ALL**
i tipi di invalidazione (posticipo, CheckAlive, will scaduto/anticipato), non types of invalidation (postpone, CheckAlive, expired/anticipated will), not
solo per il posticipo. Inoltre il metodo marcava e **persisteva** lo stato only for postpone. In addition, the method marked and **persisted** the
`INVALIDATED` su tutte le will item che condividevano gli UTXO del wallet `INVALIDATED` state on all the will items that shared the wallet's UTXOs
(tipicamente tutte). Queste will item invalidate restavano poi in memoria e su (typically all of them). These invalidated will items then stayed in memory and
disco, inquinando la ricostruzione di eredi/date e lasciando vecchie voci nella on disk, polluting the rebuild of heirs/dates and leaving old entries in the
lista. list.
### v0.3.2 (questa versione): REVERT completo ### v0.3.2 (this version): full REVERT
- Rimosso `Will.mark_invalidated_by_tx()` da `core/will.py`. - Removed `Will.mark_invalidated_by_tx()` from `core/will.py`.
- Rimossa la chiamata in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`): - Removed the call in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`): the
il metodo torna **identico** alla v0.3.0. method goes back to being **identical** to v0.3.0.
- Rimossi i due test relativi; mantenuto solo l'assert di gerarchia su - Removed the two related tests; kept only the hierarchy assertion on
`WillPostponedException` (corretto e indipendente). `WillPostponedException` (correct and independent).
- `core/will.py` e `gui/qt/dialogs.py` sono ora **byte-identici** alla v0.3.0 - `core/will.py` and `gui/qt/dialogs.py` are now **byte-identical** to the
funzionante (verificato con `git diff a394cde`). working v0.3.0 (verified with `git diff a394cde`).
Il bug della doppia invalidazione al posticipo resta quindi **aperto** e andra The double-invalidation-on-postpone bug therefore remains **open** and will
riaffrontato in modo piu mirato (senza toccare il percorso di broadcast comune e have to be tackled in a more targeted way (without touching the common
senza persistere stati su will che condividono gli UTXO), previa conferma broadcast path and without persisting states on wills that share the UTXOs),
dell'utente. La priorita era ripristinare il comportamento corretto di subject to the user's confirmation. The priority was to restore the correct
lista/eredi/date. behaviour of the list/heirs/dates.
## 16. Aggiornamenti mancati, Check/Close coerenti, e rifinitura UI (v0.3.2) ## 16. Missed updates, consistent Check/Close, and UI polish (v0.3.2)
### FIX 1 - Rimozione di un erede rilevata su Check / chiusura Electrum ### FIX 1 - Removal of an heir detected on Check / Electrum close
`core/will.py` (`check_willexecutors_and_heirs`): prima il plugin `core/will.py` (`check_willexecutors_and_heirs`): previously the plugin
rilevava solo l'**aggiunta** di un erede (raise `HeirNotFoundException` quando un detected only the **addition** of an heir (raising `HeirNotFoundException` when
erede corrente non era piu nella will). Mancava il caso inverso: la a current heir was no longer in the will). The opposite case was missing: the
**rimozione** di un erede. Aggiunto il ramo `else` che lancia **removal** of an heir. Added the `else` branch that raises
`HeirNotFoundException` anche quando la will porta ancora un erede che non e piu `HeirNotFoundException` also when the will still carries an heir that is no
presente nel set di eredi corrente. Cosi la ricostruzione dell'eredita scatta su longer present in the current heir set. This way the inheritance rebuild is
**Check** e alla **chiusura di Electrum** (entrambi usano lo stesso percorso triggered on **Check** and on **Electrum close** (both use the same
`BalBuildWillDialog.build_will_task()`), come deciso dall'utente: nessun `BalBuildWillDialog.build_will_task()` path), as decided by the user: no
aggiornamento automatico dopo la modifica, solo manuale con Check / alla automatic update after the change, only a manual one via Check / on close.
chiusura.
### FIX 2 - Check interroga i server anche per le will gia inviate ### FIX 2 - Check also queries the servers for already-sent wills
`core/will.py` (nuovo `Will.needs_server_check(w)`) e `core/will.py` (new `Will.needs_server_check(w)`) and `gui/qt/lists.py`
`gui/qt/lists.py` (`PreviewList.check`): prima il Check interrogava i server solo (`PreviewList.check`): previously Check queried the servers only for wills in
per le will in stato `PUSHED`. Le will gia inviate ma rimaste su "New / Not sent" the `PUSHED` state. Wills already sent but left at "New / Not sent" were not
non venivano ricontrollate ("nothing to do"). Ora `needs_server_check` include re-checked ("nothing to do"). Now `needs_server_check` includes every **VALID**
ogni will **VALID** con un will-executor e **non ancora CHECKED**, anche se non will with a will-executor that is **not yet CHECKED**, even if not in the
in stato `PUSHED`. Stesso controllo usato sia dal pulsante Check sia da `PUSHED` state. The same check is used both by the Check button and by
`on_close`. `on_close`.
### FIX 3 - Hide invalidated/replaced da finestra Impostazioni aggiornava la lista ### FIX 3 - Hide invalidated/replaced from the Settings window updated the list
`core/plugin_base.py` (nuovo `sync_hide_filters()`) e `core/plugin_base.py` (new `sync_hide_filters()`) and `gui/qt/window.py`
`gui/qt/window.py` (`update_all`): le checkbox "Hide Replaced" / "Hide (`update_all`): the "Hide Replaced" / "Hide Invalidated" checkboxes in the
Invalidated" nella finestra Impostazioni scrivono direttamente la config Settings window write the config directly (`BalConfig.set`) without touching the
(`BalConfig.set`) senza toccare i flag in cache `_hide_invalidated` / cached flags `_hide_invalidated` / `_hide_replaced` that the list uses to
`_hide_replaced` usati dalla lista per filtrare. Risultato: la lista continuava filter. Result: the list kept filtering with the old value until Electrum was
a filtrare col valore vecchio finche non si riavviava Electrum. Ora restarted. Now `update_all()` calls `sync_hide_filters()`, which re-reads the
`update_all()` chiama `sync_hide_filters()` che ri-legge i flag dalla config, flags from the config, so whatever the source of the change (toolbar or
quindi qualunque sorgente del cambiamento (toolbar o finestra Impostazioni) Settings window) the list updates immediately.
aggiorna subito la lista.
### Rifinitura UI - Risultati in grassetto nel dialog "Building Will" ### UI polish - Bold results in the "Building Will" dialog
`gui/qt/dialogs.py` (`BalBuildWillDialog`): i **risultati** mostrati a destra di `gui/qt/dialogs.py` (`BalBuildWillDialog`): the **results** shown to the right
ogni riga di stato (es. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`, of each status line (e.g. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`,
`Timeout`) sono ora resi in **grassetto**, mantenendo i loro colori `Timeout`) are now rendered in **bold**, keeping their colours
(verde/rosso/giallo). Le etichette di stato a sinistra restano in peso normale. (green/red/yellow). The status labels on the left stay in normal weight. The
Modifica centralizzata negli helper `msg_ok`, `msg_error`, `msg_warning`, change is centralized in the helpers `msg_ok`, `msg_error`, `msg_warning`,
`msg_set_status`, piu le righe dei will-executor (push e check) che ora mostrano `msg_set_status`, plus the will-executor lines (push and check) that now show
`Ok/Ko` e `True/False` in grassetto + colore (verde/rosso). `Ok/Ko` and `True/False` in bold + colour (green/red).
### Test ### Test
- 186 test ufficiali passano; smoke test, external-zip test e simulazione dei - 186 official tests pass; smoke test, external-zip test and the update-flow
flussi di aggiornamento (`tests/sim_update_flows.py`) OK; `ruff` senza nuove simulation (`tests/sim_update_flows.py`) OK; `ruff` with no new real warnings
segnalazioni reali (solo falsi positivi pre-esistenti da star-import). (only pre-existing false positives from star-imports).
- Aggiunti test in `tests/test_core_will.py`: - Added tests in `tests/test_core_will.py`:
`test_check_heirs_unchanged_is_coherent`, `test_check_heirs_unchanged_is_coherent`,
`test_check_heir_removed_triggers_rebuild`, `test_check_heir_removed_triggers_rebuild`,
`test_check_heir_added_triggers_rebuild`, `test_needs_server_check`. `test_check_heir_added_triggers_rebuild`, `test_needs_server_check`.
Confermato dall'utente sui dati reali: dopo Sign -> Broadcast -> Check le Confirmed by the user against real data: after Sign -> Broadcast -> Check the
transazioni gia inviate sono tornate verdi ("confirmed on server"); la lista already-sent transactions turned green again ("confirmed on server"); the list
torna pulita; il grassetto e l'aggiornamento delle hide-flag funzionano. 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) ## 17. UI polish and bug fix — signed-tx colour, wizard, Building Will dialog (v0.3.3)

View File

@@ -1,319 +0,0 @@
# BAL — Diagnosi dei problemi GUI (Fase A) → ✅ RISOLTI (Fase B)
> **STATO: tutti i bug B1-B10 sono stati CORRETTI** e mergiati in `main`
> (PR #2, squash `dd6f677`). La logica di business resta **byte-identica**
> (nessuna modifica a `bal/core/*`): sono cambiati solo presentazione, parent,
> modalità, ciclo di vita e cleanup delle finestre.
>
> | ID | Stato | Fix applicato |
> |----|-------|---------------|
> | B1 | ✅ FIXED | `self.parent` → `self._bal_parent` (dialogs/lists/widgets); parent = `top_level_of(parent)` |
> | B2 | ✅ FIXED | `.show()` → `show_on_top()` / `show_modal()` con parent corretto |
> | B3 | ✅ FIXED | init a caldo: `_setup_window()` replica `load_wallet`, niente "restart Electrum" |
> | B4 | ✅ FIXED | chiave finestra stabile `_window_key()` = `id(window)` |
> | B5 | ✅ FIXED | `on_close` riscritto: niente `except:pass`, log per-step, reset stato |
> | B6 | ✅ FIXED | `BalBlockingWaitingDialog`: `processEvents()` ripristinato |
> | B7 | ✅ FIXED | `closeEvent/hideEvent`: `stop_thread()` + `super()` |
> | B8 | ✅ FIXED | `closeEvent`: `stop_thread()` (stop+wait) + `super()` |
> | B9 | ✅ FIXED | `bring_to_front()` = `raise_()` + `activateWindow()` |
> | B10| ✅ FIXED | uso di `window.tools_menu` (API ufficiale), niente ricerca per titolo `&Tools` |
>
> Helper centralizzati in `bal/gui/qt/window_utils.py`:
> `top_level_of`, `bring_to_front`, `stop_thread`, `show_modal`, `show_on_top`.
> Test di regressione: `tests/gui_fixes_test.py` (oltre a smoke + external_zip).
---
## (Storico) Diagnosi originale
Documento di sola **diagnosi**: nessuna riga di codice funzionale era stata
modificata in Fase A. Elenca i problemi grafici/di ciclo di vita riscontrati nel
codice, la loro **causa tecnica** e il **fix proposto**, con riferimenti riga.
I due sintomi che hai segnalato:
- **(S1)** Le finestre del plugin spariscono dietro la finestra di Electrum.
- **(S2)** Alcuni meccanismi funzionano solo dopo aver chiuso e "ripulito"
Electrum.
Sono entrambi spiegati dai bug qui sotto.
---
## Riepilogo (tabella)
| ID | Gravità | Sintomo | File:riga | Causa breve |
|----|---------|---------|-----------|-------------|
| B1 | 🔴 Alta | S1 | `dialogs.py:40,69,475` | `self.parent = parent` sovrascrive il metodo `QWidget.parent()` |
| B2 | 🔴 Alta | S1 | `window.py:148,936`, `window.py:566` | dialoghi aperti con `.show()` (non-modali, senza stare in primo piano) |
| B3 | 🔴 Alta | S2 | `plugin.py:38-42` | messaggio "Please restart Electrum" = init a caldo non gestito |
| B4 | 🔴 Alta | S2 | `plugin.py:45,111` | chiave dizionario `winId` (metodo) invece di `winId()` (valore) |
| B5 | 🟠 Media | S2 | `window.py:664-677` | `on_close` con `except: pass` che nasconde errori di cleanup |
| B6 | 🟠 Media | S1/S2 | `dialogs.py:445-462` | `BalBlockingWaitingDialog` blocca il thread GUI, `processEvents` commentato |
| B7 | 🟠 Media | S2 | `dialogs.py:48-58` | `closeEvent/hideEvent` con cleanup thread commentato |
| B8 | 🟠 Media | S2 | `dialogs.py:828-830` | `closeEvent` chiama `thread.stop()` ma non `thread.wait()``super()` |
| B9 | 🟡 Bassa | S1 | `dialogs.py:1121-1122` | `show()+raise_()` senza `activateWindow()` né modalità |
| B10| 🟡 Bassa | — | `plugin.py:36` (init), vari | gestione finestre multiple/ multi-wallet fragile |
---
## Dettaglio dei problemi
### B1 — `self.parent = parent` rompe il sistema di finestre di Qt 🔴
**Dove:** `dialogs.py:40` (in `BalDialog.__init__`), ripetuto a `:69` e `:475`;
analoghi in altri dialoghi.
```python
self.parent = parent # <-- PROBLEMA
super().__init__(parent)
```
**Causa:** in Qt, `parent()` è un **metodo** di `QWidget` che restituisce il
widget genitore. Assegnando un **attributo** `self.parent`, lo si maschera: da
quel punto `self.parent` non è più il metodo ma il valore salvato. Qualunque
codice (anche interno a Qt o di Electrum) che si aspetta `widget.parent()` come
metodo può comportarsi in modo imprevisto. Inoltre il `parent` passato non è
sempre la **top-level window** corretta, quindi il dialogo non viene agganciato
gerarchicamente alla finestra di Electrum e finisce **dietro** (S1).
**Fix proposto:**
- Non sovrascrivere `parent`: rinominare l'attributo (es. `self._bal_parent`).
- Passare sempre come `parent` la **top-level window** di Electrum
(`window.top_level_window()`), così il dialogo resta in primo piano rispetto
ad essa.
---
### B2 — Dialoghi aperti con `.show()` invece che modali 🔴
**Dove:**
- `window.py:148` `show_willexecutor_dialog``self.willexecutor_dialog.show()`
- `window.py:936` `preview_modal_dialog``self.dw.show()` (il nome dice
"modal" ma usa `show()`!)
- `window.py:566` `show_transaction_real``d.show()`
**Causa:** `show()` apre una finestra **non-modale e indipendente**: se il
`parent` non è impostato correttamente (vedi B1), la finestra non resta sopra
Electrum e ci "sparisce dietro" (S1). Si nota l'incoerenza: altrove si usa
correttamente `.exec()` (es. `init_wizard` a `window.py:144`, `settings_dialog`
a `plugin.py:254`), che è modale e resta in primo piano.
**Fix proposto:**
- Per i dialoghi che devono restare in primo piano: usare `exec()` (modale) **o**
`show()` + parent corretto + `setWindowModality(Qt.WindowModal)` +
`raise_()` + `activateWindow()`.
- Mantenere la stessa logica di "cosa fa il dialogo" (nessun cambio di
comportamento funzionale, solo z-order/modalità).
---
### B3 — "Please restart Electrum to activate the BAL plugin" 🔴
**Dove:** `plugin.py:38-42` (hook `init_qt`).
```python
if wallet:
window.show_warning(_("Please restart Electrum to activate the BAL plugin"), ...)
return
```
**Causa:** quando il plugin viene **abilitato a caldo** (wallet già aperto),
l'hook `init_qt` si arrende e chiede il riavvio invece di inizializzare le tab
e i menu sul wallet già caricato. È **la causa diretta del sintomo S2**: "devi
chiudere/riavviare Electrum perché funzioni".
**Fix proposto:**
- In `init_qt`, se c'è già un wallet aperto, eseguire la stessa inizializzazione
che normalmente avviene in `load_wallet` (creare `BalWindow`, tab, menu,
caricare il will) **senza** richiedere il riavvio.
- Simmetricamente, gestire bene `close_wallet` per smontare tab/menu, così
ri-abilitare/ricaricare non lascia stato sporco.
---
### B4 — Chiave del dizionario `winId` (metodo) invece di `winId()` 🔴
**Dove:** `plugin.py:45` (scrittura) e `plugin.py:111` (lettura).
```python
self.bal_windows[top_level_window.winId] = w # scrive con la *funzione* winId
...
w = self.bal_windows.get(window.winId, None) # legge con la *funzione* winId
```
**Causa:** `winId` senza parentesi è il **metodo legato** (bound method), non
l'identificatore della finestra. Usato come chiave "funziona per caso" perché
lo stesso oggetto-finestra produce lo stesso bound method; ma è fragile e
semanticamente errato: con più finestre/wallet o dopo riaperture la
corrispondenza può saltare, creando `BalWindow` duplicati o non trovando quello
giusto → stato incoerente (contribuisce a S2).
**Fix proposto:**
- Usare una chiave stabile e corretta, es. `int(window.winId())` oppure
`id(window)`, in modo **coerente** sia in scrittura sia in lettura.
---
### B5 — `on_close` ingoia tutti gli errori 🟠
**Dove:** `window.py:664-677`.
```python
def on_close(self):
try:
if not self.disable_plugin:
close_window = BalBuildWillDialog(self)
close_window.build_will_task()
self.save_willitems()
self.heirs_tab.close()
...
except Exception:
pass # <-- nasconde qualsiasi errore di cleanup
```
**Causa:** se una qualsiasi di queste operazioni fallisce, l'eccezione viene
silenziata: tab/menu non vengono rimossi, lo stato (`willitems`, `heirs`, tab)
resta in memoria e "sporco" finché non si riavvia Electrum (S2).
**Fix proposto:**
- Non silenziare: loggare l'errore con `_logger`.
- Rendere il cleanup **robusto e idempotente** (ogni passo in un try/except
separato con log), così un fallimento parziale non blocca gli altri passi.
- Azzerare esplicitamente lo stato (`willitems={}`, riferimenti a tab/menu a
`None`) a fine `on_close`.
---
### B6 — `BalBlockingWaitingDialog` blocca il thread della GUI 🟠
**Dove:** `dialogs.py:445-462`.
```python
self.show()
# QCoreApplication.processEvents() # <-- commentato
# QCoreApplication.processEvents()
try:
task() # esegue il task SUL thread GUI -> finestra "congelata"
finally:
self.accept()
```
**Causa:** dopo `show()` non si dà alla GUI il tempo di disegnarsi
(`processEvents` è commentato) e poi si esegue `task()` **bloccando** il thread
dell'interfaccia. Risultato: la finestra "Please wait" può apparire vuota,
non ridisegnarsi, e l'app sembra bloccata (contribuisce a S1/percezione di
freeze).
**Fix proposto:**
- O eseguire il task in un `TaskThread` (come fa già `BalWaitingDialog`),
- oppure, se deve restare bloccante, ripristinare un `processEvents()` dopo
`show()` per far disegnare la finestra prima del task.
---
### B7 — `closeEvent`/`hideEvent` con cleanup thread commentato 🟠
**Dove:** `dialogs.py:48-58` (`BalDialog`).
```python
def closeEvent(self, event):
self._stopping = True
#if self.thread:
# self.thread.stop() # <-- disattivato
super().closeEvent(event)
```
**Causa:** alla chiusura del dialogo i thread eventualmente attivi **non**
vengono fermati. Restano in esecuzione in background, possono scrivere su widget
già distrutti o tenere risorse/connessioni → comportamenti erratici finché non
si riavvia (S2).
**Fix proposto:**
- Ripristinare in modo sicuro lo stop dei thread: `if self.thread:
self.thread.stop(); self.thread.wait()` con guardia su `None`.
---
### B8 — `BalBuildWillDialog.closeEvent` incompleto 🟠
**Dove:** `dialogs.py:828-830`.
```python
def closeEvent(self, event):
self._stopping = True
self.thread.stop()
# manca self.thread.wait() e manca super().closeEvent(event)
```
**Causa:** `stop()` segnala lo stop ma non attende la fine del thread
(`wait()`), e non viene chiamato `super().closeEvent(event)`: l'evento di
chiusura non è propagato correttamente. Possibili thread orfani e finestre che
non si chiudono pulite.
**Fix proposto:**
- `self.thread.stop(); self.thread.wait(); super().closeEvent(event)` con
guardia su `self.thread is None`.
---
### B9 — `show()+raise_()` senza `activateWindow()`/modalità 🟡
**Dove:** `dialogs.py:1121-1122` (es. `WillExecutorDialog`/dettaglio).
```python
self.show()
self.raise_()
# manca self.activateWindow(); nessuna modalità impostata
```
**Causa:** `raise_()` alza la finestra nello stack ma su alcuni window manager
(incluso Windows) senza `activateWindow()` non riceve il focus e può comunque
finire dietro. Senza modalità, l'utente può tornare alla finestra principale
lasciando il dialogo nascosto.
**Fix proposto:**
- Aggiungere `self.activateWindow()` dopo `raise_()`, e valutare
`setWindowModality(Qt.WindowModal)` dove ha senso.
---
### B10 — Gestione finestre multiple / multi-wallet fragile 🟡
**Dove:** `plugin.py:30-62` (`init_qt`), `get_window` (`plugin.py:109-115`).
**Causa:** la mappa `bal_windows` e l'aggancio ai menu si basano su assunzioni
(B4) e sull'iterazione dei figli del menubar per nome (`"&Tools"`), che è
sensibile alla **localizzazione** (tu usi `Locale: Italian_Italy`!). Se il menu
non si chiama esattamente `&Tools` nella lingua corrente, l'aggancio può
fallire silenziosamente.
**Fix proposto:**
- Usare l'API ufficiale `window.tools_menu` (già usata in `init_menubar`,
`plugin.py:79`) invece di cercare il menu per titolo tradotto.
- Unificare la creazione/lookup di `BalWindow` su una chiave stabile (B4).
---
## Strategia di correzione proposta (per la Fase B/C)
Per **non cambiare la logica di funzionamento** e ridurre i rischi, propongo di
introdurre un **unico punto centralizzato** di gestione finestre (un piccolo
helper, es. `gui/qt/window_utils.py`) con funzioni tipo:
- `show_modal(dialog)` → imposta parent corretto, modalità, `exec()`.
- `show_on_top(dialog)` → `show()` + `raise_()` + `activateWindow()` per i
pochi casi che devono restare non-modali.
E poi sostituire i `.show()`/`.exec()` sparsi con queste funzioni. Vantaggi:
- la **logica di business resta intatta** (cosa fa il dialogo non cambia);
- si tocca **solo** il "come" viene mostrato/chiuso;
- più facile da testare e da revisionare (diff piccolo e localizzato).
### Ordine consigliato
1. **B3 + B4** (init a caldo + chiave finestre): risolvono la radice di S2.
2. **B1 + B2 + B9** (parent/modalità/z-order): risolvono S1.
3. **B5 + B7 + B8** (cleanup robusto + thread): chiudono i residui di S2.
4. **B6 + B10** (waiting dialog + menu localizzati): rifiniture.
---
## Cosa serve da te per la Fase B/C
- Conferma che posso modificare il **comportamento della GUI** (parent,
modalità, cleanup, init a caldo) mantenendo invariata la logica di business.
- Test su **Electrum portable Windows** dopo ogni gruppo di fix, con descrizione
/screenshot di cosa succede (apertura dialoghi, abilitazione a caldo,
chiusura wallet).
> Nota: i bug B1B10 esistono **identici nell'originale** — questo refactor li
> ha preservati fedelmente (era l'obiettivo della fase precedente). La Fase B/C
> li corregge.

320
GUI_DIAGNOSIS.md Normal file
View File

@@ -0,0 +1,320 @@
# BAL — Diagnosis of the GUI problems (Phase A) → ✅ RESOLVED (Phase B)
> **STATUS: all bugs B1-B10 have been FIXED** and merged into `main`
> (PR #2, squash `dd6f677`). The business logic remains **byte-identical**
> (no changes to `bal/core/*`): only the presentation, parent, modality,
> lifecycle and cleanup of the windows changed.
>
> | ID | Status | Applied fix |
> |----|--------|-------------|
> | B1 | ✅ FIXED | `self.parent` → `self._bal_parent` (dialogs/lists/widgets); parent = `top_level_of(parent)` |
> | B2 | ✅ FIXED | `.show()` → `show_on_top()` / `show_modal()` with the correct parent |
> | B3 | ✅ FIXED | hot init: `_setup_window()` replicates `load_wallet`, no more "restart Electrum" |
> | B4 | ✅ FIXED | stable window key `_window_key()` = `id(window)` |
> | B5 | ✅ FIXED | `on_close` rewritten: no `except:pass`, per-step logging, state reset |
> | B6 | ✅ FIXED | `BalBlockingWaitingDialog`: `processEvents()` restored |
> | B7 | ✅ FIXED | `closeEvent/hideEvent`: `stop_thread()` + `super()` |
> | B8 | ✅ FIXED | `closeEvent`: `stop_thread()` (stop+wait) + `super()` |
> | B9 | ✅ FIXED | `bring_to_front()` = `raise_()` + `activateWindow()` |
> | B10| ✅ FIXED | use of `window.tools_menu` (official API), no lookup by the `&Tools` title |
>
> Helpers centralized in `bal/gui/qt/window_utils.py`:
> `top_level_of`, `bring_to_front`, `stop_thread`, `show_modal`, `show_on_top`.
> Regression test: `tests/gui_fixes_test.py` (in addition to smoke + external_zip).
---
## (Historical) Original diagnosis
A **diagnosis-only** document: no line of functional code had been modified in
Phase A. It lists the graphical/lifecycle problems found in the code, their
**technical cause** and the **proposed fix**, with line references.
The two symptoms you reported:
- **(S1)** The plugin windows disappear behind the Electrum window.
- **(S2)** Some mechanisms work only after closing and "cleaning up"
Electrum.
Both are explained by the bugs below.
---
## Summary (table)
| ID | Severity | Symptom | File:line | Short cause |
|----|----------|---------|-----------|-------------|
| B1 | 🔴 High | S1 | `dialogs.py:40,69,475` | `self.parent = parent` overrides the `QWidget.parent()` method |
| B2 | 🔴 High | S1 | `window.py:148,936`, `window.py:566` | dialogs opened with `.show()` (non-modal, not staying in the foreground) |
| B3 | 🔴 High | S2 | `plugin.py:38-42` | "Please restart Electrum" message = unhandled hot init |
| B4 | 🔴 High | S2 | `plugin.py:45,111` | dictionary key `winId` (method) instead of `winId()` (value) |
| B5 | 🟠 Medium | S2 | `window.py:664-677` | `on_close` with `except: pass` that hides cleanup errors |
| B6 | 🟠 Medium | S1/S2 | `dialogs.py:445-462` | `BalBlockingWaitingDialog` blocks the GUI thread, `processEvents` commented out |
| B7 | 🟠 Medium | S2 | `dialogs.py:48-58` | `closeEvent/hideEvent` with the thread cleanup commented out |
| B8 | 🟠 Medium | S2 | `dialogs.py:828-830` | `closeEvent` calls `thread.stop()` but not `thread.wait()` nor `super()` |
| B9 | 🟡 Low | S1 | `dialogs.py:1121-1122` | `show()+raise_()` without `activateWindow()` nor modality |
| B10| 🟡 Low | — | `plugin.py:36` (init), various | fragile multiple-window / multi-wallet handling |
---
## Detail of the problems
### B1 — `self.parent = parent` breaks Qt's window system 🔴
**Where:** `dialogs.py:40` (in `BalDialog.__init__`), repeated at `:69` and `:475`;
similar in other dialogs.
```python
self.parent = parent # <-- PROBLEM
super().__init__(parent)
```
**Cause:** in Qt, `parent()` is a **method** of `QWidget` that returns the
parent widget. By assigning an **attribute** `self.parent`, you mask it: from
that point on `self.parent` is no longer the method but the saved value. Any
code (even inside Qt or Electrum) that expects `widget.parent()` as a method
may behave unexpectedly. In addition, the `parent` that is passed is not always
the correct **top-level window**, so the dialog is not attached hierarchically
to the Electrum window and ends up **behind** it (S1).
**Proposed fix:**
- Do not override `parent`: rename the attribute (e.g. `self._bal_parent`).
- Always pass as `parent` Electrum's **top-level window**
(`window.top_level_window()`), so the dialog stays in the foreground relative
to it.
---
### B2 — Dialogs opened with `.show()` instead of modally 🔴
**Where:**
- `window.py:148` `show_willexecutor_dialog``self.willexecutor_dialog.show()`
- `window.py:936` `preview_modal_dialog``self.dw.show()` (the name says
"modal" but it uses `show()`!)
- `window.py:566` `show_transaction_real``d.show()`
**Cause:** `show()` opens a **non-modal, independent** window: if the `parent`
is not set correctly (see B1), the window does not stay above Electrum and
"disappears behind it" (S1). The inconsistency is noticeable: elsewhere `.exec()`
is used correctly (e.g. `init_wizard` at `window.py:144`, `settings_dialog` at
`plugin.py:254`), which is modal and stays in the foreground.
**Proposed fix:**
- For the dialogs that must stay in the foreground: use `exec()` (modal) **or**
`show()` + correct parent + `setWindowModality(Qt.WindowModal)` +
`raise_()` + `activateWindow()`.
- Keep the same logic of "what the dialog does" (no change of functional
behaviour, only z-order/modality).
---
### B3 — "Please restart Electrum to activate the BAL plugin" 🔴
**Where:** `plugin.py:38-42` (`init_qt` hook).
```python
if wallet:
window.show_warning(_("Please restart Electrum to activate the BAL plugin"), ...)
return
```
**Cause:** when the plugin is **enabled hot** (wallet already open), the
`init_qt` hook gives up and asks for a restart instead of initializing the tabs
and menus on the already-loaded wallet. It is **the direct cause of symptom
S2**: "you have to close/restart Electrum for it to work".
**Proposed fix:**
- In `init_qt`, if there is already an open wallet, run the same initialization
that normally happens in `load_wallet` (create `BalWindow`, tabs, menu, load
the will) **without** requiring a restart.
- Symmetrically, handle `close_wallet` properly to tear down the tabs/menu, so
that re-enabling/reloading does not leave dirty state.
---
### B4 — Dictionary key `winId` (method) instead of `winId()` 🔴
**Where:** `plugin.py:45` (write) and `plugin.py:111` (read).
```python
self.bal_windows[top_level_window.winId] = w # writes with the *function* winId
...
w = self.bal_windows.get(window.winId, None) # reads with the *function* winId
```
**Cause:** `winId` without parentheses is the **bound method**, not the window
identifier. Used as a key it "works by accident" because the same window object
produces the same bound method; but it is fragile and semantically wrong: with
more windows/wallets or after reopening, the matching can break, creating
duplicate `BalWindow` objects or failing to find the right one → inconsistent
state (contributes to S2).
**Proposed fix:**
- Use a stable, correct key, e.g. `int(window.winId())` or `id(window)`,
**consistently** both when writing and when reading.
---
### B5 — `on_close` swallows all errors 🟠
**Where:** `window.py:664-677`.
```python
def on_close(self):
try:
if not self.disable_plugin:
close_window = BalBuildWillDialog(self)
close_window.build_will_task()
self.save_willitems()
self.heirs_tab.close()
...
except Exception:
pass # <-- hides any cleanup error
```
**Cause:** if any of these operations fails, the exception is silenced:
tabs/menu are not removed, the state (`willitems`, `heirs`, tabs) stays in
memory and "dirty" until Electrum is restarted (S2).
**Proposed fix:**
- Do not silence it: log the error with `_logger`.
- Make the cleanup **robust and idempotent** (each step in a separate
try/except with logging), so a partial failure does not block the other
steps.
- Explicitly reset the state (`willitems={}`, references to tabs/menu set to
`None`) at the end of `on_close`.
---
### B6 — `BalBlockingWaitingDialog` blocks the GUI thread 🟠
**Where:** `dialogs.py:445-462`.
```python
self.show()
# QCoreApplication.processEvents() # <-- commented out
# QCoreApplication.processEvents()
try:
task() # runs the task ON the GUI thread -> "frozen" window
finally:
self.accept()
```
**Cause:** after `show()` the GUI is not given time to paint itself
(`processEvents` is commented out) and then `task()` is run **blocking** the
interface thread. Result: the "Please wait" window can appear empty, fail to
repaint, and the app seems stuck (contributes to S1/the perception of a
freeze).
**Proposed fix:**
- Either run the task in a `TaskThread` (as `BalWaitingDialog` already does),
- or, if it must stay blocking, restore a `processEvents()` after `show()` so
the window is painted before the task.
---
### B7 — `closeEvent`/`hideEvent` with the thread cleanup commented out 🟠
**Where:** `dialogs.py:48-58` (`BalDialog`).
```python
def closeEvent(self, event):
self._stopping = True
#if self.thread:
# self.thread.stop() # <-- disabled
super().closeEvent(event)
```
**Cause:** when the dialog closes, any active threads are **not** stopped. They
keep running in the background, can write to already-destroyed widgets or hold
resources/connections → erratic behaviour until a restart (S2).
**Proposed fix:**
- Safely restore stopping the threads: `if self.thread:
self.thread.stop(); self.thread.wait()` with a guard on `None`.
---
### B8 — `BalBuildWillDialog.closeEvent` incomplete 🟠
**Where:** `dialogs.py:828-830`.
```python
def closeEvent(self, event):
self._stopping = True
self.thread.stop()
# missing self.thread.wait() and missing super().closeEvent(event)
```
**Cause:** `stop()` signals the stop but does not wait for the thread to finish
(`wait()`), and `super().closeEvent(event)` is not called: the close event is
not propagated correctly. Possible orphan threads and windows that do not close
cleanly.
**Proposed fix:**
- `self.thread.stop(); self.thread.wait(); super().closeEvent(event)` with a
guard on `self.thread is None`.
---
### B9 — `show()+raise_()` without `activateWindow()`/modality 🟡
**Where:** `dialogs.py:1121-1122` (e.g. `WillExecutorDialog`/detail).
```python
self.show()
self.raise_()
# missing self.activateWindow(); no modality set
```
**Cause:** `raise_()` raises the window in the stack but on some window
managers (including Windows) without `activateWindow()` it does not receive
focus and may still end up behind. Without modality, the user can go back to the
main window leaving the dialog hidden.
**Proposed fix:**
- Add `self.activateWindow()` after `raise_()`, and consider
`setWindowModality(Qt.WindowModal)` where it makes sense.
---
### B10 — Fragile multiple-window / multi-wallet handling 🟡
**Where:** `plugin.py:30-62` (`init_qt`), `get_window` (`plugin.py:109-115`).
**Cause:** the `bal_windows` map and the menu attachment rely on assumptions
(B4) and on iterating the menubar's children by name (`"&Tools"`), which is
sensitive to **localization** (you use `Locale: Italian_Italy`!). If the menu
is not named exactly `&Tools` in the current language, the attachment can fail
silently.
**Proposed fix:**
- Use the official `window.tools_menu` API (already used in `init_menubar`,
`plugin.py:79`) instead of looking up the menu by its translated title.
- Unify the creation/lookup of `BalWindow` on a stable key (B4).
---
## Proposed correction strategy (for Phase B/C)
In order to **not change the operating logic** and reduce the risks, I propose
to introduce a **single centralized point** for window management (a small
helper, e.g. `gui/qt/window_utils.py`) with functions such as:
- `show_modal(dialog)` → sets the correct parent, modality, `exec()`.
- `show_on_top(dialog)` → `show()` + `raise_()` + `activateWindow()` for the
few cases that must stay non-modal.
And then replace the scattered `.show()`/`.exec()` calls with these functions.
Advantages:
- the **business logic stays intact** (what the dialog does does not change);
- only the "how" it is shown/closed is touched;
- easier to test and to review (small, localized diff).
### Recommended order
1. **B3 + B4** (hot init + window key): fix the root of S2.
2. **B1 + B2 + B9** (parent/modality/z-order): fix S1.
3. **B5 + B7 + B8** (robust cleanup + threads): close the remaining S2 issues.
4. **B6 + B10** (waiting dialog + localized menus): polish.
---
## What is needed from you for Phase B/C
- Confirmation that I may modify the **GUI behaviour** (parent, modality,
cleanup, hot init) while keeping the business logic unchanged.
- Testing on **Electrum portable Windows** after each group of fixes, with a
description/screenshot of what happens (opening dialogs, hot enabling,
closing the wallet).
> Note: bugs B1B10 exist **identically in the original** — this refactor
> preserved them faithfully (that was the goal of the previous phase). Phase B/C
> fixes them.