@@ -39,6 +39,7 @@ interface MigrationWizardProps {
3939export function MigrationWizard ( { characterId, onClose, onComplete } : MigrationWizardProps ) {
4040 const wizard = useMigrationWizard ( characterId ) ;
4141 const [ isApplying , setIsApplying ] = useState ( false ) ;
42+ const [ isSyncing , setIsSyncing ] = useState ( false ) ;
4243
4344 // Get breaking changes that need decisions
4445 const breakingChanges = wizard . report ?. changes . filter ( ( c ) => c . severity === "breaking" ) || [ ] ;
@@ -61,6 +62,24 @@ export function MigrationWizard({ characterId, onClose, onComplete }: MigrationW
6162 }
6263 } ;
6364
65+ const handleFreshSync = async ( ) => {
66+ setIsSyncing ( true ) ;
67+ try {
68+ const response = await fetch ( `/api/characters/${ characterId } /sync/migrate` , {
69+ method : "POST" ,
70+ headers : { "Content-Type" : "application/json" } ,
71+ body : JSON . stringify ( { freshSync : true } ) ,
72+ } ) ;
73+
74+ if ( response . ok ) {
75+ onComplete ?.( ) ;
76+ onClose ( ) ;
77+ }
78+ } finally {
79+ setIsSyncing ( false ) ;
80+ }
81+ } ;
82+
6483 if ( ! wizard . report ) {
6584 return (
6685 < WizardContainer onClose = { onClose } >
@@ -71,19 +90,28 @@ export function MigrationWizard({ characterId, onClose, onComplete }: MigrationW
7190 ) ;
7291 }
7392
93+ const isNoBaseline = wizard . report . noBaseline && wizard . report . changes . length === 0 ;
94+
7495 return (
7596 < WizardContainer onClose = { onClose } >
7697 { /* Progress indicator */ }
77- < WizardProgress
78- currentStep = { wizard . currentStep }
79- totalSteps = { wizard . totalSteps }
80- breakingChanges = { breakingChanges . length }
81- />
98+ { ! isNoBaseline && (
99+ < WizardProgress
100+ currentStep = { wizard . currentStep }
101+ totalSteps = { wizard . totalSteps }
102+ breakingChanges = { breakingChanges . length }
103+ />
104+ ) }
82105
83106 { /* Content area */ }
84107 < div className = "p-6" >
85108 { wizard . currentStep === 0 && (
86- < ReviewStep report = { wizard . report } breakingCount = { breakingChanges . length } />
109+ < ReviewStep
110+ report = { wizard . report }
111+ breakingCount = { breakingChanges . length }
112+ onFreshSync = { isNoBaseline ? handleFreshSync : undefined }
113+ isSyncing = { isSyncing }
114+ />
87115 ) }
88116
89117 { currentChange && (
@@ -101,17 +129,19 @@ export function MigrationWizard({ characterId, onClose, onComplete }: MigrationW
101129 ) }
102130 </ div >
103131
104- { /* Footer with navigation */ }
105- < WizardFooter
106- currentStep = { wizard . currentStep }
107- totalSteps = { wizard . totalSteps }
108- canApply = { wizard . canApply }
109- isApplying = { isApplying }
110- onPrev = { wizard . prevStep }
111- onNext = { wizard . nextStep }
112- onApply = { handleApply }
113- onCancel = { onClose }
114- />
132+ { /* Footer with navigation (hidden when showing fresh sync UI) */ }
133+ { ! isNoBaseline && (
134+ < WizardFooter
135+ currentStep = { wizard . currentStep }
136+ totalSteps = { wizard . totalSteps }
137+ canApply = { wizard . canApply }
138+ isApplying = { isApplying }
139+ onPrev = { wizard . prevStep }
140+ onNext = { wizard . nextStep }
141+ onApply = { handleApply }
142+ onCancel = { onClose }
143+ />
144+ ) }
115145 </ WizardContainer >
116146 ) ;
117147}
@@ -226,11 +256,47 @@ function WizardProgress({ currentStep, totalSteps, breakingChanges }: WizardProg
226256// =============================================================================
227257
228258interface ReviewStepProps {
229- report : { changes : DriftChange [ ] } ;
259+ report : { changes : DriftChange [ ] ; noBaseline ?: boolean } ;
230260 breakingCount : number ;
261+ onFreshSync ?: ( ) => void ;
262+ isSyncing ?: boolean ;
231263}
232264
233- function ReviewStep ( { report, breakingCount } : ReviewStepProps ) {
265+ function ReviewStep ( { report, breakingCount, onFreshSync, isSyncing } : ReviewStepProps ) {
266+ // No baseline: show fresh sync UI
267+ if ( report . noBaseline && report . changes . length === 0 && onFreshSync ) {
268+ return (
269+ < div className = "space-y-6" >
270+ < div >
271+ < h3 className = "text-lg font-medium text-gray-900 dark:text-white mb-2" >
272+ Ruleset Snapshot Missing
273+ </ h3 >
274+ < p className = "text-sm text-gray-500 dark:text-gray-400" >
275+ This character references a ruleset snapshot that no longer exists. This can happen when
276+ a character was created before the snapshot system was set up.
277+ </ p >
278+ </ div >
279+
280+ < div className = "bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4" >
281+ < p className = "text-sm text-blue-700 dark:text-blue-300 mb-4" >
282+ Sync to the current rules to create a baseline snapshot. Future ruleset changes will be
283+ tracked against this baseline.
284+ </ p >
285+ < button
286+ onClick = { onFreshSync }
287+ disabled = { isSyncing }
288+ className = { `
289+ px-4 py-2 text-sm font-medium text-white rounded-lg
290+ ${ isSyncing ? "bg-gray-400 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700" }
291+ ` }
292+ >
293+ { isSyncing ? "Syncing..." : "Sync to Current Rules" }
294+ </ button >
295+ </ div >
296+ </ div >
297+ ) ;
298+ }
299+
234300 const nonBreaking = report . changes . filter ( ( c ) => c . severity !== "breaking" ) ;
235301 const breaking = report . changes . filter ( ( c ) => c . severity === "breaking" ) ;
236302
0 commit comments