Skip to content

fix(ui): build save/restore options per engine, not in a module global - #5876

Merged
springfall2008 merged 1 commit into
mainfrom
fix/save-restore-options-per-engine
Oct 31, 2026
Merged

springfall2008 merged 1 commit into
mainfrom
fix/save-restore-options-per-engine

Conversation

@mgazza

@mgazza mgazza commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

update_save_restore_list built the save/restore options by rebinding a module-level PREDBAT_SAVE_RESTORE with global, then assigned that list to the engine's config_index["saverestore"]["options"]. With more than one engine instance in a process, the last engine to run wins the module global, and options could be read from another engine's list.

It now builds a local list and assigns it to this engine's entry only. The module global is no longer written, and the shared default list in config.py is not mutated. Config entries are already deep-copied per engine, so the per-engine assignment cannot leak.

Tests

test_save_restore_options_are_per_engine in tests/test_fetch_config_options.py: two engines with different config folders each see only their own saved file, the lists are distinct, and neither the module global nor the default list changes. It fails on main and passes with this change.

--quick stops at test_data_age_metrics_round_trip, which fails the same way on main without this change.

🤖 Generated with Claude Code

update_save_restore_list rebound a module global and assigned that shared
list as the saverestore options, so several engine instances in one process
could see each other's saved settings files. Build a local list per call
and assign it to this engine's config entry only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the BOT_REVIEW Trigger an autotriage label Oct 30, 2026

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated comment from the triage bot.

Reviewed head 4a179ace61f7aedbecbf06381c39f12968a00497. The change is sound: 0 findings inline, 1 out-of-diff note in the body.

What I checked and confirmed by reading both versions (main via git show origin/main:... and this head):

  • The bug this fixes is real. On main update_save_restore_list() rebinds the module global and only later assigns it to the engine's config_index entry, with the os.walk in between (userinterface.py:689-702 on this head mirrors main's shape). It runs per engine at the end of fetch_config_options() every cycle (apps/predbat/fetch.py:4721) and from the saverestore select handler in an executor thread (apps/predbat/userinterface.py:415, 682-683), so two engines in one process can interleave across that window and the last rebind wins: engine A's dropdown gets engine B's saved file list, and since expose_config appends to the assigned list (apps/predbat/userinterface.py:626-628) as does the web config page (apps/predbat/web.py:4370-4372), a cross-assigned list also collects the other engine's mutations. Building a local list per engine removes both. Single-engine installs are behaviourally unchanged.
  • The body's deep-copy claim holds: apps/predbat/predbat.py:340 deep-copies CONFIG_ITEMS per engine and load_user_config() builds config_index from that copy (apps/predbat/userinterface.py:1172-1174), so the per-engine assignment cannot leak.
  • The test is registered in TEST_REGISTRY (apps/predbat/unit_test.py:575, slow=False so it runs under --quick), matches the harness call shape, and fails on main for the right reason: main's userinterface.py has no module-level PREDBAT_SAVE_RESTORE definition, so after the two engines run the attribute exists and the getattr(...) is global_before assertion can only pass once the function stops writing the global. The options-content assertions also discriminate (main returns distinct lists here because each engine rebinds a fresh list; the failing assertion is the one that pinpoints the removed shared state).
  • No docs impact: no setting, entity, service or user-visible default changes; nothing needs a docs/ update or a CONFIG_ITEMS entry.

apps/predbat/github.py:152 (untouched by this PR, same pattern, low) — download_predbat_releases() still does exactly what this PR removes from update_save_restore_list(): global PREDBAT_UPDATE_OPTIONS (apps/predbat/github.py:152), rebind (apps/predbat/github.py:185), appends (apps/predbat/github.py:197, 212) and item["options"] = PREDBAT_UPDATE_OPTIONS (apps/predbat/github.py:221), with the this_tag append at apps/predbat/github.py:227 mutating the list currently assigned to the update dropdown. Two engines in one process get the same last-rebind-wins race this fix closes for saverestore, on a path that also runs per engine per cycle. A follow-up applying the same local-list fix would close out the pattern; flagged here for visibility, not as a defect of this diff.

@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Oct 30, 2026
@springfall2008
springfall2008 merged commit fe4649c into main Oct 31, 2026
3 checks passed
@springfall2008
springfall2008 deleted the fix/save-restore-options-per-engine branch October 31, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants