Build a single-file Streamlit dashboard, `connection_optimizer.py`, that optimizes an IDEA StatiCa steel **connection** by driving the **IDEA StatiCa Connection API** (a local REST service), not the desktop UI. Everything — UI, SDK calls, sweep logic, cost discovery — lives in this one file. `streamlit run connection_optimizer.py`. ## The workflow the engineer follows 1. **Connect** to the REST service (URL text box, default `http://localhost:5000`, "Connect" button). 2. **Open** an `.ideacon` (file uploader OR a typed path on this machine), then auto-load its parameters. 3. **Optimize ONE parameter at a time**: pick a parameter, sweep a range / list of values / boolean states, find the pass/fail boundary. 4. See a **history table** (every iteration: value, PASS/FAIL, max utilization %, production cost) and a **cost-vs-iteration plot** (green PASS dots, red FAIL x's, annotate the cheapest passing point). 5. Run **any number of sequential sweeps** — each starts from the best value found so far; history accumulates across all sweeps this session. 6. **The engineer chooses which iteration to save** — passing or failing. There is intentionally **NO "save the best" automation**; re-apply the chosen value and write a new `.ideacon`. Also give the sidebar a live "current parameter values" table plus a "Set a parameter" control that pins one parameter, calculates, logs a row, and **keeps the value regardless of pass/fail (no revert)** — used to fix one parameter before sweeping another. ## The exact SDK — use these calls verbatim, do NOT invent others Package: `ideastatica-connection-api` (its version must match the installed product). Import the SDK lazily inside a helper so the file still imports on a machine without the package (raise a clear error telling the user to `pip install ideastatica-connection-api==26.0.*`). ```python from ideastatica_connection_api.connection_api_service_attacher import ConnectionApiServiceAttacher from ideastatica_connection_api import IdeaParameterUpdate ``` Confirmed API surface (verified June 2026 — treat as ground truth, do not guess field names): - `ConnectionApiServiceAttacher(base_url).create_api_client()` → a **context manager**; enter it to get `api_client`. - `api_client.project.open_project_from_filepath(path)` - `api_client.project.active_project_id` - `api_client.project.download_project(project_id, out_path)` ← this is how you SAVE - `api_client.connection.get_connections(project_id)` → items with `.id` - `api_client.connection.get_production_cost(project_id, conn_id)` → a `ConProductionCost` object - `api_client.parameter.get_parameters(project_id, conn_id)` → list of `IdeaParameter` (fields: `key`, `value`, `expression`, `unit`, `description`, `lower_bound`, `upper_bound`) - `api_client.parameter.update(project_id, conn_id, [IdeaParameterUpdate(key=..., expression=...)])` — `expression` must ALWAYS be a string - `api_client.calculation.calculate(project_id, [conn_id])` → list of `ConResultSummary`; each has `.id` and `.result_summary` (a list of checks). Each check has `check_status`, `check_value`, `skipped`. ## Domain rules — get these exactly right - **PASS semantics.** The connection PASSES only if EVERY non-skipped check passes; **no checks → FAIL**. A check passes if `check_status` is `True`, or the string `"ok"/"pass"/"passed"/"true"` (case-insensitive). Max utilization = the largest `check_value` across non-skipped checks (None if none). - **Boolean parameters.** The API exposes no explicit type. Infer boolean from a native `bool` or a `"true"`/`"false"` token in `value`/`expression`. Store booleans internally as float `1.0`/`0.0` so all float plumbing works. When writing a boolean, set `expression` to `"true"`/`"false"`. **Flag in a comment that boolean writes are UNVERIFIED against a live service** — the API may want `"1"`/`"0"`; numeric is the safe fallback. - **Production cost field is UNDOCUMENTED.** `ConProductionCost` field names aren't in the public API. Discover the total at runtime: use `to_dict()` if present (strip leading `_` from keys), else `vars()`, else scan public attrs. Then prefer a numeric field whose name contains `"total"` → else `"cost"`/`"price"` → else sum all numeric fields as a proxy → else report "cost unavailable." Cache the discovered field name for the axis label; allow pinning it. - **Sweep boundary logic** (one parameter, injected callables so it's testable without the SDK): - `DECREASING` (smaller = cheaper, e.g. plate thickness): walk high→low while passing; on the **first FAIL**, optimum = the last passing value. - `INCREASING` (search upward for first adequate size): walk low→high; **stop at the first value that PASSES**. - Unordered **list of values** and **booleans**: evaluate EVERY value (no early stop), report the **cheapest PASSING** one (ties → first; passing-but-no-cost → first passing; nothing passes → None). - After a sweep, leave the connection set to the optimum so the next sweep builds on it. - Range builder: inclusive `lower`→`upper` in `step` increments, ordered by direction, floating-point safe (guarantee the exact upper bound is present). Validate: `step > 0`, `upper >= lower`. - List parser: split on commas/spaces/newlines, floats only, preserve order, dedupe, clear error on a non-numeric token. ## Robustness / UX - Wrap every SDK call so failures raise one clear `ConnectionApiError` with an actionable message (service unreachable → tell them to start `IdeaStatiCa.ConnectionRestApi.exe` and check `/heartbeat`; import failure → tell them to install the matching SDK version). - Streamlit gotcha: the **sidebar renders before button handlers run**, so after any mutation, re-read live parameter values from the connection and `st.rerun()` so the sidebar reflects real state. Keep a success message across the rerun. - Show a progress bar during the sweep with per-value PASS/FAIL text. - Use plotly for the cost plot; fall back to `st.scatter_chart` if plotly isn't installed. Keep the sweep functions pure (inject `set_param`, `calculate`, `cost`, `now` callables) so they're unit-testable without the product. Comment the non-obvious domain decisions. When done, let me know how to start the app.