Phase 36: Scenario Tweaker & Polish¶
Status: Complete Date: 2026-03-06 Tests: ~59 new (13 Python + 46 vitest), ~7,705 total (7,474 Python + 231 vitest)
Overview¶
Final phase of Block 3. Adds clone-and-tweak scenario editing, export capabilities, keyboard shortcuts, and application polish. Zero simulation engine changes.
Sub-phases¶
36a: Scenario Tweaker¶
- Backend:
POST /api/scenarios/validate(pydantic CampaignScenarioConfig validation),POST /api/runs/from-config(inline config → temp YAML → RunManager) - Frontend:
useScenarioEditorreducer hook (11 action types), ScenarioEditorPage (two-column layout), GeneralSection, TerrainSection, WeatherSection, ForceEditor, UnitPicker modal, ConfigToggles (6 optional systems), CalibrationSliders (4 params), YamlPreview - Route:
/scenarios/:name/edit, "Clone & Tweak" button on ScenarioDetailPage
36b: Terrain Preview¶
- TerrainPreview canvas component showing terrain type color fill, objective circles, dimension labels
terrainTypeColors.ts— 10 terrain type string → color mappings
36c: Export & Reporting¶
useExporthook: downloadJSON, downloadCSV, downloadYAML (client-side Blob + invisible anchor), printReport (navigate)ExportMenucomponent (Headless UI Menu dropdown)PrintReportPage— print-optimized layout with summary, forces, narrative. Auto-triggerswindow.print()eventsToCsvRowsin format.ts- "Download YAML" button on ScenarioDetailPage
- Export menu on RunDetailPage (JSON, CSV, narrative txt, print)
36d: Polish¶
useKeyboardShortcutshook — global keydown listener, skips inputs/textareasKeyboardShortcutHelpmodal (Headless UI Dialog)- Map keyboard shortcuts: Space (play/pause), Arrow keys (step), 1-4 (speed)
- Responsive sidebar: mobile hamburger + overlay, desktop always-visible
ErrorMessagevariant prop:error|warning|connection- WebSocket reconnect: exponential backoff (1s/2s/4s), max 3 attempts,
connectionStateenum RunProgressPanel: connection state display, polling fallback on WS failure
Dependencies¶
- New npm:
js-yaml+@types/js-yaml
File Inventory¶
Backend (3 modified + 1 new test)¶
api/schemas.py— 3 new models (RunFromConfigRequest, ValidateConfigRequest, ValidateConfigResponse)api/routers/scenarios.py— POST /validate endpointapi/routers/runs.py— POST /from-config endpointtests/api/test_scenario_editor.py— 13 tests
Frontend (~20 new + ~12 modified)¶
- 12 new page/component files in
src/pages/editor/ - 3 new lib/hook files (yamlExport, useExport, useKeyboardShortcuts)
- 3 new component files (ExportMenu, KeyboardShortcutHelp, PrintReportPage)
- 1 new types file (editor.ts), 1 new API client (editor.ts)
- Modified: App.tsx, Layout.tsx, Sidebar.tsx, ErrorMessage.tsx, RunDetailPage.tsx, ScenarioDetailPage.tsx, RunProgressPanel.tsx, TacticalMap.tsx, useWebSocket.ts, format.ts
Test Files (11 new)¶
- 6 frontend test files for 36a (editor, API, hooks, pages)
- 1 frontend test for 36b (TerrainPreview)
- 2 frontend tests for 36c (useExport, ExportMenu)
- 2 frontend tests for 36d (useKeyboardShortcuts, KeyboardShortcutHelp)
Design Decisions¶
- Inline config via temp YAML:
POST /from-configwrites totempfile.mkdtemp(), avoids filesystem persistence of custom scenarios - Client-side YAML download:
js-yamlserializes config dict, browser download via Blob — no backend write endpoint - No PDF library:
window.print()with print-optimized CSS, Plotly charts excluded from print - Validation via backend: Single source of truth using pydantic CampaignScenarioConfig
- Keyboard shortcuts skip inputs: Global keydown, checks
e.target.tagName - WS reconnect with backoff: 3 attempts max, falls back to polling
Lessons Learned¶
@headlessui/reactv2 requires ResizeObserver: Test environments needvi.stubGlobal('ResizeObserver', ...)for Menu components- JSX in test files needs
.tsxextension: vitest with esbuild won't transform JSX in.tsfiles structuredClonefor state init: Prevents shared reference bugs in reducer initializer- Canvas mock strategy carries forward: Same pattern from Phase 35 — mock
getContextwith property setters for style props