Fix#1: Don't sleep after the final exhausted retry attempt in
_graph_request(). The sleep on the last iteration was pure overhead —
the loop exits immediately after, so the caller just waited an extra
backoff period for no reason. Guard with attempt < _MAX_RETRIES - 1.
Task 10: Add threading.Lock (_edits_lock) for all compound operations
on active_edits, which is accessed from both the UI thread and
background edit threads:
- __init__: declare self._edits_lock = threading.Lock()
- open_file: snapshot already_editing and at_limit under the lock,
then release before showing blocking UI dialogs
- process_file: wrap initial dict assignment, waiting=True, the
in-check+waiting=False, and in-check+del under the lock
- on_done_editing_clicked: lock the items() snapshot used to build
waiting_files, preventing iteration over a dict being mutated
Add 8 new unit tests (1 for Fix#1 sleep count, 7 for Task 10 lock).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add module-level _graph_request() that wraps requests.request() with:
- Up to 3 retries on HTTP 429 (rate limited) and 503 (unavailable)
- Exponential backoff capped at 60 s, honouring Retry-After header
- Default timeout=30 s injected via setdefault (caller can override)
Wire all 13 retry-eligible API calls through _graph_request(). The 3
file-upload requests.put(data=f) calls are kept direct since an open
stream cannot be re-read after the first attempt.
Add 9 unit tests covering: success path, 429/503 retry, Retry-After
header, max-retry exhaustion, timeout injection and override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add timeout= to every requests.get/post/put/patch/delete call so that
background threads cannot hang indefinitely when the network is stalled:
- timeout=30 on all API calls (delete, post, patch, get — 13 locations)
- timeout=120 on file upload calls (requests.put with data= — 3 locations)
to allow sufficient time for large file transfers
Add 1 new unit test that scans the source file and fails if any
requests.* call is missing a timeout= parameter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace 4 bare `except:` with `except Exception:` (load_settings,
get_txt, get_icon_idx_for_file, process_file cleanup block) so
SystemExit and KeyboardInterrupt are no longer swallowed
- Replace 2 print() calls with logger.error() (__init__ MSAL init,
ensure_valid_token) so errors appear in the configurable log output
- Sanitize item['name'] with os.path.basename() in on_download_clicked
and _download_folder_recursive_sync to prevent path traversal from
server-controlled filenames
- Add 8 new unit tests covering all Task 7 changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The S3 fix removed the only conditional that read is_breadcrumb. Remove
the parameter from the signature and its kwarg from the one call site in
the breadcrumb button handler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change nav_gen default from 0 to None in _fetch_list_contents_bg and
_finalize_list_loading. The guard is updated to only apply when a gen
is explicitly provided (`nav_gen is not None`).
Refresh call sites (lines 1515, 1529, 1538) pass no gen, so they receive
None and bypass the guard — their results are always applied. Navigation
calls still pass an explicit integer gen, so stale-navigation protection
remains fully intact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- S1: drop pulse_gauge(True) from inside pagination while-loops in
_fetch_sites_bg, _fetch_tree_children_bg, and _fetch_list_contents_bg;
the gauge is already running from the call before the loop
- S3: remove the is_breadcrumb bypass on the early-return guard so
clicking the already-active breadcrumb segment no longer fires a
redundant network request
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces self._nav_gen, incremented on every _navigate_to_item_data
call. The counter is threaded through _fetch_list_contents_bg and
checked in _finalize_list_loading: if the user navigated away while a
fetch was in flight, the stale results are silently discarded instead of
overwriting the active folder view and re-sorting its items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- C2: remove duplicate EVT_SIZE binding (on_status_bar_resize); merge
gauge repositioning into the single on_resize handler
- I4: position gauge correctly on first show by updating rect inside
pulse_gauge._do() when start=True, so no resize event is required
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- C1: initialize url=None before conditional in _fetch_tree_children_bg
to prevent UnboundLocalError on unexpected data types
- I2: translate System tab label via get_txt instead of hardcoded string
- I3: add status_loading_items to STRINGS (da+en) and use it in
_fetch_list_contents_bg instead of hardcoded Danish f-string
- S2: remove unreachable SITE branch from _append_list_items
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>