Spaces:
Sleeping
Save & Reload Sessions
Overview
Co-Study4Grid supports saving and reloading full analysis sessions:
- Save Results exports the complete state (configuration, contingency, active recommender model, actions, combined pairs, overflow graph) to a session folder
- Reload Session restores a previously saved session, bringing the UI back to the state when it was saved
What is saved
session.json— all inputs, outputs, user decisions, combined action pairs, per-action enrichment details (load_shedding_details/curtailment_details/redispatch_details/pst_details/lines_overloaded_after), the active recommender model that produced the suggestions (analysis.active_model, mirrored atconfiguration.model), and — when available — the per-element sidebar sticky-header loading ratios (n_overloads_rho/n1_overloads_rho).interaction_log.json— timestamped log of every user interaction, suitable for automated session replay (see docs/features/interaction-logging.md)<overflow>.html— a copy of the interactive overflow graph viewer (when an analysis has been run). Legacy sessions saved before the HTML switch contain<overflow>.pdfinstead; reload handles both transparently.
All files are written to a session folder named costudy4grid_session_<contingency>_<timestamp>/ inside the configured Output Folder Path.
Setup: Configure the Output Folder
Open Settings → Paths and set the Output Folder Path:
Settings → gear icon → Paths tab → Output Folder Path
Without an output folder: Save falls back to browser JSON download. Reload is not available.
Banner
| Button | Description |
|---|---|
| Load Study | Load/reload the network and configuration |
| Save Results | Save the current session (disabled until a contingency is selected) |
| Reload Session | Restore a previously saved session from the output folder |
| Settings | Open settings modal |
How to Save
- Open Settings → Paths and configure Action Dictionary File Path and Output Folder Path.
- Open Settings → Recommender and pick the recommendation model (it can also be swapped later from the model dropdown above the Analyze & Suggest button — same persisted
recommenderModelstate). The selection is captured at save time underconfiguration.model; the model the backend actually executed is captured separately underanalysis.active_model(the two may differ when an unknown name silently falls back to the default). - Load a study (click Load Study).
- Select a contingency in the Select Contingency box.
- Optionally run analysis, select/reject actions, simulate manual/combined actions.
- Click Save Results in the header.
Output
<output_folder_path>/
costudy4grid_session_LINE_XYZ_2026-03-11T14-23-05/
session.json
interaction_log.json <- replay-ready event log
overflow_abc123.html <- copy of the interactive overflow graph
(.pdf on legacy sessions saved before
the HTML switch; both are reloadable)
How to Reload
- Ensure the Output Folder Path is configured in Settings → Paths.
- Click Reload Session in the banner.
- A modal lists all saved sessions (most recent first).
- Click on a session name to restore it.
What happens on reload
- Configuration is restored (all paths and algorithm parameters, including
min_load_sheddingandmin_renewable_curtailment_actions; the active recommendermodelandcompute_overflow_graphtoggle are also restored fromconfiguration.model/configuration.compute_overflow_graphwhen present) committedNetworkPathRefis set to the restorednetwork_path— this gates the "Change Network?" confirmation dialog the next time the user edits the Header network input. Without this the dialog would either misfire (empty ref) or fire against a stale previous value.- Network is loaded on the backend with the saved configuration, including the recommender model selection so that any subsequent run-analysis call uses the same model as the saved session
- Monitored-line set + computed-pair cache —
useSessioncallsPOST /api/restore-analysis-contextwith the savedlines_we_care_aboutandcomputed_pairsso any subsequent simulate-action uses the SAME monitored-line policy as the original study instead of the backend default. Wrapped in try/catch so an offline backend does not abort the reload. - N-1 diagram fetch —
restoringSessionRef.current = trueis set beforesetSelectedBranch(disconnected_element)so the N-1 effect inApp.tsxbypasses itshasAnalysisState()short-circuit (the restored actions would otherwise cause the effect to skip the fetch, leaving the N-1 tab blank and the N-1 Overloads panel empty).clearContingencyState()is also skipped on restore so the just-restored actions / overloads / result survive. Active-tab switching is suppressed so the user lands on whichever tab the VisualizationPanel default resolves to given the restored state. - Overflow graph PDF is restored (backend copies the PDF from the session folder back to
Overflow_Graph/if missing, so the iframe URL resolves) - Action cards are displayed with their saved data — rho values, status tags, and all enrichment fields (
load_shedding_details,curtailment_details,redispatch_details,pst_details,lines_overloaded_after) so the PST / load-shedding / curtailment / redispatch editor cards render their inputs populated and the Remedial Action tab draws its post-action overload halos - Combined pairs are restored in the Combine Actions modal. Estimation-only combined entries (
"act_a+act_b"withis_estimated: trueand no manual simulation) are filtered out of the top-levelactionsmap but survive undercombined_actions. - Action status flags (
is_selected,is_suggested,is_rejected,is_manually_simulated) are partitioned back into the fourSet<string>state variables used by the Selected / Rejected / Manual buckets. - No action card is active initially — no re-simulation until the user clicks one
- When the user clicks an action card, the action is simulated on-demand to generate its diagram
Key design decision: Actions are not re-simulated on reload. The saved rho values, status tags, enrichment details, combined pairs and the active recommender model are displayed immediately. Simulation only happens when the user actively selects an action card to view its diagram.
What is NOT persisted
Some pieces of in-memory state are deliberately ephemeral and will reset on reload:
- Per-card edit state (
cardEditMw,cardEditTap): the raw, uncommitted values in the action card inputs. Only the committed, re-simulated result survives — persisted aspst_details.tap_position/load_shedding_details[].shedded_mw/curtailment_details[].curtailed_mw. A replay agent that wants to reproduce exact keystrokes must consume theaction_mw_resimulated/pst_tap_resimulatedevents frominteraction_log.jsoninstead. - Detached / tied visualization tab state (PRs #84/#85):
detachedTabsand the tied-tab registry are not insession.json. Detaching a tab does not change any analysis result, so reload intentionally starts with all tabs attached to the main window. A replay that needs to reproduce detach / reattach / tie / untie must stream the matching events frominteraction_log.json. - Overflow Analysis tab UI state:
overflowPinsEnabled(📌 Pins toggle),overflowLayoutMode(Hierarchical / Geo), the iframe layer-toggle checkboxes, and the sharedoverviewFilters(category chips / threshold / action-type / Show unsimulated) are deliberately ephemeral. On reload the toggle starts OFF (auto-disabled whileresult.actionsis empty during the brief restore window), the layout mode resets to the backend default, and the filters reset toDEFAULT_ACTION_OVERVIEW_FILTERS. A replay that needs to reproduce the operator's exact overflow-tab navigation must streamoverflow_pins_toggled/overflow_layout_mode_toggled/overflow_layer_toggled/overflow_select_all_layers/overflow_pin_clicked/overflow_pin_double_clicked/overflow_node_double_clicked/overview_filter_changed/overview_unsimulated_toggledevents frominteraction_log.json. - Live diagram rho ratios are not threaded back from
session.overloads.n1_overloads_rhointo then1Diagramobject — they come from the re-fetched N-1 payload aftersetSelectedBranchfires. The persisted rho arrays are primarily useful for inspection of standalonesession.jsondumps and for replay agents that don't re-run the backend.
JSON Structure
{
"saved_at": "2026-03-11T14:23:05.123Z",
"configuration": {
"network_path": "/data/bare_env_...",
"action_file_path": "/data/actions.json",
"layout_path": "/data/grid_layout.json",
"min_line_reconnections": 2.0,
"min_close_coupling": 3.0,
"min_open_coupling": 2.0,
"min_line_disconnections": 3.0,
"min_pst": 1.0,
"min_load_shedding": 2.5,
"min_renewable_curtailment_actions": 1.25,
"min_redispatch": 1.0,
"allowed_action_types": [],
"n_prioritized_actions": 10,
"lines_monitoring_path": "/data/monitoring.csv",
"monitoring_factor": 0.95,
"pre_existing_overload_threshold": 0.02,
"ignore_reconnections": false,
"pypowsybl_fast_mode": true,
"model": "expert",
"compute_overflow_graph": true
},
"contingency": {
"disconnected_element": "LINE_XYZ",
"selected_overloads": ["LINE_A", "LINE_B"],
"monitor_deselected": false
},
"overloads": {
"n_overloads": ["LINE_PRE"],
"n1_overloads": ["LINE_A", "LINE_B", "LINE_C"],
"resolved_overloads": ["LINE_A", "LINE_B"],
"n_overloads_rho": [1.04],
"n1_overloads_rho": [1.23, 1.17, 1.07]
},
"overflow_graph": {
"pdf_url": "/results/pdf/overflow_abc123.html",
"pdf_path": "/home/.../Overflow_Graph/overflow_abc123.html"
},
"analysis": {
"message": "Found 5 prioritized actions",
"dc_fallback": false,
"active_model": "expert",
"compute_overflow_graph": true,
"action_scores": { ... },
"lines_we_care_about": ["LINE_A", "LINE_B", "LINE_C"],
"computed_pairs": { "act1+act2": { "max_rho": 0.87, "max_rho_line": "LINE_C" } },
"actions": {
"action_id": {
"description_unitaire": "Open switch SW_42",
"rho_before": [1.12, 1.05, 0.95],
"rho_after": [0.88, 0.92, 0.91],
"max_rho": 0.92,
"max_rho_line": "LINE_B",
"is_rho_reduction": true,
"estimated_max_rho": 0.90,
"estimated_max_rho_line": "LINE_B",
"is_islanded": false,
"non_convergence": null,
"action_topology": {
"lines_ex_bus": {},
"lines_or_bus": {},
"gens_bus": {},
"loads_bus": {},
"loads_p": {},
"gens_p": {},
"voltage_level_id": "VL_HUB"
},
"lines_overloaded_after": ["LINE_C"],
"load_shedding_details": [
{ "load_name": "LOAD_1", "voltage_level_id": "VL_1", "shedded_mw": 4.2 }
],
"curtailment_details": [
{ "gen_name": "WIND_1", "voltage_level_id": "VL_2", "curtailed_mw": 7.5 }
],
"redispatch_details": [
{ "gen_name": "GEN_3", "voltage_level_id": "VL_3", "delta_mw": -6.0, "target_mw": 42.0, "direction": "down" }
],
"pst_details": [
{ "pst_name": "PST_A", "tap_position": 5, "low_tap": -16, "high_tap": 16 }
],
"origin": "expert",
"status": {
"is_selected": true,
"is_suggested": true,
"is_rejected": false,
"is_manually_simulated": false
}
}
},
"combined_actions": {
"act1+act2": {
"action1_id": "act1",
"action2_id": "act2",
"betas": [0.5, 0.3],
"max_rho": 0.85,
"max_rho_line": "LINE_C",
"is_rho_reduction": true,
"description": "Combined act1 + act2",
"estimated_max_rho": 0.82,
"estimated_max_rho_line": "LINE_C",
"is_islanded": false,
"simulated_max_rho": 0.83,
"simulated_max_rho_line": "LINE_C",
"is_simulated": true
}
}
}
}
Field Reference
configuration
All settings active when the analysis was run. Matches the fields sent to POST /api/config.
| Field | Description |
|---|---|
layout_path |
Path to the grid layout file (node positions) |
min_pst |
Minimum PST actions threshold |
min_load_shedding |
Minimum load-shedding actions threshold (PR #73/#78 — new loads_p format) |
min_renewable_curtailment_actions |
Minimum renewable-curtailment actions threshold (PR #73/#78 — new gens_p format) |
allowed_action_types |
(optional) The "Restrict to action types" recommender control — the subset of action families the recommender is allowed to propose. An empty array means all families are enabled (no restriction); a non-empty subset restricts the recommender to only those types. Persisted in the snapshot and restored on reload (and backed up for Settings-cancel). Mirrors SettingsState.allowedActionTypes. Defaults to []. |
model |
(optional) Registry key of the recommender model selected at save time. Captures what the operator picked in the Settings → Recommender tab — useful for replay agents that want to reproduce the same model selection before re-running analysis. Defaults to "expert". See docs/backend/recommender_models.md. |
compute_overflow_graph |
(optional) Whether the Compute Overflow Graph toggle was ON at save time. Locked-on for models with requires_overflow_graph=true; opt-in for the others. |
(Other fields match the Recommender and Configurations settings tabs.)
Legacy sessions:
min_load_shedding/min_renewable_curtailment_actions/model/compute_overflow_graphwere each added to the schema after the original release. Older session dumps that predate any of these fields are restored with sensible defaults (0.0for the threshold fields,"expert"formodel,trueforcompute_overflow_graph) so reload remains byte-compatible.
contingency
| Field | Description |
|---|---|
disconnected_element |
The branch/line that was disconnected for N-1 simulation |
selected_overloads |
Overloads the user chose to resolve (used as selected_overloads in step 2) |
monitor_deselected |
Whether deselected overloads were still monitored in rho calculations |
overloads
| Field | Source |
|---|---|
n_overloads |
Overloaded lines detected in the N (base) state |
n1_overloads |
Overloaded lines detected in the N-1 (post-contingency) state |
resolved_overloads |
Lines the recommender was asked to resolve (result.lines_overloaded) |
n_overloads_rho |
(optional) Per-element loading ratio (max|i|/permanent_limit) parallel to n_overloads. Feeds the sidebar sticky-header percentages (PR #88). |
n1_overloads_rho |
(optional) Per-element loading ratio parallel to n1_overloads. |
Length guard:
buildSessionResultonly persists an*_rhoarray when its length matches the corresponding name array. A shorter or missing array means the N / N-1 diagram payload predates the rho feature — the field is omitted from the JSON rather than saved misaligned. Older session dumps without these fields reload correctly; the sticky header renders its percentages from the freshly re-fetched N-1 diagram instead.
overflow_graph
URL and file path to the overflow graph artifact generated by expert_op4grid_recommender. With the default VISUALIZATION_FORMAT="html" this is the interactive alphaDeesp viewer (<overflow>.html) embedded by the Overflow Analysis tab via the inject_overlay middleware (see docs/features/interactive-overflow-analysis.md). Legacy sessions saved before the HTML switch carry <overflow>.pdf; reload globs both extensions and resolves the basename stored in pdf_url. The field names (pdf_url / pdf_path) are kept for backwards compatibility — they hold the actual file URL / absolute path regardless of extension. null if analysis was not run.
analysis
| Field | Description |
|---|---|
message |
Human-readable summary from the recommender |
dc_fallback |
true if AC load flow did not converge and DC was used |
active_model |
(optional) Registry key of the recommender model the backend actually executed (echoed in the result event from /api/run-analysis-step2). Differs from configuration.model (= what the operator picked) only when an unknown name silently fell back to the default — this field is the ground truth. See docs/backend/recommender_models.md. |
compute_overflow_graph |
(optional) Whether step-2 overflow graph was actually computed for this run. True for any model with requires_overflow_graph=true, OR when the operator opted in. Useful when reloading a session to know whether the Overflow Analysis tab will have content. |
step1_time |
(optional) Wall-clock (seconds) of run_analysis_step1 — the contingency simulation + overload detection that runs before step 2. May be near-zero when the post-contingency observation was pre-warmed by the N-1 diagram fetch. |
overflow_graph_time |
(optional) Wall-clock (seconds) of the overflow-graph build phase in step 2: _narrow_context_to_selected_overloads + run_analysis_step2_graph + the PDF mtime poll. null when the active model does not consume the overflow graph (e.g. random); 0.0 on a cached re-run (model swap fast path). |
action_prediction_time |
(optional) Wall-clock (seconds) of the recommender's intrinsic recommend() call. For Expert-style models this still includes the internal candidate simulation done to score topology actions; what it excludes is the per-prioritized-action re-simulation. |
assessment_time |
(optional) Wall-clock (seconds) of reassess_prioritized_actions + compute_combined_pairs: each prioritized action is re-simulated to compute its final rho_before/rho_after, then combined-pair scores are computed. Scales linearly with the number of prioritized actions. |
enrichment_time |
(optional) Wall-clock (seconds) of the Co-Study4Grid post-processing layer (_enrich_actions + _augment_combined_actions_with_target_max_rho + _compute_mw_start_for_scores). Distinct from assessment_time because it's pure UI-facing decoration, not part of the upstream library's discovery pipeline. |
wall_clock_time |
(optional) Frontend-measured wall-clock (seconds) from the "Analyze & Suggest" click until the result event arrives. Captures every backend stage + network round-trip + NDJSON streaming overhead. The "Other (network / streaming)" residual in the tooltip is wall_clock_time − sum(stage timings). |
action_scores |
Raw scoring metrics returned by the recommender (varies by version) |
actions |
Map of action ID -> enriched action data (see below) |
combined_actions |
Map of combined pair ID -> computed pair data (see below) |
lines_we_care_about |
(optional) Monitored-line set captured at analysis time. Re-pushed to the backend on reload via POST /api/restore-analysis-context so subsequent simulate-action calls use the same set instead of the backend default (see "What happens on reload" step 4). Older session dumps without the field reload unchanged and silently skip the context push. |
computed_pairs |
(optional) Superposition-computed pair cache keyed by "actionA+actionB". Re-pushed alongside lines_we_care_about so the Combine modal does not re-score every pair from scratch after reload. |
null when no analysis has been run.
The six timing fields (step1_time, overflow_graph_time,
action_prediction_time, assessment_time, enrichment_time,
wall_clock_time) drive the "Suggestions produced by <model> in
<X>s ⓘ" line in the ActionFeed sidebar and the
Total execution time: \<X\>s subtitle inside the Overflow Analysis
iframe. They are persisted verbatim and restored as-is on reload so the
operator sees the same numbers without re-running the analysis. Saved
sessions from before the breakdown landed have these fields absent;
the reload path treats absent and null differently — see
docs/backend/recommender_models.md § Execution-time breakdown.
Action fields
Each action entry mirrors the ActionDetail type plus a status object:
| Field | Description |
|---|---|
description_unitaire |
Human-readable description of the topology change |
rho_before |
Current-ratio array before the action (one value per monitored line) |
rho_after |
Current-ratio array after the action |
max_rho |
Maximum rho across all monitored lines after the action |
max_rho_line |
Equipment ID of the most-loaded line after the action |
is_rho_reduction |
true if the action improves (reduces) the worst-case loading |
estimated_max_rho |
Estimated max rho from superposition (for combined actions) |
estimated_max_rho_line |
Line with estimated max rho |
is_islanded |
true if the action causes network islanding |
n_components |
Number of connected components after action |
disconnected_mw |
MW disconnected by islanding |
non_convergence |
Error message when the AC load flow did not converge; null otherwise |
action_topology |
Bus assignments changed by the action (lines_ex_bus, lines_or_bus, loads_p, gens_p, etc.) plus the optional voltage_level_id hint surfaced from the dict_action entry (pypowsybl switch-based / coupling shape) — used by the Action Overview pin anchor and the ActionCard VL chip. |
lines_overloaded_after |
Overloaded lines remaining after the action is applied — drives the post-action overload halos on the Remedial Action NAD / SLD tab (PR #83). |
load_shedding_details |
Per-load MW values for the load-shedding editor card. Array of { load_name, voltage_level_id, shedded_mw } (PR #73). |
curtailment_details |
Per-generator MW values for the renewable-curtailment editor card. Array of { gen_name, voltage_level_id, curtailed_mw } (PR #73). |
redispatch_details |
Per-generator values for the redispatch editor card — sibling of curtailment_details. Array of { gen_name, voltage_level_id, delta_mw, target_mw, direction }: delta_mw is the signed MW change (> 0 raises, < 0 lowers), target_mw the resulting production magnitude, direction is "up" / "down". May also carry optional current_mw / max_raise_mw / max_lower_mw bounds. |
pst_details |
PST editor state: array of { pst_name, tap_position, low_tap, high_tap }. low_tap / high_tap may be null if the network manager did not expose bounds (PR #78). |
origin |
(optional) Provenance of the action card — "user" (the operator simulated it themselves via the manual search / "Make a first guess") or a recommender model id ("expert", "random_overflow", … — the active_model that produced / scored it; this also covers an unsimulated pin the operator materialised). Distinct from the is_manually_simulated status flag, which also flips true when the operator merely stars a recommender suggestion. Surfaced as the "Source" row in the unfolded action card. Legacy dumps that predate the field get an origin derived on reload from the status flags + analysis.active_model (is_manually_simulated → "user", else is_suggested → active_model). |
Why the enrichment fields matter on reload: the PST / load-shedding / curtailment / redispatch editor cards read directly from
pst_details/load_shedding_details/curtailment_details/redispatch_detailsto populate their inputs, and the Remedial Action tab useslines_overloaded_afterto clone the post-action overload halos. Before PR #83, these fields were written tosession.jsonbut silently dropped on reload, which left the editor cards empty and wiped the overload halos until the user re-ran analysis.redispatch_detailslater hit the exact same "save-drop" trap (written bybuildSessionResultbut dropped on the reload path) until it was wired throughhandleRestoreSessionalongside the others. They are now restored in full.
Action status tags
| Tag | Meaning |
|---|---|
is_suggested |
The recommender ever returned this action for the current contingency |
is_selected |
The user starred / favorited this action |
is_rejected |
The user explicitly rejected this action |
is_manually_simulated |
The user added this action via the manual search / simulation flow |
originvs.is_manually_simulated: the status flag is an interaction record — it flipstrueboth when the operator manually simulates an action AND (historically) when they star a recommender suggestion.originis the provenance — set once at creation, never changed by starring / re-simulating. A starred recommender suggestion hasis_manually_simulated-adjacent state but keepsorigin: "<model>".
combined_actions (Computed Pairs)
Each combined action entry represents a pair of actions estimated by linear superposition:
| Field | Description |
|---|---|
action1_id |
First action in the pair |
action2_id |
Second action in the pair |
betas |
Superposition coefficients |
max_rho |
Max loading from estimation |
max_rho_line |
Line with max loading from estimation |
is_rho_reduction |
Whether the pair reduces worst-case loading |
description |
Human-readable description |
estimated_max_rho |
Estimated max rho from superposition |
estimated_max_rho_line |
Line with estimated max rho |
is_islanded |
Whether estimation detected islanding |
disconnected_mw |
MW disconnected by islanding |
simulated_max_rho |
Max rho from full simulation (null if not simulated) |
simulated_max_rho_line |
Line with simulated max rho |
is_simulated |
true if the user ran a full simulation for this pair |
API Endpoints
Save
POST /api/save-session — Save session files to disk
| Field | Type | Description |
|---|---|---|
session_name |
str |
Folder name to create |
json_content |
str |
Serialised session.json content |
pdf_path |
str | null |
Absolute path to the overflow PDF to copy |
output_folder_path |
str |
Parent output directory |
interaction_log |
str | null |
Serialised interaction_log.json content (see docs/features/interaction-logging.md). When non-null, the backend writes it to <session_folder>/interaction_log.json. |
Returns { "session_folder": "<path>", "pdf_copied": bool }.
List
GET /api/list-sessions?folder_path=<path> — List saved sessions
Returns { "sessions": ["session_name_1", "session_name_2", ...] } sorted most-recent first.
Load
POST /api/load-session — Read a session file
| Field | Type | Description |
|---|---|---|
folder_path |
str |
Parent output directory |
session_name |
str |
Session folder name |
Returns the parsed session.json content. Also restores the overflow PDF to Overflow_Graph/ if it was removed since saving.
Path validation (session_name)
session_name is joined onto the caller-supplied folder to build the
session directory, so both save-session and load-session route it
through _safe_session_dir (main.py) first. It must be a single
path component: a .., a path separator, or an absolute path (which
os.path.join silently honours, dropping the base) is rejected with
HTTP 400 {"detail": "Invalid session name", "code": "BAD_REQUEST"}
before any filesystem access, backed by a resolve()+relative_to()
containment check that mirrors the /results/pdf traversal guard (QW7,
2026-07). Regression-guarded by TestSessionPathTraversal in
expert_backend/tests/test_api_endpoints.py.
Restore Analysis Context
POST /api/restore-analysis-context — Re-push the session's monitored-line set and computed-pair cache into the backend service's _analysis_context so any subsequent simulate-manual-action / compute-superposition call on the reloaded session uses the same policy as the original study. Called from useSession::handleRestoreSession right after the base-diagram Promise.all (step 4 of the reload flow).
| Field | Type | Description |
|---|---|---|
lines_we_care_about |
list[str] | null |
Monitored-line set saved in session.analysis.lines_we_care_about |
disconnected_element |
str | null |
Saved contingency — helps the backend disambiguate between N-1 variants |
lines_overloaded |
list[str] | null |
Saved session.overloads.resolved_overloads — the N-1 overloads the recommender targeted |
computed_pairs |
dict | null |
Superposition-computed pair cache from session.analysis.computed_pairs |
Returns { "status": "success", "lines_we_care_about_count": int, "computed_pairs_count": int }. Wrapped in try/catch on the frontend so a 4xx response does not abort the reload — the user loses the per-study monitored set but the rest of the session state is preserved.
Recommender model persistence
The pluggable recommender selection ships through the session JSON via two distinct fields, captured at different points of the request lifecycle so reloaded sessions can distinguish operator intent from backend behaviour:
configuration.model— captures what the operator picked in the Settings → Recommender dropdown at save time. Restored on reload so the UI shows the same selection.analysis.active_model— captures what the backend actually executed (echoed in theresultevent from/api/run-analysis-step2). Differs fromconfiguration.modelonly when an unknown name silently fell back to the default — this field is the ground truth and what the action cards were produced by.configuration.compute_overflow_graph/analysis.compute_overflow_graph— same split for the Compute-Overflow-Graph toggle.
Older session dumps that predate these fields reload unchanged and
silently fall back to "expert" / true defaults so the legacy
user-experience is preserved.
Full pluggable-recommender reference (registry, three-layer filter chain, step-by-step plug-in guide, troubleshooting): docs/backend/recommender_models.md.
Implementation Details
Frontend (hooks/useSession.ts, utils/sessionUtils.ts)
Save flow:
handleSaveResultsinhooks/useSession.tscallsbuildSessionResult(input: SessionInput)to build the JSON- The
SessionInputit passes includesnOverloadsRho/n1OverloadsRhofromnDiagram.lines_overloaded_rho/n1Diagram.lines_overloaded_rho(when present) buildSessionResultwrites them intosession.overloads.n_overloads_rho/n1_overloads_rhoonly if the arrays match the corresponding name arrays in length (length guard, see "Overloads" section above)buildSessionResultalso propagatesresult.active_modelandresult.compute_overflow_graph(echoed by the backend in theresultevent) intosession.analysis.active_model/session.analysis.compute_overflow_graph; the capturedrecommenderModel/computeOverflowGraphfromuseSettingsgo intosession.configuration.model/session.configuration.compute_overflow_graph(conditional spread — omitted when undefined so legacy callers stay byte-compatible)- If
outputFolderPathis set: callsapi.saveSession(...)with the JSON and the currentinteractionLogger.getLog()serialised asinteraction_log→ backend writes bothsession.jsonandinteraction_log.json - If empty: falls back to browser download (no
interaction_log.jsonis written in that case)
Reload flow:
handleOpenReloadModalcallsapi.listSessions(outputFolderPath)and displays the modal- User clicks a session →
handleRestoreSession(sessionName, restoreContext):- Calls
api.loadSession(...)to fetch the session JSON - Restores every configuration field — including
min_load_shedding,min_renewable_curtailment_actions, the active recommendermodelandcompute_overflow_graphtoggle, falling back to defaults (0.0/"expert"/true) when absent — via the setters inrestoreContext - Calls
api.updateConfig(...)to load the network, forwarding every threshold and the model selection - Updates
committedNetworkPathRef.currentto the restorednetwork_path - Fetches branches, voltage levels, nominal voltages and the base diagram in parallel
- Calls
api.restoreAnalysisContext(...)to re-pushlines_we_care_about+computed_pairsinto the backend service (wrapped in try/catch — failures log a warning but let the reload complete) - Rebuilds each
ActionDetailfrom itsSavedActionEntry, includinglines_overloaded_after,load_shedding_details,curtailment_details,redispatch_details,pst_detailsandorigin. Estimation-only combined entries (id.includes('+') && is_estimated && !is_manually_simulated) are filtered out of the top-level actions map but kept undercombined_actions. - Re-attaches
analysis.active_model+analysis.compute_overflow_graphonto the liveresultso the "Suggestions produced by <model>" reminder below the Suggested Actions tab header survives the reload — previously both were saved but never restored ontoresult, so the reminder vanished. For legacy dumps whose action entries predate theoriginfield, anoriginis derived here from the status flags +active_model(is_manually_simulated→"user", elseis_suggested→active_model). - Partitions action status flags into
selectedActionIds/rejectedActionIds/manuallyAddedIds/suggestedByRecommenderIds - Sets
restoringSessionRef.current = trueBEFOREsetSelectedBranch(...)so the N-1 fetch effect inApp.tsxcan tell "session restore" from "user changed contingency", bypass itshasAnalysisState()short-circuit, skipclearContingencyState()(which would wipe the just-restored state), and avoid forcing an active-tab switch. The ref is reset tofalsethe moment the effect consumes it. - No action card is active — diagrams are fetched on-demand when clicked
- Calls
handleActionSelectfalls back tosimulateManualActionifgetActionVariantDiagramfails (action not in backend memory after restore)
Session reload regression guard (2026-04-20): three visible symptoms were fixed in one pass by the ref-ordering + short-circuit-relaxation above: (a) empty N-1 diagram despite a selected contingency, (b) empty N / N-1 Overloads panel, (c) Overflow-Analysis tab unavailable because
result.pdf_urlwas wiped alongside the rest of the analysis state. The testuseSession.test.ts::sets restoringSessionRef BEFORE setSelectedBranchlocks the ref ordering in place.
SessionInput / RestoreContext signatures:
SessionInput(inutils/sessionUtils.ts) is the plain-data interface consumed bybuildSessionResult. It carries every configuration field, the action-status sets, the interaction log, the optionalnOverloadsRho/n1OverloadsRhoarrays, and the optionalrecommenderModel/computeOverflowGraphsnapshot fromuseSettings.RestoreContext(inhooks/useSession.ts) is the companion interface forhandleRestoreSession. It includescommittedNetworkPathRefso the restore path can update it in place — a missed setter would silently break the "Change Network?" confirmation dialog after a reload.
Standalone Interface (auto-generated)
The single-file HTML distribution is now produced by
npm run build:standalone → frontend/dist-standalone/standalone.html.
It compiles from the same frontend/src/ source tree, so save/reload
logic is inherited automatically (no manual mirroring). The former
hand-maintained standalone_interface.html was decommissioned
2026-04-20 (renamed to standalone_interface_legacy.html and committed
as a frozen snapshot — do NOT edit further).
Testing
frontend/src/utils/sessionUtils.test.ts (pure unit tests on buildSessionResult)
- Configuration fields including
layout_path,min_pst,min_load_shedding,min_renewable_curtailment_actions - Recommender model persistence:
result.active_modelandresult.compute_overflow_graphare written intosession.analysis.active_model/session.analysis.compute_overflow_graph;recommenderModelandcomputeOverflowGraphfromSessionInputgo tosession.configuration.model/session.configuration.compute_overflow_graph(conditional — omitted when undefined for legacy callers). - All four status tags independently computed
- Action origin:
detail.origin("user"/ a recommender model id) is serialised verbatim; leftundefinedfor legacy / un-tracked actions - Combined actions: serialised with estimation and simulation data
- Combined actions:
is_simulatedflag based on presence inresult.actions - Combined actions: empty object when no combined actions exist
- Edge cases for
is_suggested/is_manually_simulatedoverlap - Sticky-header rho persistence:
n_overloads_rho/n1_overloads_rhoare persisted when the rho arrays match the corresponding name array length- Both fields are omitted when lengths mismatch (guard against misaligned legacy data)
- Both fields are omitted when not provided at all (legacy save flow)
frontend/src/hooks/useSession.test.ts (handleRestoreSession tests)
A makeCtx() / makeSession() fixture pair makes each test self-contained:
- Full configuration restore — every setter is called with the saved value, including the new
min_load_shedding/min_renewable_curtailment_actionsthresholds - Legacy sessions (neither new threshold present) fall back to
0.0on reload committedNetworkPathRef.currentis set to the restorednetwork_path— regression guard for the "Change Network?" dialog misfireapi.updateConfigis called once with the full configuration payload- A
session_reloadedinteraction event is recorded with the session name - Empty
outputFolderPathshort-circuits the call: no API requests, no setters, ref untouched - Backend errors surface via
ctx.setError; the ref is left empty on failure - Enrichment field round-trip: a
captureRestoredResult()helper replays the functionalsetResultupdater against anullprevious state and assertsload_shedding_details,curtailment_details,redispatch_details,pst_detailsandlines_overloaded_afterall land on the restoredActionDetail - Recommender model restore:
analysis.active_model+analysis.compute_overflow_graphare re-attached onto the liveresult(regression guard for the vanished "Suggestions produced by <model>" reminder) - Action origin restore: a saved
originis restored verbatim; legacy entries that predate the field get anoriginderived from the status flags +analysis.active_model(is_manually_simulated→"user", elseis_suggested→active_model) - Action status flags partition correctly into the four
Set<string>state updates - Legacy action entries that omit enrichment fields don't crash — values come through as
undefined - Estimation-only combined entries are filtered out of top-level
actionsbut survive undercombined_actions /api/restore-analysis-contextis called withlines_we_care_about, the selected contingency andcomputed_pairson restore — regression guard for the silent monitored-line-set drift that used to happen after reload- The context push is skipped for legacy sessions that predate
lines_we_care_about - Backend-offline failures on
restoreAnalysisContextare swallowed so the reload still completes restoringSessionRef.currentflips totrueBEFOREsetSelectedBranchis called — regression guard for the N-1-diagram-not-fetched / overloads-not-populated / stale-Overflow-PDF trio that regressed when the App-level N-1 effect short-circuited on restored analysis state- Mixed-extension overflow restore (
/api/load-session): a session folder that contains either<overflow>.html(current interactive viewer) or<overflow>.pdf(legacy) must copy the matching file intoOverflow_Graph/sopdf_urlresolves. Guard:find_latest_pdf(expert_backend/services/analysis/pdf_watcher.py) globs both*.htmland*.pdf; the load-session glob inmain.pydoes the same and prefers the basename that matches the storedpdf_url. Added when switchingVISUALIZATION_FORMATto"html".
frontend/src/components/ActionFeed.test.tsx (re-simulation logging)
action_mw_resimulatedis recorded withtarget_mw === parseFloat(userInput)on load-shedding / curtailment re-simulatemanual_action_simulatedis NOT emitted on LS re-simulate (regression guard for the mistyped event that used to live inuseActions)pst_tap_resimulatedis recorded withtarget_tap === parseInt(userInput, 10)on PST re-simulatetarget_mwis normalised viaparseFloateven when the user types trailing zeros (5.400→5.4)
frontend/src/components/modals/SettingsModal.test.tsx (tab-change logging)
settings_tab_changedis logged with{ from_tab, to_tab }on every real transitionfrom_tabtracks the currently-active tab, not the initial one- Clicking the already-active tab does NOT emit a
settings_tab_changedevent (no-op skip) setSettingsTabis still called unconditionally on no-op clicks — pins the "setter always, logger only on transition" split behaviour
Run tests with:
cd frontend
npm run test