@@ -652,49 +652,146 @@ export const useAdsStore = create<AdsStore>((set, get) => ({
652652 if ( get ( ) . scenarioRun ?. active ) return ;
653653
654654 const title = scenarioTitle ( kind ) ;
655- set ( {
656- scenarioRun : {
657- active : true ,
658- title,
659- step : 0 ,
660- total : 2 ,
661- message : "Preparing timed scenario. This uses the same application command path as operator actions." ,
662- } ,
663- eventLog : [ `Scenario started: ${ title } . Step timing ${ sequenceDelayMs / 1000 } s.` , ...get ( ) . eventLog ] . slice ( 0 , 120 ) ,
664- } ) ;
655+ const previewDelayMs = 1000 ;
656+ const actionDelayMs = 800 ;
657+
658+ const totalByKind : Record < ScenarioKind , number > = {
659+ manual_relief : 2 ,
660+ topology_split : 2 ,
661+ generation_derate : 2 ,
662+ frequency_islanding : 5 ,
663+ ols_overload : 2 ,
664+ ogs_surplus : 6 ,
665+ } ;
665666
666- await wait ( ) ;
667+ const total = totalByKind [ kind ] ?? 2 ;
668+ let step = 0 ;
669+
670+ const emit = ( message : string , log ?: string ) => {
671+ step += 1 ;
672+ set ( {
673+ scenarioRun : {
674+ active : true ,
675+ title,
676+ step,
677+ total,
678+ message,
679+ } ,
680+ eventLog : [ log ?? `${ title } : ${ message } ` , ...get ( ) . eventLog ] . slice ( 0 , 120 ) ,
681+ } ) ;
682+ } ;
683+
684+ const clearPreview = ( ) => {
685+ if ( get ( ) . hoverObjectId ) get ( ) . setHoverObject ( null ) ;
686+ } ;
687+
688+ const previewObject = async ( objectId : string , message : string ) => {
689+ emit (
690+ `Preview: ${ message } ` ,
691+ `Scenario preview: hover ${ objectId } . Trip Matrix row is shown before the action is executed.` ,
692+ ) ;
693+ get ( ) . setHoverObject ( objectId ) ;
694+ await wait ( previewDelayMs ) ;
695+ } ;
696+
697+ const executeObjectOpen = async ( objectId : string , message : string ) => {
698+ emit (
699+ `Execute: ${ message } ` ,
700+ `Scenario action: execute ${ objectId } through the same SLD click path.` ,
701+ ) ;
702+ const state = get ( ) . objectStates [ objectId ] ?? "closed" ;
703+ if ( state !== "open" && state !== "failed" ) {
704+ get ( ) . toggleObject ( objectId ) ;
705+ } else {
706+ set ( {
707+ eventLog : [ `Scenario action skipped: ${ objectId } is already open.` , ...get ( ) . eventLog ] . slice ( 0 , 120 ) ,
708+ } ) ;
709+ }
710+ clearPreview ( ) ;
711+ await wait ( actionDelayMs ) ;
712+ } ;
713+
714+ const applyFrequencyPreview = async ( hz : number , message : string ) => {
715+ emit (
716+ message ,
717+ `Frequency guided preview: ${ hz . toFixed ( 2 ) } Hz. Rebuilding Trip Matrix without executing load trip yet.` ,
718+ ) ;
719+ const current = get ( ) ;
720+ const tripMatrix = buildMatrix (
721+ current . feeders ,
722+ current . objectStates ,
723+ current . contingencyRules ,
724+ current . sourceMw ,
725+ current . minReserveMw ,
726+ hz ,
727+ ) ;
728+ set ( {
729+ frequencyHz : hz ,
730+ ...matrixState ( tripMatrix ) ,
731+ decision : tripMatrix . baseDecision ,
732+ hoverDecision : null ,
733+ hoverObjectId : null ,
734+ } ) ;
735+ await wait ( actionDelayMs ) ;
736+ } ;
737+
738+ const applyScenarioAfterPreview = async ( scenarioKind : ScenarioKind , scenarioValue : number | undefined , message : string ) => {
739+ emit ( message , `Scenario action: apply ${ scenarioKind } after visual preview.` ) ;
740+ clearPreview ( ) ;
741+ get ( ) . injectScenario ( scenarioKind , scenarioValue ) ;
742+ await wait ( actionDelayMs ) ;
743+ } ;
667744
668745 set ( {
669746 scenarioRun : {
670747 active : true ,
671748 title,
672- step : 1 ,
673- total : 2 ,
674- message :
675- kind === "topology_split"
676- ? "Operator action: open Bus B coupler."
677- : kind === "frequency_islanding"
678- ? `Parameter action: inject ${ ( value ?? 48.25 ) . toFixed ( 2 ) } Hz.`
679- : "Scenario action: apply system event and rebuild Trip Matrix." ,
749+ step : 0 ,
750+ total,
751+ message : "Starting guided scenario. Hover preview is shown first, then the same action path as SLD click is executed." ,
680752 } ,
753+ eventLog : [ `Scenario started: ${ title } . Preview delay 1.0s, action delay ${ ( actionDelayMs / 1000 ) . toFixed ( 1 ) } s.` , ...get ( ) . eventLog ] . slice ( 0 , 120 ) ,
681754 } ) ;
682755
756+ await wait ( 300 ) ;
757+
683758 if ( kind === "topology_split" ) {
684- if ( get ( ) . objectStates . LINE_COUPLER !== "open" ) get ( ) . toggleObject ( "LINE_COUPLER" ) ;
759+ await previewObject ( "LINE_COUPLER" , "Bus B coupler contingency. Watch Trip Matrix arming before the coupler opens." ) ;
760+ await executeObjectOpen ( "LINE_COUPLER" , "Open Bus B coupler as an operator CB action." ) ;
761+ } else if ( kind === "ogs_surplus" ) {
762+ await previewObject ( "LINE_AC" , "Open Line A-C to start separating the C area." ) ;
763+ await executeObjectOpen ( "LINE_AC" , "Trip Line A-C." ) ;
764+
765+ await previewObject ( "LINE_BC" , "Open Line B-C so C is separated from the A/B network." ) ;
766+ await executeObjectOpen ( "LINE_BC" , "Trip Line B-C." ) ;
767+
768+ await previewObject ( "IBT_C" , "Trip IBT C / grid support. This tests whether the C area becomes a true island and whether OGS/runback is required." ) ;
769+ await executeObjectOpen ( "IBT_C" , "Trip IBT C as the final island-forming contingency." ) ;
770+ } else if ( kind === "generation_derate" ) {
771+ await previewObject ( "GEN_C2" , "KIT C2 derate scenario. Generator stays online but available MW is reduced." ) ;
772+ await applyScenarioAfterPreview ( "generation_derate" , value , "Apply KIT C2 derate and rebuild PowerFlowLite + Trip Matrix." ) ;
773+ } else if ( kind === "ols_overload" ) {
774+ await previewObject ( "IBT_C" , "IBT C overload scenario. ADS should show why the load-side relief target is selected." ) ;
775+ await applyScenarioAfterPreview ( "ols_overload" , value , "Apply IBT C overload test case after preview." ) ;
776+ } else if ( kind === "frequency_islanding" ) {
777+ const finalHz = value ?? 48.25 ;
778+ await applyFrequencyPreview ( 50.0 , "Frequency normal reference: 50.00 Hz. No frequency-based ADS action yet." ) ;
779+ await applyFrequencyPreview ( 49.0 , "Frequency enters UFLS coordination zone. ADS prepares but does not blindly trip." ) ;
780+ await applyFrequencyPreview ( 48.4 , "Frequency approaches islanding threshold. Trip Matrix is recalculated from the current state." ) ;
781+ await applyFrequencyPreview ( finalHz , `Final frequency injection ${ finalHz . toFixed ( 2 ) } Hz. ADS preview is now visible before execution.` ) ;
782+ await applyScenarioAfterPreview ( "frequency_islanding" , finalHz , `Execute frequency injection at ${ finalHz . toFixed ( 2 ) } Hz.` ) ;
685783 } else {
686- get ( ) . injectScenario ( kind , value ) ;
784+ await applyScenarioAfterPreview ( kind , value , "Apply timed scenario." ) ;
687785 }
688786
689- await wait ( ) ;
690-
787+ clearPreview ( ) ;
691788 set ( {
692789 scenarioRun : {
693790 active : false ,
694791 title,
695- step : 2 ,
696- total : 2 ,
697- message : "Scenario complete. Review Power Flow cards , Trip Matrix, and Event Log." ,
792+ step : total ,
793+ total,
794+ message : "Guided scenario complete. Review SLD response, Power Flow card , Trip Matrix, and Event Log." ,
698795 } ,
699796 eventLog : [ `Scenario complete: ${ title } .` , ...get ( ) . eventLog ] . slice ( 0 , 120 ) ,
700797 } ) ;
0 commit comments