Skip to content

Add sticky contingency/overloads header with loading percentages - #88

Merged
marota merged 4 commits into
mainfrom
claude/contingency-zoom-feature-jhqsE
Apr 14, 2026
Merged

marota merged 4 commits into
mainfrom
claude/contingency-zoom-feature-jhqsE

Conversation

@marota

@marota marota commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a compact sticky header in the sidebar that keeps the selected contingency and N-1 overloads visible while scrolling through the action feed. It also adds loading ratio (rho) percentages next to overloaded lines throughout the UI, and removes the redundant "Loading before" display from individual action cards.

Key Changes

  • Sticky Contingency & Overloads Header: Added a non-scrolling sticky strip at the top of the sidebar that displays:

    • The selected contingency as a clickable button that zooms to it in the current diagram tab (without switching tabs)
    • N-1 overloaded lines with their loading percentages, clickable to switch to N-1 tab and zoom
    • This keeps critical information visible while scrolling through actions
  • Loading Ratio Display:

    • Backend now returns lines_overloaded_rho (per-element I/limit ratios) alongside lines_overloaded names
    • Frontend displays these as "(XX.X%)" next to each overloaded line in the Overloads panel, sticky header, and action cards
    • Rho values are aligned with element names for consistent rendering
  • New handleZoomOnActiveTab Handler: Allows zooming to an asset in the current diagram without switching tabs, used by the sticky contingency button and overload links

  • Sidebar Layout Restructuring:

    • Sidebar now uses flexbox with overflow: hidden to prevent nested scrollbars
    • Sticky summary strip (flex-shrink: 0) stays fixed at top
    • Scrollable content area below contains the full Select Contingency card, Overloads panel, and ActionFeed
    • Unified scrollbar for the entire feed area
  • Removed Redundant "Loading Before": Deleted the "Loading before" line from individual action cards since this information is now prominently displayed in the sticky N-1 overloads section

  • Updated OverloadPanel Component: Added nOverloadsRho and n1OverloadsRho props to render loading percentages; improved documentation of the asset click behavior

  • Test Updates: Updated tests to verify the new sticky header layout and rho percentage rendering

Implementation Details

  • The sticky header conditionally renders only when there's a selected contingency or N-1 overloads, minimizing visual clutter
  • Overload links in the sticky header use wrappedAssetClick for N-1 (switches tabs) vs handleZoomOnActiveTab for contingency (stays on current tab)
  • Backend _get_overloaded_lines() method now supports with_rho=True parameter to return both names and rho values as a tuple
  • Rho formatting is consistent across all UI surfaces: (rho * 100).toFixed(1) + '%'
  • Selected overloads in N-1 are visually emphasized with darker text and underlines; deselected ones are grayed out

https://claude.ai/code/session_01JpNKJ8NCyrv4erGkKBp2Nz

claude added 4 commits April 14, 2026 09:48
Rationalize the left feed so the operator always sees the selected
contingency and the N / N-1 overloads while scrolling through the
action list, and lets them zoom those items on whichever diagram is
currently displayed.

- Sidebar layout: contingency selector and OverloadPanel now sit in a
  non-scrolling sticky block; only the ActionFeed body scrolls
  (App.tsx + standalone_interface.html).
- Selected contingency: rendered as a clickable link beneath the
  datalist input; clicking re-centers the currently-active diagram
  (N / N-1 / Action) on the contingency without switching tabs.
- Overloads panel: the N and N-1 line names now show their loading
  ratio as "(XX.X%)" next to the name, pulled from a new
  lines_overloaded_rho field on the diagram response.  Clicking a
  line zooms the active tab instead of force-switching to N or N-1.
- Backend: _get_overloaded_lines optionally returns parallel rho
  values; get_network_diagram / get_n1_diagram expose them via the
  new lines_overloaded_rho field.
- Action cards: the per-card "Loading before" line is removed —
  the information is now permanently visible in the sticky Overloads
  N-1 section and was duplicated on every card.
- Tests updated (OverloadPanel, ActionCard, App.contingency sidebar
  layout).

https://claude.ai/code/session_01JpNKJ8NCyrv4erGkKBp2Nz
Replace the heavy sticky block (full Select Contingency card + full
Overloads panel) with a thin sticky strip that only carries the
clickable fields of interest — saving vertical space in the feed.

- Sticky strip: two compact rows
  - 🎯 Contingency: clickable selected branch → zooms on the
    currently-active diagram tab (no tab switch).
  - ⚠️ N-1: clickable N-1 overload names with "(XX.X%)" loading,
    each opening the N-1 tab and zooming on the element.
- Full Select Contingency card (search input) and full Overloads
  panel (with warnings, N/N-1 breakdown, rho %) move back into the
  scroll area so they scroll out of view as the operator browses
  actions, preserving the screen for the action cards.
- OverloadPanel click reverted to the per-tab switch behavior:
  N overloads → N tab, N-1 overloads → N-1 tab.  This matches the
  old "Loading Before" link on action cards which always landed on
  the N-1 state, so the info flow from the sticky strip and the
  full panel is consistent.
- standalone_interface.html mirrored.
- Tests updated for the reverted OverloadPanel click API.

https://claude.ai/code/session_01JpNKJ8NCyrv4erGkKBp2Nz
Clicking the line name in the action card's "Max loading: X% on Y"
row was silently falling back to the previously-zoomed element (the
pre-action N-1 overload) whenever Y was a line that is not itself in
the disconnectable `branches` list — typical case: a remedial action
re-distributes flows and the new worst line is not a contingency
target.

Root cause: the auto-zoom effect guards `setInspectQuery(...)` with
`knownItemsSet.has(targetId)` and that set was only built from
`branches ∪ voltageLevels`.  When the click landed on a line outside
this set, the guard rejected it, `lastZoomState.query` stayed on the
previous target, and the diagram stayed centered on whatever was
zoomed last — giving the user the impression that the Max-loading
link "always points to the overload before".

Fix: widen the guard to also include every equipment ID present in
any loaded NAD metadata index (nodes + edges for N / N-1 / Action).
This is pulled out as a standalone `computeKnownItemsSet` helper so
it can be unit-tested.

Tests:
- `useDiagrams.test.ts` — 6 unit tests for `computeKnownItemsSet`
  covering branches, voltage levels, metadata-only IDs, null indices,
  and the partial-text rejection still holding.
- `ActionCard.test.tsx` — regression: clicking the "Max loading on
  LINE_B" button when `linesOverloaded=['LINE_A']` and
  `max_rho_line='LINE_B'` must call `onAssetClick` with `LINE_B`.

https://claude.ai/code/session_01JpNKJ8NCyrv4erGkKBp2Nz
The selected action card used to show a rectangular "VIEWING" pill
in the top-right of its header, stealing a chunk of the one row of
horizontal space shared with the action ID and severity badge — on a
narrow sidebar with long equipment IDs this routinely forced the
title to wrap.

Re-render it as a vertical ribbon flush against the left border:

- Outer card becomes `display: flex; align-items: stretch; padding: 0`
  with an inner content column that carries the card's original
  `padding: 10px`.
- When `isViewing`, a sibling ribbon is inserted as the first child,
  sitting between the 5px-colored left border and the content
  column.  It uses `writing-mode: vertical-rl` + `transform:
  rotate(180deg)` — the cross-browser combination for bottom-to-top
  "book-spine" orientation that works consistently on Chromium,
  Firefox and Safari (unlike the newer `sideways-lr`).
- The inline top-right VIEWING pill is removed; the severity badge
  ("Solves overload" / "Still overloaded" / ...) keeps its place.

Standalone interface mirrored.

Tests (ActionCard.test.tsx):
- asserts the ribbon renders with `writing-mode: vertical-rl` when
  `isViewing`, contains "VIEWING", and is queryable via a stable
  test id.
- asserts no <span> in the card contains the old inline pill text.
- asserts the ribbon is absent when `isViewing=false`.

https://claude.ai/code/session_01JpNKJ8NCyrv4erGkKBp2Nz
@marota
marota merged commit 669ddd4 into main Apr 14, 2026
2 checks passed
marota pushed a commit that referenced this pull request Apr 14, 2026
Session reload no longer loses data introduced by PRs #73/#78/#83/#88:

- handleRestoreSession now restores lines_overloaded_after,
  load_shedding_details, curtailment_details and pst_details on each
  ActionDetail. Previously these were dropped on reload, so the PST /
  load-shedding / curtailment editor cards rendered empty and the
  Remedial Action tab lost its post-action overload halos until the
  user re-ran analysis.
- buildSessionResult persists the sticky-header rho arrays
  (n_overloads_rho / n1_overloads_rho) alongside the overload name
  lists, guarded on matching length so misaligned legacy data is
  omitted instead of saved.
- committedNetworkPathRef is now updated on session restore so the
  "Change Network?" confirmation dialog no longer misfires (or
  silently drops the study) after a reload.

Interaction logging now captures every user gesture the replay
contract needs to faithfully reproduce a session:

- config_loaded and settings_applied include the full settings
  payload (all paths, every recommender threshold including
  min_load_shedding and min_renewable_curtailment_actions, monitoring,
  pre-existing overload threshold, ignore_reconnections,
  pypowsybl_fast_mode).
- settings_tab_changed emits { from_tab, to_tab } and skips no-op
  clicks on the already-active tab.
- New event types action_mw_resimulated and pst_tap_resimulated are
  logged from ActionFeed.handleResimulate / handleResimulateTap with
  the raw user-entered target_mw / target_tap. useActions no longer
  logs manual_action_simulated from handleActionResimulated, which
  conflated the two flows and made replay impossible.

docs/interaction-logging.md is rewritten to reflect all of the above:
documented tab_detached / tab_reattached / tab_tied / tab_untied
visualisation events (previously in types.ts but undocumented),
corrected the details shape for view_mode_changed / asset_clicked /
inspect_query_changed / sld_overlay_* / session_* to match the actual
emitted payloads, documented the applySettings / loadStudy /
changeNetwork cases on contingency_confirmed, and added a new
"Session reload fidelity" section listing exactly which fields are
persisted / restored and which are intentionally ephemeral.

Tests: 695 frontend tests still pass; sessionUtils.test.ts gains
coverage for rho persistence guards and useActions.test.ts now
asserts that handleActionResimulated does not log from the hook.

https://claude.ai/code/session_013qJjLFQWMR91ZfPTRCLFiu
marota pushed a commit that referenced this pull request Apr 19, 2026
…tton release

Four user-reported issues from the last screenshot batch:

## 1. Combine-modal Simulate button stuck in running mode

`simulatingActionId` was only reset in the outer try's finally, AFTER
the async action-variant-diagram fetch completed.  On big grids
that's ~5-6 seconds of stale "spinner" UI even though the action
CARD (what matters to the sidebar feed) has already landed.

Fix: call setSimulatingActionId(null) + setIsSimulatingCombined(false)
the moment the simulate response arrives and the result has been
merged.  The subsequent variant-diagram fetch continues
asynchronously — buttons release immediately, tab flips happen
naturally when the diagram is ready, matching React's perceived
responsiveness.  Applied to both handleAddManualAction and
handleSimulateCombined.

## 2. Tab switching / action diagram loading felt sluggish

Two React performance optimisations were missing on the standalone:

### 2a. `/api/network-diagram` served as plain JSON → large JSON.parse

The backend exposes a `format=text` variant that returns
`{header}\n<svg…>` instead of embedding the multi-MB SVG inside a
JSON string.  Skips a ~500 ms main-thread JSON.parse on the
PyPSA-EUR France grid (see docs/perf-loading-parallel.md).  React
has used this since PR #71.

Ported to the standalone via a new `_fetchNetworkDiagramTextFormat()`
helper that fetches + splits on the first newline, with a graceful
fallback to the JSON endpoint if the text variant 404s (older
backends).

### 2b. Base NAD fetched AFTER branches + VLs + nominal-voltages

Previously the standalone did `Promise.all([branches, VLs, nomV])`
THEN called `fetchBaseDiagram`.  Since network-diagram is by far
the slowest XHR, stacking it after the metadata round-trips adds
~1-2 s to the critical path.  React parallelises all four since
PR #88 (docs/perf-loading-parallel.md).

Ported: both call sites (applySettingsImmediate + handleLoadConfig
equivalents) now issue a 4-way Promise.all that includes the
network-diagram.  The SVG is processed synchronously after the
Promise.all resolves, matching the React flow.

## 3. Pin rendering

Prior implementation was an SVG circle rendered in a viewBox-
scaled layer, so pins shrunk / grew and drifted as the user
zoomed.  Label was the rank number, not the loading percentage.
Anchor math was fine but the visual anchoring looked off because
the pin's centroid scaled with the map.

Rewrite:

  - Pins rendered as absolutely-positioned `<div>` elements in
    SCREEN pixel space.  A `projectSvgPoint(x, y)` function maps
    each pin's SVG-local coordinates through the current
    overviewVb + overlayRect using the same `xMidYMid meet`
    projection as the SVG itself, so pins stay exactly on the
    line midpoint as the user pans + zooms.
  - Teardrop glyph: inline <svg> with a <path> for the classic
    Google-Maps pin shape (`d="M18 0 … Z"`), tip at (0, 48) so
    `translate(-50%, -100%)` on the wrapper plants the tip on
    the asset.  Inner white circle for label contrast.
  - Label is the loading percentage (rounded), falling back to
    the rank when rho isn't set — matches the React ActionOverview
    component's pin label.
  - Size is 36x48 CSS-px regardless of zoom.  A ResizeObserver
    keeps overlayRect current when the window or sidebar resizes,
    so pin projection stays accurate through responsive layout
    changes.

Combined-pair curves (React's Bézier connecting two pins) are
still omitted — it's purely cosmetic and the per-pin events +
metadata round-trip already cover replay parity.

## 4. Deselecting an action jumped to N-1 tab instead of staying on Action

React switched this behaviour in PR #93 — clicking the selected
action card again (or the × chip) now keeps the user on the
Remedial Action tab, which falls through to the
ActionOverviewDiagram pin view.  The standalone was still doing
`setActiveTab('n-1')` in the deselect branch of handleActionSelect.

Fix: drop that line.  The tab stays on 'action', selectedActionId
becomes null, and the conditional render below picks the Overview
component because it's the only branch where
`result?.actions && !selectedActionId`.

---

Parity-script impact:

  - Layer 1 still 51/51, exits 0.
  - Layer 2 unchanged (26/30 React, 30/30 standalone).
  - Layer 3a 22/22.
  - Vitest 972/972.

https://claude.ai/code/session_01LGL7gvedQLrUGGzqVm4wAG
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