fix(uploaders): convert master/builder to single-file slot pattern

Multi-file uploaders trigger Windows browsers' folder-select mode
which bypasses the type filter. Switch to N single-file slots
(matching the working set comparator pattern) so the OS picker
stays in single-file mode where folder selection is impossible.
Default 5 slots, expandable up to 20 via dropdown.
This commit is contained in:
unknown
2026-05-06 17:13:02 +10:00
parent 995205ed16
commit 62848079b3
2 changed files with 76 additions and 51 deletions
+38 -25
View File
@@ -592,6 +592,7 @@ def _init_state():
"pb_portfolios": {}, "pb_portfolios": {},
"pb_lot_overrides": {}, # label → float multiplier "pb_lot_overrides": {}, # label → float multiplier
"pb_deposit": 10000.0, "pb_deposit": 10000.0,
"pb_n_slots": 5, # number of file upload slots shown
}.items(): }.items():
if k not in st.session_state: if k not in st.session_state:
st.session_state[k] = v st.session_state[k] = v
@@ -618,31 +619,43 @@ def render():
# ── Upload panel ───────────────────────────────────────────────────────── # ── Upload panel ─────────────────────────────────────────────────────────
with st.expander("📂 Upload Strategy Reports", with st.expander("📂 Upload Strategy Reports",
expanded=not bool(st.session_state.pb_uploaded_files)): expanded=not bool(st.session_state.pb_uploaded_files)):
st.caption("Accepts: `.htm` · `.html` · `.csv`") st.caption("Accepts: `.htm` · `.html` · `.csv` — one file per slot. "
uploaded = st.file_uploader( "Increase the slot count if you need more.")
"Select HTM, HTML or CSV files",
type=["htm", "html", "csv"], sc1, sc2 = st.columns([1, 5])
accept_multiple_files=True, key="pb_uploader", with sc1:
) n_slots = st.selectbox(
if uploaded: "Slots",
# Streamlit already filters by type, but keep a defensive check list(range(1, 21)),
# in case type filter is ever loosened index=max(0, st.session_state.pb_n_slots - 1),
rejected = [f.name for f in uploaded key="pb_n_slots_select",
if not f.name.lower().endswith((".htm",".html",".csv"))] )
if rejected: if n_slots != st.session_state.pb_n_slots:
st.warning(f"Ignored {len(rejected)} unsupported file(s): " st.session_state.pb_n_slots = n_slots
+ ", ".join(rejected[:5]) st.rerun()
+ ("" if len(rejected) > 5 else ""))
uploaded = [f for f in uploaded slots_per_row = 5
if f.name.lower().endswith((".htm",".html",".csv"))] for row_start in range(0, st.session_state.pb_n_slots, slots_per_row):
for f in uploaded: cols = st.columns(slots_per_row)
stem = os.path.splitext(f.name)[0] for j in range(slots_per_row):
if stem not in st.session_state.pb_uploaded_files: idx = row_start + j
df = _parse_uploaded(f) if idx >= st.session_state.pb_n_slots:
if df is not None: break
df = _ensure_columns(df, stem) with cols[j]:
st.session_state.pb_uploaded_files[stem] = df f = st.file_uploader(
st.success(f"✅ **{stem}** — {len(df):,} trades") f"File {idx + 1}",
type=["htm", "html", "csv"],
accept_multiple_files=False,
key=f"pb_upload_slot_{idx}",
)
if f is not None:
stem = os.path.splitext(f.name)[0]
if stem not in st.session_state.pb_uploaded_files:
df = _parse_uploaded(f)
if df is not None:
df = _ensure_columns(df, stem)
st.session_state.pb_uploaded_files[stem] = df
st.success(f"{stem}{len(df):,} trades")
if st.session_state.pb_uploaded_files: if st.session_state.pb_uploaded_files:
st.markdown("**Loaded strategies:**") st.markdown("**Loaded strategies:**")
+38 -26
View File
@@ -478,6 +478,7 @@ def _init_state():
"pm_thread_results": None, "pm_thread_results": None,
"pm_progress_q": None, "pm_progress_q": None,
"pm_uploader_key": 0, "pm_uploader_key": 0,
"pm_n_slots": 5,
}.items(): }.items():
if k not in st.session_state: if k not in st.session_state:
st.session_state[k] = v st.session_state[k] = v
@@ -544,32 +545,43 @@ def render():
# ── Upload ─────────────────────────────────────────────────────────────── # ── Upload ───────────────────────────────────────────────────────────────
with st.expander("📂 Upload Backtest Files", with st.expander("📂 Upload Backtest Files",
expanded=not bool(st.session_state.pm_files)): expanded=not bool(st.session_state.pm_files)):
st.caption("Accepts `.htm` · `.html` · `.csv` — selecting a folder will only " st.caption("Accepts `.htm` · `.html` · `.csv` — one file per slot. "
"import the supported files inside it (others are skipped).") "Increase the slot count if you need more.")
uploaded = st.file_uploader(
"Select files", sc1, _ = st.columns([1, 5])
type=["htm", "html", "csv"], with sc1:
accept_multiple_files=True, n_slots = st.selectbox(
key=f"pm_uploader_{st.session_state.pm_uploader_key}", "Slots",
) list(range(1, 21)),
if uploaded: index=max(0, st.session_state.pm_n_slots - 1),
# Some browsers ignore the type filter when a folder is selected — key="pm_n_slots_select",
# filter again here and report what was skipped. )
valid = [f for f in uploaded if n_slots != st.session_state.pm_n_slots:
if f.name.lower().endswith((".htm",".html",".csv"))] st.session_state.pm_n_slots = n_slots
rejected = len(uploaded) - len(valid) st.rerun()
if rejected:
st.warning(f"⚠️ Skipped {rejected} non-supported file(s). " slots_per_row = 5
f"Importing {len(valid)} valid file(s).") for row_start in range(0, st.session_state.pm_n_slots, slots_per_row):
uploaded = valid cols = st.columns(slots_per_row)
for f in uploaded: for j in range(slots_per_row):
stem = os.path.splitext(f.name)[0] idx = row_start + j
if stem not in st.session_state.pm_files: if idx >= st.session_state.pm_n_slots:
df = _parse_file(f) break
if df is not None: with cols[j]:
df = _normalise(df.copy(), stem) f = st.file_uploader(
st.session_state.pm_files[stem] = df f"File {idx + 1}",
st.success(f"✅ **{stem}** — {len(df):,} trades") type=["htm", "html", "csv"],
accept_multiple_files=False,
key=f"pm_upload_slot_{st.session_state.pm_uploader_key}_{idx}",
)
if f is not None:
stem = os.path.splitext(f.name)[0]
if stem not in st.session_state.pm_files:
df = _parse_file(f)
if df is not None:
df = _normalise(df.copy(), stem)
st.session_state.pm_files[stem] = df
st.success(f"{stem}{len(df):,} trades")
if st.session_state.pm_files: if st.session_state.pm_files:
# Clear all button # Clear all button