@@ -123,21 +123,6 @@ const SchedulePage: React.FC = () => {
123123 setPhaseDurations ( prev => ( { ...prev , [ key ] : Math . max ( 1 , value ) } ) ) ;
124124 } ;
125125
126- // Helper functions for locking/unlocking dates
127- const lockDate = ( fork : string , phaseId : string , itemName : string , date : string ) => {
128- const key = `${ fork } :${ phaseId } :${ itemName } ` ;
129- setLockedDates ( prev => ( { ...prev , [ key ] : date } ) ) ;
130- } ;
131-
132- const unlockDate = ( fork : string , phaseId : string , itemName : string ) => {
133- const key = `${ fork } :${ phaseId } :${ itemName } ` ;
134- setLockedDates ( prev => {
135- const { [ key ] : _removed , ...rest } = prev ;
136- void _removed ; // Intentionally discarding this value
137- return rest ;
138- } ) ;
139- } ;
140-
141126 // Get the effective date (locked value or calculated value)
142127 const getEffectiveDate = ( fork : string , phaseId : string , itemName : string , calculatedDate : string ) : string => {
143128 const key = `${ fork } :${ phaseId } :${ itemName } ` ;
@@ -198,6 +183,99 @@ const SchedulePage: React.FC = () => {
198183 [ hekotaMainnetDate , hekotaDevnetCount , phaseDurations ]
199184 ) ;
200185
186+ // Get all milestones in chronological order for a fork
187+ const getMilestoneOrder = ( fork : string ) : Array < { phaseId : string ; itemName : string } > => {
188+ const projection = fork === 'glamsterdam' ? dynamicGlamsterdamProjection : dynamicHekotaProjection ;
189+ const devnetCount = fork === 'glamsterdam' ? glamsterdamDevnetCount : hekotaDevnetCount ;
190+
191+ const milestones : Array < { phaseId : string ; itemName : string } > = [
192+ { phaseId : 'headliner-selection' , itemName : 'Proposal Deadline' } ,
193+ { phaseId : 'headliner-selection' , itemName : 'Selection Date' } ,
194+ { phaseId : 'eip-selection' , itemName : 'PFI Deadline' } ,
195+ { phaseId : 'eip-selection' , itemName : 'CFI Deadline' } ,
196+ ] ;
197+
198+ // Add devnets
199+ for ( let i = 0 ; i < devnetCount ; i ++ ) {
200+ milestones . push ( { phaseId : 'development' , itemName : `Devnet-${ i } ` } ) ;
201+ }
202+
203+ // Add testnets (skip Holesky as it's deprecated)
204+ const testnetPhase = projection . phases . find ( p => p . phaseId === 'public-testnets' ) ;
205+ testnetPhase ?. testnets ?. forEach ( testnet => {
206+ if ( testnet . status !== 'deprecated' ) {
207+ milestones . push ( { phaseId : 'public-testnets' , itemName : testnet . name } ) ;
208+ }
209+ } ) ;
210+
211+ return milestones ;
212+ } ;
213+
214+ // Get calculated date for a milestone from projections
215+ const getCalculatedDateForMilestone = ( fork : string , phaseId : string , itemName : string ) : string => {
216+ const projection = fork === 'glamsterdam' ? dynamicGlamsterdamProjection : dynamicHekotaProjection ;
217+ const phase = projection . phases . find ( p => p . phaseId === phaseId ) ;
218+
219+ if ( phaseId === 'headliner-selection' || phaseId === 'eip-selection' ) {
220+ const substep = phase ?. substeps ?. find ( s => s . name === itemName ) ;
221+ return substep ?. date || substep ?. projectedDate || '' ;
222+ } else if ( phaseId === 'development' ) {
223+ const devnet = phase ?. devnets ?. find ( d => d . name === itemName ) ;
224+ return devnet ?. date || devnet ?. projectedDate || '' ;
225+ } else if ( phaseId === 'public-testnets' ) {
226+ const testnet = phase ?. testnets ?. find ( t => t . name === itemName ) ;
227+ return testnet ?. date || testnet ?. projectedDate || '' ;
228+ }
229+ return '' ;
230+ } ;
231+
232+ // Lock a date and cascade to all previous milestones
233+ const lockDate = ( fork : string , phaseId : string , itemName : string , date : string ) => {
234+ // Fusaka is read-only, no cascading needed
235+ if ( fork === 'fusaka' ) {
236+ const key = `${ fork } :${ phaseId } :${ itemName } ` ;
237+ setLockedDates ( prev => ( { ...prev , [ key ] : date } ) ) ;
238+ return ;
239+ }
240+
241+ const milestones = getMilestoneOrder ( fork ) ;
242+ const targetIndex = milestones . findIndex ( m => m . phaseId === phaseId && m . itemName === itemName ) ;
243+
244+ if ( targetIndex === - 1 ) {
245+ // Fallback: just lock the single date
246+ const key = `${ fork } :${ phaseId } :${ itemName } ` ;
247+ setLockedDates ( prev => ( { ...prev , [ key ] : date } ) ) ;
248+ return ;
249+ }
250+
251+ // Lock all milestones from start up to and including the target
252+ setLockedDates ( prev => {
253+ const newLocked = { ...prev } ;
254+ for ( let i = 0 ; i <= targetIndex ; i ++ ) {
255+ const milestone = milestones [ i ] ;
256+ const key = `${ fork } :${ milestone . phaseId } :${ milestone . itemName } ` ;
257+ // Only lock if not already locked
258+ if ( ! ( key in newLocked ) ) {
259+ const calcDate = getCalculatedDateForMilestone ( fork , milestone . phaseId , milestone . itemName ) ;
260+ const effectiveDate = prev [ key ] ?? calcDate ;
261+ newLocked [ key ] = effectiveDate ;
262+ }
263+ }
264+ // Always set the target date to the specified value
265+ newLocked [ `${ fork } :${ phaseId } :${ itemName } ` ] = date ;
266+ return newLocked ;
267+ } ) ;
268+ } ;
269+
270+ const unlockDate = ( fork : string , phaseId : string , itemName : string ) => {
271+ const key = `${ fork } :${ phaseId } :${ itemName } ` ;
272+ setLockedDates ( prev => {
273+ const { [ key ] : _removed , ...rest } = prev ;
274+ void _removed ;
275+ return rest ;
276+ } ) ;
277+ } ;
278+
201279 useMetaTags ( {
202280 title : 'ACD Planning Sandbox - Forkcast' ,
203281 description : 'Internal planning tool for Ethereum core developers. Explore hypothetical upgrade timelines - these are not committed dates.' ,
0 commit comments