Co-Study4Grid / CLAUDE.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
45.5 kB
# CLAUDE.md - Co-Study4Grid
## Project Overview
Co-Study4Grid is a full-stack web application for **power grid contingency analysis and N-1 planning**. It provides an interface to the `expert_op4grid_recommender` library, allowing operators to simulate element disconnections, visualize network overflow graphs, and receive prioritized remedial action recommendations.
## Architecture
**Monorepo** with two main components plus a standalone HTML mirror:
```
Co-Study4Grid/
├── CLAUDE.md # This file — project overview + standalone parity audit
├── README.md # User-facing project description + quick start
├── CHANGELOG.md # Per-release changelog (current: 0.9.0)
├── CONTRIBUTING.md # Contributor setup, code-quality gate
├── pyproject.toml # Python project metadata + ruff config (E9/F ruleset)
├── pytest.ini # Pytest config (testpaths = expert_backend/tests)
├── expert_backend/ # Python FastAPI backend
│ ├── CLAUDE.md # Backend-scoped guide (singletons, mixins, lifecycle)
│ ├── main.py # FastAPI app: endpoints, CORS, gzip helpers, NDJSON streaming
│ ├── test_backend.py # Ad-hoc integration script (not part of pytest)
│ ├── recommenders/ # Pluggable recommendation-model registry: registry.py,
│ │ │ # random_basic / random_overflow canonical examples,
│ │ │ # overflow_path_filter + network_existence sampling
│ │ │ # filters, synthetic_actions builders. Models are
│ │ │ # registered at package import; the service integration
│ │ │ # is EXPLICIT composition (RecommenderService inherits
│ │ │ # ModelSelectionMixin; AnalysisMixin.run_analysis_step2
│ │ │ # consumes the registry — no import-time patching).
│ │ │ # Full reference: docs/backend/recommender_models.md
│ ├── services/
│ │ ├── network_service.py # pypowsybl Network singleton + metadata queries
│ │ ├── recommender_service.py # Analysis orchestrator (composes the 4 mixins below)
│ │ ├── diagram_mixin.py # NAD/SLD orchestrator — delegates to services/diagram/
│ │ ├── analysis_mixin.py # Two-step analysis orchestrator (model-aware step-2
│ │ │ # dispatch through the recommenders registry) —
│ │ │ # delegates to services/analysis/
│ │ ├── simulation_mixin.py # Manual-action + superposition orchestrator
│ │ ├── model_selection_mixin.py # Active recommender model + overflow-graph toggle
│ │ ├── simulation_helpers.py # Stateless helpers extracted from simulation_mixin (PR #104)
│ │ ├── overflow_overlay.py # Pin / filter overlay injector for the interactive
│ │ │ # HTML overflow viewer (PR #116, 0.7.0)
│ │ ├── sanitize.py # NumPy → native-Python recursive coercion
│ │ ├── analysis/ # PR #104 decomposition — action_enrichment,
│ │ │ # mw_start_scoring, analysis_runner, pdf_watcher,
│ │ │ # overflow_geo_transform (PR #116 — geo-layout SVG
│ │ │ # transform for /api/regenerate-overflow-graph)
│ │ └── diagram/ # PR #104 decomposition — layout_cache, nad_params,
│ │ # nad_render, sld_render, overloads, flows, deltas,
│ │ # obs_prewarm (post-contingency obs cache prewarm
│ │ # that lets run_analysis_step1 skip the LF),
│ │ # action_patch (extracted action-variant patch
│ │ # pipeline — keeps diagram_mixin under the LoC
│ │ # ceiling)
│ └── tests/ # pytest suite — see tests/CLAUDE.md for the mock layer
├── frontend/ # React 19 + TypeScript 5.9 + Vite 7 frontend
│ ├── CLAUDE.md # Frontend-scoped guide (App.tsx hub, hooks, SVG levers)
│ ├── package.json, vite.config.ts, vite.config.standalone.ts,
│ │ # eslint.config.js, tsconfig*.json
│ └── src/
│ ├── App.tsx # State orchestration hub (~1400 lines)
│ ├── api.ts # Axios HTTP client (base URL: 127.0.0.1:8000)
│ ├── types.ts # All TypeScript interfaces (one file)
│ ├── styles/ # Design-token palette (PR #120, 0.7.0):
│ │ # tokens.css (CSS custom properties) +
│ │ # tokens.ts (typed colors / space / text /
│ │ # radius / pinColors* constants). Single
│ │ # source of truth for the entire UI; the
│ │ # code-quality gate enforces zero hex
│ │ # literals outside these two files.
│ ├── hooks/ # useSettings / useActions / useAnalysis / useDiagrams /
│ │ # useSession / useDetachedTabs / useTiedTabsSync /
│ │ # usePanZoom / useSldOverlay / useContingencyFetch (svgPatch fast-
│ │ # path + full fallback) / useDiagramHighlights (per-tab
│ │ # highlight pipeline + Flow/Impacts view-mode state) /
│ │ # useOverflowIframe (PR #116 — iframe lifecycle, layer
│ │ # toggles, postMessage bridge, pin overlay payload) /
│ │ # useSldTopologyEdit (interactive SLD switch-edit →
│ │ # manual action, 0.8.0) / useTheme (light/dark theme
│ │ # toggle + persistence, 0.8.0)
│ ├── components/ # Header, ActionFeed, ActionCard, ActionCardPopover,
│ │ # ActionSearchDropdown (editable Δ MW column
│ │ # for redispatch in score table),
│ │ # ActionTypeFilterChips,
│ │ # ActionFilterRings (shared severity + action-type +
│ │ # Max-loading ring strip, 0.8.0), ActionTypeIcon /
│ │ # SeverityIcon (action-type + severity pictograms),
│ │ # AdditionalLinesPicker,
│ │ # ActionOverviewDiagram, AppSidebar (collapsible
│ │ # shell — readability-feed PR), SidebarSummary
│ │ # (sticky strip — hosts Clear button + overload
│ │ # info bubble that absorbed the legacy
│ │ # OverloadPanel affordances),
│ │ # NotificationHost (typed toast store —
│ │ # severity/dismiss/aria-live, PR D5),
│ │ # VisualizationPanel, OverloadPanel
│ │ # (kept on disk for unit-test backwards-compat;
│ │ # no longer rendered from App.tsx),
│ │ # CombinedActionsModal, ComputedPairsTable,
│ │ # ExplorePairsTab, SldOverlay, SldEditPanel
│ │ # (interactive maneuver list, 0.8.0),
│ │ # SldInjectionPopover (SLD load/gen active-
│ │ # power editor bubble), DetachableTabHost,
│ │ # MemoizedSvgContainer, ErrorBoundary, NoticesPanel
│ │ # (PR #122 tier system), DiagramLegend (PR #122),
│ │ # InspectSearchField + DetachedPlaceholder (PR #116
│ │ # — extracted from VisualizationPanel)
│ │ # + modals/ (SettingsModal, ReloadSessionModal,
│ │ # ConfirmationDialog)
│ ├── game/ # Timed, scored Game Mode (0.8.0; active only
│ │ # with ?game=1) — GameShell / useGameSession /
│ │ # gameBridge / GameConfigScreen / GameHud /
│ │ # GameResults / scoring / gameLog / presets /
│ │ # types. See docs/features/game-mode-codabench.md
│ └── utils/ # svgUtils (barrel re-exporting utils/svg/*),
│ # svgPatch (DOM-recycling patch applier),
│ # overloadHighlights, sessionUtils, interactionLogger,
│ # popoverPlacement, mergeAnalysisResult, actionTypes
│ # (classifyActionType + DEFAULT_ACTION_OVERVIEW_FILTERS),
│ # inspectables (filterInspectables — match an element
│ # by its displayed name, not just its raw id),
│ # ndjsonStream (single NDJSON reader, D5),
│ # notifications (typed toast store, D5),
│ # fileRegistry (structure regression guard)
│ └── svg/ # PR #104 decomposition — idMap, metadataIndex,
│ # svgBoost, fitRect, deltaVisuals, actionPinData,
│ # actionPinRender, highlights, vlInteractions (VL-disk
│ # hover name / click-to-inspect / dbl-click SLD),
│ # overflowPinPayload
│ # + overflowOverlayRender + pinGlyph (PR #116 —
│ # iframe-overlay pin pipeline)
├── standalone_interface_legacy.html # DECOMMISSIONED 2026-04-20 — hand-maintained
│ # single-file mirror frozen at its last version and
│ # tracked here for reference only. Replaced by the
│ # auto-generated `frontend/dist-standalone/standalone.html`
│ # (`npm run build:standalone`). New UI changes land ONLY
│ # in `frontend/src/` — do NOT edit this file further.
├── docs/ # Design docs — organized into features/, performance/
│ # (+ history/), architecture/, proposals/, data/.
│ # See `docs/README.md` for the index.
├── data/ # Sample grids: bare_env_small_grid_test, pypsa_eur_fr400,
│ # pypsa_eur_fr225_400 (France 225/400 kV — its large
│ # network ships compressed as network.xiidm.zip,
│ # auto-decompressed by network_service on load)
├── benchmarks/ # Perf scripts (bench_load_study, _bench_common)
├── Overflow_Graph/ # Generated PDFs (created at runtime)
├── overrides.txt # Pinned versions for transitive Python deps
├── requirements_py310.txt # Python 3.10-pinned requirements superset
├── scripts/ # Integration / parity / build helpers —
│ # `check_standalone_parity.py`,
│ # `check_session_fidelity.py`,
│ # `check_gesture_sequence.py`,
│ # `check_invariants.py`, `check_code_quality.py`,
│ # `check_openapi_contract.py` (D2 — diffs the live
│ # app.openapi() against expert_backend/openapi.snapshot.json),
│ # `code_quality_report.py`, `profile_diagram_perf.py`,
│ # `test_code_quality_report.py`,
│ # `test_estimation_vs_simulation_small_grid.py`,
│ # `pypsa_eur/` (full PyPSA-EUR → XIIDM pipeline
│ # with its own pytest coverage; `build_pipeline.py`
│ # writes a per-bundle provenance.json, D8), and
│ # `game_mode/` (`e2e_game_session.py` — real-backend
│ # Game Mode replay; `scoring_program/score.py` —
│ # in-repo Codabench scorer twin of scoring.ts,
│ # pinned to `scoring_golden.json` by test_score.py
│ # + scoring.test.ts for cross-language parity, D8)
├── Dockerfile # Single-container HuggingFace Docker Space image —
│ # same-origin SPA + FastAPI on :7860, game mode on
├── .dockerignore
├── deploy/ # HuggingFace Space README + step-by-step SETUP.md
├── config.default.json # Bundled first-run settings (fr225_400 grid +
│ # per-action-type recommender minima)
├── .editorconfig # Cross-editor indent / EOL defaults
├── .env.example # Template for backend env vars (CORS, …)
├── .gitattributes # Git LFS tracking for *.zip / *.png / *.jpg(eg)
│ # (HuggingFace Space git endpoint requires LFS)
└── .gitignore # Excludes __pycache__/, *.pyc, *.pyo, node_modules/
```
### Per-subtree docs
| File | Scope |
|------|-------|
| `CLAUDE.md` (this file) | Project overview, API table, conventions, parity-audit pointer |
| `frontend/PARITY_AUDIT.md` | Full standalone-parity audit: feature inventory, mirror-status table, Layer 1–4 conformity findings, gap-priority list, deltas. Split out of this file 2026-04-20. |
| `expert_backend/CLAUDE.md` | Backend internals: singletons, mixin composition, state lifecycle, NDJSON streaming, gzip helpers, layout cache invariants, NAD prefetch |
| `expert_backend/tests/CLAUDE.md` | Test conventions, the `conftest.py` mock layer for `pypowsybl` / `expert_op4grid_recommender`, frontend Vitest patterns |
| `frontend/CLAUDE.md` | Frontend internals: hook split, data flow, state reset, SVG performance levers, detached/tied tabs, interaction logger contract |
| `docs/README.md` | Index of design/feature/perf/architecture/proposal docs. Start here for any `docs/**` lookup. |
| `docs/features/save-results.md` | Save / reload session contract (JSON schema, reload flow, regression-guard matrix) |
| `docs/features/adding-action-type.md` | Cross-cutting checklist for adding/upgrading a remedial-action type (lib → backend → frontend → save/log/reload triad → regression specs) |
| `docs/features/interaction-logging.md` | Replay-ready event log contract |
| `docs/features/sld-topology-edit.md` | Interactive SLD topology edit → manual action card |
| `docs/features/sld-diagram-feeder-labels.md` | SLD feeders relabelled by far-end VL name (+ parallel index), overload-halo friendly-name↔IIDM-id bridge, and the charging-current annotation explaining the "after" loading of a line opened at one end |
| `docs/features/vl-disk-interactions.md` | Interactive VL disks on the NAD (hover name / click → Inspect / double-click → SLD) + delegation performance contract |
| `docs/features/game-mode-codabench.md` | Timed, scored Game Mode (`?game=1`) + Codabench benchmark bundle |
| `deploy/huggingface/` | HuggingFace Docker Space deployment (Space README + `SETUP.md`) |
## Tech Stack
### Backend
- **Python** with **FastAPI** + **Uvicorn**
- **pypowsybl** - Power system network loading, load flow, and diagram generation
- **expert_op4grid_recommender** - Domain-specific grid optimization recommendations
- **grid2op** / **pandapower** / **lightsim2grid** - Grid simulation backends
### Frontend
- **React 19** with **TypeScript 5.9**
- **Vite 7** - Build tool and dev server
- **axios** - HTTP client
- **react-select** - Searchable dropdown for branch selection
- **vite-plugin-singlefile** - Auto-generated single-file standalone bundle
- **Vitest** + **React Testing Library** - Unit / integration tests
## Development Workflow
### Running the Backend
```bash
# From the project root:
python -m expert_backend.main
# Or:
uvicorn expert_backend.main:app --host 0.0.0.0 --port 8000
```
The backend serves on `http://localhost:8000`. It expects `pypowsybl` and `expert_op4grid_recommender` to be available in the Python environment.
### Running the Frontend
```bash
cd frontend
npm install
npm run dev # Start Vite dev server with HMR
```
The frontend dev server proxies API calls to `http://localhost:8000` (hardcoded in `frontend/src/api.ts`).
### Build & Lint
```bash
cd frontend
npm run build # TypeScript compilation (tsc -b) + Vite production build
npm run lint # ESLint
npm run preview # Preview production build
```
### Running Tests
Backend unit tests use `pytest` and run against the in-repo mock layer
(no live pypowsybl required):
```bash
pytest # Full backend suite
pytest expert_backend/tests/test_foo.py # Single file
```
Ad-hoc integration scripts live in `scripts/` (and `scripts/pypsa_eur/`
for the PyPSA-EUR → XIIDM pipeline). The pipeline scripts carry their
own pytest coverage (`scripts/pypsa_eur/test_*.py`) alongside the
backend suite; the rest require a running backend with real data:
```bash
pytest scripts/pypsa_eur # Pipeline unit tests
python scripts/pypsa_eur/test_pipeline.py # End-to-end smoke test
python scripts/pypsa_eur/test_n1_calibration.py # N-1 flow calibration check
python scripts/pypsa_eur/test_grid_layout.py # Layout loading sanity check
python scripts/profile_diagram_perf.py # NAD rendering profiler
```
Frontend unit tests use Vitest:
```bash
cd frontend
npm run test # Run Vitest test suite
```
### Code-Quality Checks (continuous reporting)
```bash
# Generate a full JSON + Markdown report (backend + frontend metrics)
python scripts/code_quality_report.py --output reports/code-quality.json \
--markdown reports/code-quality.md
# Gate a pull request: non-zero exit on threshold violation
python scripts/check_code_quality.py
```
Both scripts run in CI (`.github/workflows/code-quality.yml`). The gate
guards the reductions documented in
[`docs/architecture/code-quality-analysis.md`](docs/architecture/code-quality-analysis.md)
(no new `print()` / bare except, module-size ceilings, no `any` /
`@ts-ignore` in frontend source).
## API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/user-config` | Read persisted user configuration (paths, recommender params) |
| POST | `/api/user-config` | Persist user configuration |
| GET | `/api/config-file-path` | Get the current user-config file path |
| POST | `/api/config-file-path` | Set a custom user-config file path |
| POST | `/api/config` | Set network path, action file path, and all recommender parameters (incl. `model` and `compute_overflow_graph`) |
| GET | `/api/models` | List registered recommendation models with their `params_spec()` and capability flags |
| POST | `/api/recommender-model` | Lightweight swap of the active recommender model (no network reload) — fired by the model dropdowns in Settings and above Analyze & Suggest |
| GET | `/api/branches` | List disconnectable elements (lines + 2-winding transformers) |
| GET | `/api/voltage-levels` | List voltage levels in the network |
| GET | `/api/nominal-voltages` | Map voltage level IDs to nominal voltages (kV) |
| GET | `/api/element-voltage-levels` | Resolve equipment ID to its voltage level IDs |
| GET | `/api/voltage-level-substations` | Map voltage level IDs to their parent substation IDs (used by the SLD overlay and overflow pin pipeline) |
| POST | `/api/run-analysis` | Run full N-1 contingency analysis (streaming NDJSON, legacy) |
| POST | `/api/run-analysis-step1` | Two-step analysis Part 1: detect overloads |
| POST | `/api/run-analysis-step2` | Two-step analysis Part 2: resolve with actions (streaming NDJSON) |
| GET | `/api/network-diagram` | Get N-state network SVG diagram (NAD) |
| POST | `/api/contingency-diagram` | Get post-contingency N-1 diagram with flow deltas |
| POST | `/api/contingency-diagram-patch` | SVG-less per-branch delta for DOM-recycling fast path (PR #108) |
| POST | `/api/action-variant-diagram` | Get network state after applying a remedial action |
| POST | `/api/action-variant-diagram-patch` | Per-branch delta + VL-subtree splice for action DOM recycling |
| POST | `/api/focused-diagram` | Generate NAD sub-diagram focused on a specific element |
| POST | `/api/action-variant-focused-diagram` | Focused NAD for specific VL in post-action state |
| POST | `/api/n-sld` | Single Line Diagram for voltage level in N state. Response includes `switch_states` (per-switch open/closed map), `injections` (per-load/generator active-power baseline) used by the interactive SLD-edit feature, **and** `feeder_labels` (per-branch `{name, other_vl, label}``label` = far-end VL name + parallel index, used to relabel feeders and bridge friendly-named overloads to the SLD cell). |
| POST | `/api/contingency-sld` | Single Line Diagram in N-1 state (with flow deltas + `switch_states` + `injections` + `feeder_labels`). |
| POST | `/api/action-variant-sld` | SLD in post-action state (with flow deltas, `changed_switches`, `switch_states`, `injections`, `feeder_labels`). |
| POST | `/api/sld-topology-preview` | Target-topology preview SLD for the interactive SLD-edit feature: applies staged switch overrides on a throwaway variant and re-renders with topological colouring (no load flow; `stale_flows: true`). |
| GET | `/api/actions` | Return all available action IDs and descriptions |
| POST | `/api/regenerate-overflow-graph` | Regenerate (or serve from cache) the overflow graph in hierarchical / geo layout — drives the toggle on the Overflow Analysis tab |
| POST | `/api/simulate-manual-action` | Simulate a specific action against a contingency. Accepts an optional `voltage_level_id` field used to auto-name switch-only user actions (interactive SLD-edit feature). |
| POST | `/api/simulate-and-variant-diagram` | NDJSON stream: `{type:"metrics"}` then `{type:"diagram"}` so sidebar updates ahead of the SVG |
| POST | `/api/compute-superposition` | Compute combined effect of two actions (superposition theorem) |
| POST | `/api/save-session` | Save session folder with JSON snapshot + PDF copy |
| GET | `/api/list-sessions` | List available session folders in a directory |
| POST | `/api/load-session` | Load session JSON and restore PDFs |
| POST | `/api/restore-analysis-context` | Restore analysis context from saved session |
| GET | `/api/pick-path` | Open native OS file/directory picker (tkinter subprocess) |
| GET | `/results/pdf/{filename}` | Serve generated overflow-graph files from `Overflow_Graph/` — HTML (interactive viewer, current default via `config.VISUALIZATION_FORMAT="html"`) or PDF (legacy sessions). URL path kept for backward compatibility. |
## Key Patterns & Conventions
### Backend
- **Singleton services**: `network_service` and `recommender_service` are module-level singleton instances
- **Streaming responses**: Analysis uses `StreamingResponse` with NDJSON (`application/x-ndjson`), yielding `{"type": "pdf", ...}` then `{"type": "result", ...}` events
- **AC/DC fallback**: Analysis first tries AC load flow; falls back to DC if AC does not converge
- **Threaded analysis**: `run_analysis` runs the computation in a background thread and polls for PDF generation
- **JSON sanitization**: NumPy types are recursively converted to native Python types via `sanitize_for_json()`
- **Unified error contract (D2, 2026-07)**: every error is `{"detail", "code"}` produced in one place (`services/api_errors.py`, `install_error_handlers(app)`). Raise a plain `HTTPException` (code derived from status: 400/404/409/422/500) or `AppHTTPException(status, detail, code)` when the frontend branches on the failure (e.g. `ACTION_RESULT_UNAVAILABLE`, `STUDY_BUSY`). Uncaught exceptions → generic logged 500 (never `str(e)` — it leaks paths). The frontend reads it via `frontend/src/utils/apiError.ts`.
- **Machine-checked API contract (D2, 2026-07)**: `app.openapi()` is snapshotted to `expert_backend/openapi.snapshot.json` and diffed in CI (`scripts/check_openapi_contract.py`, `test_openapi_contract.py`). Regenerate on any deliberate endpoint / model / status change: `python scripts/check_openapi_contract.py --write`.
- **Concurrency ownership (D3, 2026-07)**: a service-level re-entrant lock (`services/service_lock.py`, `@with_network_lock[_stream]`) serializes the variant-switching entry points on the shared `Network`; overlapping study mutations (config load / analysis) get **HTTP 409**. See [`docs/architecture/shared-network-concurrency.md`](docs/architecture/shared-network-concurrency.md).
- **Mixin → helper-package decomposition (PR #104 / #106)**: `DiagramMixin`, `AnalysisMixin` and `SimulationMixin` are thin orchestrators. Pure numerics live in `services/diagram/`, `services/analysis/` and `services/simulation_helpers.py` respectively — dependency-injected so existing `@patch` tests keep working.
- **SVG DOM recycling (PR #108)**: patch endpoints (`/api/contingency-diagram-patch`, `/api/action-variant-diagram-patch`) return per-branch deltas + optional VL-subtree splices so the frontend can clone the already-mounted N-state SVG instead of re-downloading the full NAD (~80 % faster tab switches on large grids).
- **Interactive overflow viewer (PR #116, 0.7.0)**: `services/overflow_overlay.py` injects a Co-Study4Grid pin / filter overlay (`<style>` + `<script>` block) into the upstream `expert_op4grid_recommender` HTML viewer before serving it from `/results/pdf/{filename}`. `services/analysis/overflow_geo_transform.py` is a pure lxml transform that rewrites the hierarchical-layout SVG to geographic coordinates for the `/api/regenerate-overflow-graph` toggle; the geo cache is per-study and cleared on `reset()`.
- **Shared diagram helpers**: `RecommenderService` uses `_load_network()`, `_load_layout()`, `_default_nad_parameters()`, and `_generate_diagram()` to deduplicate diagram generation logic across endpoints
- **Focused diagrams**: The `/api/focused-diagram` endpoint resolves an element to its voltage levels and generates a sub-diagram with configurable depth, useful for inspecting specific parts of large grids
- **Ruff-gated**: `pyproject.toml` configures a narrow `E9` + `F` ruleset (real bugs only); stylistic rules deliberately off
### Frontend
- **Strict TypeScript**: `strict: true`, `noUnusedLocals`, `noUnusedParameters`, `noFallthroughCasesInSwitch`
- **Functional components** with React hooks; no external state management library
- **Inline styles**: Components use inline `style` objects rather than CSS modules or utility classes
- **Design tokens (PR #120, 0.7.0)**: every colour / spacing / typography / radius value lives in `frontend/src/styles/tokens.{css,ts}`. Inline `style` objects import the typed `colors` / `space` / `text` / `radius` constants from `tokens.ts`; stylesheet rules use `var(--…)` from `tokens.css`. Raw SVG attribute setters (`element.setAttribute('fill', …)`) import the hex-valued `pinColors` / `pinChrome` constants because browsers don't reliably resolve `var(--…)` inside SVG presentation attributes. **The code-quality gate enforces zero hex literals outside the two token files.**
- **Light / dark theme (0.8.0)**: theming is a token swap, not per-component overrides — the `useTheme` hook flips a theme attribute that re-points the `tokens.css` custom properties, with a tiny pre-mount script to avoid the first-paint flash. A legibility pass covers the pypowsybl NAD / SLD chrome and the injected overflow-viewer overlay. See [`docs/features/dark-mode.md`](docs/features/dark-mode.md).
- **Component architecture (Phase 2 hook extraction, PR #109)**:
- `App.tsx` (~1400 lines) is the **state orchestration hub** — it wires all hooks together and handles cross-hook logic (e.g., `handleApplySettings`). It should NOT contain large JSX blocks.
- **Presentational components** live in `components/` and `components/modals/`. They receive data and callbacks via typed props; all business logic stays in `App.tsx` or in hooks.
- `hooks/useContingencyFetch.ts` owns the N-1 diagram fetch pipeline (svgPatch fast-path + `/api/contingency-diagram` fallback + contingency-change confirm routing).
- `hooks/useDiagramHighlights.ts` owns the per-tab SVG highlight pipeline (overload halos, contingency highlight, action targets, delta visuals) + per-tab Flow/Impacts view-mode state.
- `hooks/useOverflowIframe.ts` (PR #116, 0.7.0) owns the interactive overflow viewer — iframe lifecycle, layer-toggle state, hierarchical ↔ geo layout switch, postMessage bridge to the host, and the action-pin overlay payload computation.
- `hooks/useSldTopologyEdit.ts` (0.8.0) owns the interactive SLD edit flow — `editMode` (implicit while the SLD is open — no toggle button; read-only on close), staged `pendingStates` (switch toggles) + `pendingInjections` (load / generator active-power retunes), `toggle` / `removeSwitch(es)` / `setInjection` / `removeInjection` / `focusedSwitchId` — that turns clicked breakers AND injection retunes into one manual action card (`SldInjectionPopover` is the active-power editor bubble). See [`docs/features/sld-topology-edit.md`](docs/features/sld-topology-edit.md).
- `useSettings.ts` exposes `SettingsState` (all settings values + setters), which is passed wholesale to `SettingsModal` to avoid 30+ prop-drilling.
- **SVG DOM recycling (PR #108)**: `utils/svgPatch.ts` clones the already-mounted N-state `SVGSVGElement` and patches only per-branch deltas on N-1 / action tab switches, saving a 12–28 MB SVG re-download and re-parse.
- **Props-based data flow**: State lifted to `App.tsx`, passed down via props
- **ESLint**: Flat config (v9+) with `typescript-eslint`, `react-hooks`, and `react-refresh` plugins
- **Unit tests** use Vitest + React Testing Library. Isolated component tests (no backend mocking needed) live alongside their components as `*.test.tsx` files.
### Data Flow
1. User sets network path + action file path -> `POST /api/config` loads the network
2. Frontend fetches disconnectable branches -> `GET /api/branches`
3. User selects a contingency branch -> N-1 diagram fetched with overload highlighting
4. User runs analysis (two-step flow):
- Step 1: `POST /api/run-analysis-step1` detects overloads in N-1 state
- User selects which overloads to resolve
- Step 2: `POST /api/run-analysis-step2` streams PDF event + action results
5. Frontend displays overflow PDF and action cards in ActionFeed panel
6. User can star/reject actions, manually simulate others, compute combined pairs
7. Action selection triggers `POST /api/action-variant-diagram` -> post-action diagram
8. Session save captures full snapshot to `session.json` + overflow PDF copy
### Session Save/Load
- **Save**: `buildSessionResult()` in `sessionUtils.ts` serializes all state (config, contingency, actions with status tags, combined pairs) -> `POST /api/save-session` writes to disk
- **Load**: `POST /api/load-session` reads `session.json` -> frontend restores all state without re-simulating actions
- **Output folder**: `<output_folder>/costudy4grid_session_<contingency>_<timestamp>/` contains `session.json` + overflow PDF
- **Interaction logging**: Every user interaction is logged as a timestamped, replay-ready event via `interactionLogger`. Saved as `interaction_log.json` alongside `session.json`.
- See `docs/features/save-results.md` for session save/load, `docs/features/interaction-logging.md` for the replay contract, and `docs/features/action-overview-diagram.md` for the Remedial Action overview (pin overlay on N-1 network)
### SVG Visualization
Both the React frontend and the auto-generated
`frontend/dist-standalone/standalone.html` render pypowsybl
NAD/SLD payloads:
- **Dynamic text scaling** (`utils/svgUtils.ts:boostSvgForLargeGrid` /
standalone `boostSvgForLargeGrid`): font sizes for node labels, edge
info, and legends scale proportionally to diagram size via
`sqrt(diagramSize / referenceSize)`, so text is readable when zoomed
in and naturally invisible at full zoom-out. Engaged for grids
≥ 500 voltage levels.
- **Bus / transformer scaling**: circle radii for bus nodes and
transformer windings are boosted proportionally.
- **Edge-info scaling**: flow values and arrow glyphs are scaled via
transform groups so they remain proportional to the line on which
they sit.
- **ViewBox zoom**: auto-centers on selected contingency targets with
adjustable padding.
- **Pan/zoom**: the `usePanZoom` hook writes the SVG `viewBox`
directly (rAF-batched, cached CTM) — no pan/zoom library — in both
the React dev build and the auto-generated standalone (they share
the same source tree).
## Dependencies
### Backend (`pyproject.toml` + `overrides.txt`)
- `fastapi`, `uvicorn`, `python-multipart`
- `pypowsybl`, `expert_op4grid_recommender` (expected in venv)
- `pandas>=2.2.2`, `numpy>=2.0.0`, `grid2op>=1.12.2`, `pandapower>=2.14.0`
- `lightsim2grid>=0.12.0`, `matplotlib>=3.10.6`, `scipy>=1.16.0`
- `lxml>=6.0.0`, `contourpy>=1.2.0`, `tqdm>=4.65.0`
### Frontend (`frontend/package.json`)
- See `dependencies` and `devDependencies` in `frontend/package.json`
## File Conventions
- Network data files: `.xiidm` format (loaded by pypowsybl)
- Action definitions: `.json` files with action IDs mapping to descriptions
- Generated outputs: PDF files in `Overflow_Graph/` directory
- Network layouts: `grid_layout.json` (node ID -> [x, y] coordinates)
## Notes for AI Assistants
- The backend API base URL defaults to `http://127.0.0.1:8000` in `frontend/src/api.ts` (`API_BASE_URL`), overridable at build time via `VITE_API_BASE_URL` — set it to `""` for **same-origin** hosting where the backend serves the SPA (the HuggingFace Docker Space), so requests become relative `/api/...`
- CORS defaults to the local Vite dev/preview origins on loopback (`localhost`/`127.0.0.1` on `:5173` / `:4173`); a wildcard is explicit opt-in and any other set is configurable via the `CORS_ALLOWED_ORIGINS` env var (see `.env.example`)
- **Frontend architecture (Phase 2 hook extraction, PR #109)**: `App.tsx` is the state orchestration hub; it must NOT contain large inline JSX blocks. Extracted presentational components live in `components/` and `components/modals/`; cross-cutting state pipelines live in `hooks/` (notably `useContingencyFetch` and `useDiagramHighlights`). When adding new UI sections, create a new component file (or hook for stateful pipelines) and wire it in `App.tsx`.
- **`useSettings` hook**: Exposes a `SettingsState` object with all settings fields + setters. This is passed wholesale to `SettingsModal` to avoid excessive prop drilling. Adding a new setting means: (1) add to `useSettings.ts`, (2) add to `SettingsModal.tsx`. No manual standalone mirror is required — the legacy hand-maintained file has been decommissioned and the auto-generated bundle inherits from the React source automatically.
- **Standalone bundle (auto-generated)**: `npm run build:standalone` in `frontend/` produces `frontend/dist-standalone/standalone.html` — a single-file HTML with React + CSS inlined via `vite-plugin-singlefile`. This is the canonical distribution artifact replacing the former `standalone_interface.html`. The legacy file remains on disk as `standalone_interface_legacy.html` (tracked as a frozen snapshot — do NOT edit).
- **Online deployment (HuggingFace Docker Space, 0.8.0)**: `Dockerfile` builds the SPA with `VITE_API_BASE_URL=""` + `VITE_GAME_MODE=1` and serves it **same-origin** with the FastAPI backend on port 7860. `main.py` optionally mounts the built SPA via `COSTUDY4GRID_FRONTEND_DIST` (mounted LAST, after every `/api/*` and `/results/*` route; inert when the dist is absent, so local dev is unaffected). One Space instance serves one player (module-level singletons). See `deploy/huggingface/`.
- **Deployment lockdown (D7, 2026-07)**: the `Dockerfile` sets `COSTUDY4GRID_LOCKDOWN=1`, which disables the desktop-era filesystem RPCs (custom config-file path, session save/list/load, native file picker) with a `403 {code: LOCKED_DOWN}` — they assume a local operator and would otherwise expose the container filesystem to an anonymous Space visitor. The read-only app config stays available so the SPA boots; unset locally so dev is unaffected. The HF deploy is **test-gated** (`workflow_run` on a green Tests run) and **tags each deploy** on origin (`space-deploy-*`) as the rollback pointer. See [`docs/architecture/deployment-trust.md`](docs/architecture/deployment-trust.md).
- **Game Mode (0.8.0)**: a timed, scored session shell in `frontend/src/game/`, **additive and inert unless `?game=1`**`main.tsx` mounts `GameShell` instead of `App`; App integration is three `gameBridge.isGameMode()`-guarded touch points. See `docs/features/game-mode-codabench.md`.
- **Binary assets via Git LFS + transparent network decompression (0.8.0)**: `.gitattributes` tracks `*.zip` / `*.png` / `*.jpg` via Git LFS (the HuggingFace Space git endpoint rejects non-LFS binaries); the large France 225/400 kV grid ships as `network.xiidm.zip` and `network_service._resolve_network_file` / `_extract_network_zip` decompress it transparently on load.
- **CI pipelines**: GitHub Actions only (`.github/workflows/`: `code-quality.yml`, `parity.yml`, `test.yml`, `canary.yml`, `deploy-huggingface.yml`) run the code-quality gate, ruff, the pytest + Vitest suites, the data-pipeline + scorer-parity slice, and the parity scripts. CircleCI was removed (QW23 — GitHub Actions is a strict superset). The backend test + the `Dockerfile` install `expert_op4grid_recommender` from **`recommender-pin.txt`** — one pinned, exact version (QW8) so an upstream release can't silently break a PR or a rebuild; the weekly `canary.yml` floats to the latest release and flags regressions, so upgrades are a deliberate bump of that file.
- Root `.gitignore` excludes `__pycache__/`, `*.pyc`, `*.pyo`; `frontend/.gitignore` handles frontend build artifacts
- Integration helpers and parity scripts live under `scripts/`. They are NOT part of the pytest suite — invoke them directly. The PyPSA-EUR pipeline scripts under `scripts/pypsa_eur/` DO carry pytest coverage (`test_build_pipeline.py`, `test_calibrate_thermal_limits.py`, `test_generate_n1_overloads.py`, `test_regenerate_grid_layout.py`).
- `overrides.txt` contains pinned versions for transitive Python dependencies that need to be forced to specific versions
- **Frontend unit tests** use Vitest + React Testing Library. Isolated component tests live as `*.test.tsx` files next to their component. Run with `cd frontend && npm run test`. No backend mocking is needed for component tests since they only use mocked props.
- The two-step analysis flow (step1: detect overloads, step2: resolve) is the primary user workflow; the single-step `/api/run-analysis` is a legacy alternative
- Session save/load is documented in `docs/features/save-results.md`
- **`grid_layout.json` coordinate scale (2026-05-08)**: the on-disk layout MUST be in raw Mercator metres (span ≈ 1.4–1.6 M for the French grid). pypowsybl emits VL outer circles at a *fixed* `r = 27.5` user-space units, so any layout squashed below ~500 000 units forces overlap on dense regions (Paris/Lyon). `scripts/pypsa_eur/regenerate_grid_layout.py` defaults to raw metres; the legacy `--target-width 8000` flag is preserved but warns. Full rationale + operator-vs-PyPSA comparison in [`docs/data/grid-layout-coordinate-scale.md`](docs/data/grid-layout-coordinate-scale.md).
---
## Contributing & Pull Requests
- **Upstream is `ainetus`; `marota` is the working fork.** Development branches
are pushed to `marota/Co-Study4Grid`, but **pull requests are opened directly
against the upstream `ainetus/Co-Study4Grid`** (base = its default branch,
head = `marota:<branch>`) — *not* against `marota`. The sibling library
`Expert_op4grid_recommender` follows the same rule against
`ainetus/Expert_op4grid_recommender`.
- **Load `ainetus` as an initial source.** A cross-fork PR into `ainetus` can
only be created from a session/tool context that has the `ainetus` repo in
scope, so a new working session should be started with
**`ainetus/Co-Study4Grid` and `ainetus/Expert_op4grid_recommender` as the
initial sources** (they should always be auto-loaded). A session rooted only
at `marota` cannot target `ainetus` (cross-tier adds are blocked) and the PR
step fails with an access-denied error.
- **Sync `marota` with `ainetus` before starting new work.** PRs merge into
`ainetus/main`, but development happens on `marota`, so `marota/main` drifts
behind `ainetus/main` after every merged PR (this applies to **both** repos —
Co-Study4Grid and Expert_op4grid_recommender). **At the start of a dev session,
bring `marota/main` up to date with `ainetus/main`** — GitHub "Sync fork", or
locally `git fetch ainetus main && git merge --ff-only ainetus/main` then push
`marota/main` — and branch from there. Skipping this makes a new branch collide
with the already-merged revisions when it is PR'd into `ainetus`. If the sync
was missed and the PR already shows conflicts, merge `ainetus/main` into the
branch (or rebase onto it) and resolve, then force-with-lease push.
- **DCO sign-off is required on every commit.** The `ainetus` repos enforce the
[Developer Certificate of Origin](https://developercertificate.org/): every
commit must carry a `Signed-off-by: <Name> <amarot91@gmail.com>` trailer, and
because the DCO check matches the sign-off against the commit **author**, the
commit must also be *authored* under that same identity (author email =
`amarot91@gmail.com`):
```bash
git config user.name "<Name>"
git config user.email "amarot91@gmail.com"
git commit -s -m "..." # -s appends the Signed-off-by trailer
```
To sign off commits already made under a different identity, re-author and add
the trailer (`git rebase --exec 'git commit --amend --no-edit --reset-author \
-s' <base>`), then force-with-lease push.
---
## Standalone Interface Parity Audit
The detailed audit — feature inventory, mirror-status table, Layer
1–4 conformity findings, regression-guard matrix, gap-priority list
and delta-vs-previous commits — lives in
[`frontend/PARITY_AUDIT.md`](frontend/PARITY_AUDIT.md). That
document is the working record of the parity project and is
updated as fixes land.
Quick status summary (2026-05-05):
- Canonical distribution is now the auto-generated
`frontend/dist-standalone/standalone.html`
(`npm run build:standalone`). The hand-maintained
`standalone_interface.html` has been decommissioned and
renamed to `standalone_interface_legacy.html` — committed as
a frozen snapshot of its last version (commit `5d2b9d1` content),
do NOT edit further. Regenerate UI from `frontend/src/` via
`npm run build:standalone` instead. The standalone versioned
snapshot was bumped to v0.7 on `adae7ac` to include references
to the new `/api/*-diagram-patch` endpoints.
- Four parity layers run against the React source + the
standalone of choice:
- **Layer 1 — static parity** (`scripts/check_standalone_parity.py`)
- **Layer 2 — session-reload fidelity** (`scripts/check_session_fidelity.py`)
- **Layer 3a — gesture-sequence static proxy** (`scripts/check_gesture_sequence.py`)
- **Layer 3b — behavioural E2E** (`scripts/parity_e2e/e2e_parity.spec.ts`)
- **Layer 4 — user-observable invariants** (`scripts/check_invariants.py`)
- All parity scripts accept `COSTUDY4GRID_STANDALONE_PATH` to
re-target any artifact; they default to the auto-gen bundle
and fall back to the legacy file when the auto-gen is not
built.
See [`frontend/PARITY_AUDIT.md`](frontend/PARITY_AUDIT.md) for
the full gap list, the session-fidelity regression record, the
honest-gap report of what each layer catches and misses, and the
2026-04-20 delta that documents the `/api/restore-analysis-context`
one-way API drift (now resolved) and the auto-generated-standalone
viability confirmation.