2025-10-16 10:59:50 -06:00
import $ from 'jquery' ;
import { config , translations } from 'grav-config' ;
import formatBytes from '../utils/formatbytes' ;
import request from '../utils/request' ;
const t = ( key , fallback = '' ) => {
if ( translations && translations . PLUGIN _ADMIN && translations . PLUGIN _ADMIN [ key ] ) {
return translations . PLUGIN _ADMIN [ key ] ;
}
return fallback ;
} ;
const r = ( key , value , fallback = '' ) => {
const template = t ( key , fallback ) ;
if ( ! template || typeof template . replace !== 'function' ) {
return fallback . replace ( '%s' , value ) ;
}
return template . replace ( '%s' , value ) ;
} ;
const STAGE _TITLES = {
2025-10-16 17:31:57 -06:00
queued : ( ) => t ( 'SAFE_UPGRADE_STAGE_QUEUED' , 'Waiting for worker' ) ,
2025-10-16 10:59:50 -06:00
initializing : ( ) => t ( 'SAFE_UPGRADE_STAGE_INITIALIZING' , 'Preparing upgrade' ) ,
downloading : ( ) => t ( 'SAFE_UPGRADE_STAGE_DOWNLOADING' , 'Downloading update' ) ,
installing : ( ) => t ( 'SAFE_UPGRADE_STAGE_INSTALLING' , 'Installing update' ) ,
finalizing : ( ) => t ( 'SAFE_UPGRADE_STAGE_FINALIZING' , 'Finalizing changes' ) ,
complete : ( ) => t ( 'SAFE_UPGRADE_STAGE_COMPLETE' , 'Upgrade complete' ) ,
error : ( ) => t ( 'SAFE_UPGRADE_STAGE_ERROR' , 'Upgrade encountered an error' )
} ;
export default class SafeUpgrade {
constructor ( updatesInstance ) {
this . updates = updatesInstance ;
this . modalElement = $ ( '[data-remodal-id="update-grav"]' ) ;
this . modal = this . modalElement . remodal ( { hashTracking : false } ) ;
this . steps = {
preflight : this . modalElement . find ( '[data-safe-upgrade-step="preflight"]' ) ,
progress : this . modalElement . find ( '[data-safe-upgrade-step="progress"]' ) ,
result : this . modalElement . find ( '[data-safe-upgrade-step="result"]' )
} ;
this . buttons = {
start : this . modalElement . find ( '[data-safe-upgrade-action="start"]' ) ,
cancel : this . modalElement . find ( '[data-safe-upgrade-action="cancel"]' ) ,
recheck : this . modalElement . find ( '[data-safe-upgrade-action="recheck"]' )
} ;
this . urls = this . buildUrls ( ) ;
this . decisions = { } ;
this . pollTimer = null ;
2025-10-16 13:49:53 -06:00
this . statusRequest = null ;
this . isPolling = false ;
2025-10-16 10:59:50 -06:00
this . active = false ;
2025-10-16 17:31:57 -06:00
this . jobId = null ;
2025-10-16 10:59:50 -06:00
this . registerEvents ( ) ;
}
buildUrls ( ) {
const task = ` task ${ config . param _sep } ` ;
const nonce = ` admin-nonce ${ config . param _sep } ${ config . admin _nonce } ` ;
const base = ` ${ config . base _url _relative } /update.json ` ;
return {
preflight : ` ${ base } / ${ task } safeUpgradePreflight/ ${ nonce } ` ,
start : ` ${ base } / ${ task } safeUpgradeStart/ ${ nonce } ` ,
status : ` ${ base } / ${ task } safeUpgradeStatus/ ${ nonce } `
} ;
}
registerEvents ( ) {
$ ( document ) . on ( 'click' , '#grav-update-button' , ( event ) => {
if ( $ ( event . currentTarget ) . hasClass ( 'pointer-events-none' ) ) {
return ;
}
event . preventDefault ( ) ;
this . open ( ) ;
} ) ;
this . modalElement . on ( 'closed' , ( ) => {
this . stopPolling ( ) ;
this . active = false ;
} ) ;
this . modalElement . on ( 'click' , '[data-safe-upgrade-action="recheck"]' , ( event ) => {
event . preventDefault ( ) ;
if ( ! this . active ) {
return ;
}
this . fetchPreflight ( true ) ;
} ) ;
this . modalElement . on ( 'click' , '[data-safe-upgrade-action="start"]' , ( event ) => {
event . preventDefault ( ) ;
if ( $ ( event . currentTarget ) . prop ( 'disabled' ) ) {
return ;
}
this . startUpgrade ( ) ;
} ) ;
this . modalElement . on ( 'change' , '[data-safe-upgrade-decision]' , ( event ) => {
const target = $ ( event . currentTarget ) ;
const decision = target . val ( ) ;
const type = target . data ( 'safe-upgrade-decision' ) ;
this . decisions [ type ] = decision ;
this . updateStartButtonState ( ) ;
} ) ;
}
setPayload ( payload = { } ) {
this . payload = payload ;
}
open ( ) {
this . active = true ;
this . decisions = { } ;
this . renderLoading ( ) ;
this . modal . open ( ) ;
this . fetchPreflight ( ) ;
}
renderLoading ( ) {
this . switchStep ( 'preflight' ) ;
this . steps . preflight . html ( `
< div class = "safe-upgrade-loading" >
< span class = "fa fa-refresh fa-spin" > < / s p a n >
< p > $ { t ( 'SAFE_UPGRADE_CHECKING' , 'Running preflight checks...' ) } < / p >
< / d i v >
` );
this . buttons . start . prop ( 'disabled' , true ) . addClass ( 'hidden' ) ;
this . modalElement . find ( '[data-safe-upgrade-footer]' ) . removeClass ( 'hidden' ) ;
}
fetchPreflight ( silent = false ) {
if ( ! silent ) {
this . renderLoading ( ) ;
}
request ( this . urls . preflight , ( response ) => {
if ( ! this . active ) {
return ;
}
if ( response . status === 'error' ) {
this . renderPreflightError ( response . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) ) ;
return ;
}
this . renderPreflight ( response . data || { } ) ;
} ) ;
}
renderPreflightError ( message ) {
this . switchStep ( 'preflight' ) ;
this . steps . preflight . html ( `
< div class = "safe-upgrade-error" >
< p > $ { message } < / p >
< button data - safe - upgrade - action = "recheck" class = "button secondary" > $ { t ( 'SAFE_UPGRADE_RECHECK' , 'Re-run Checks' ) } < / b u t t o n >
< / d i v >
` );
this . buttons . start . prop ( 'disabled' , true ) . addClass ( 'hidden' ) ;
}
renderPreflight ( data ) {
const blockers = [ ] ;
const version = data . version || { } ;
const releaseDate = version . release _date || '' ;
const packageSize = version . package _size ? formatBytes ( version . package _size ) : t ( 'SAFE_UPGRADE_UNKNOWN_SIZE' , 'unknown' ) ;
const warnings = ( data . preflight && data . preflight . warnings ) || [ ] ;
const pending = ( data . preflight && data . preflight . plugins _pending ) || { } ;
const psrConflicts = ( data . preflight && data . preflight . psr _log _conflicts ) || { } ;
const monologConflicts = ( data . preflight && data . preflight . monolog _conflicts ) || { } ;
if ( data . status === 'error' ) {
blockers . push ( data . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) ) ;
}
if ( ! data . requirements || ! data . requirements . meets ) {
blockers . push ( r ( 'SAFE_UPGRADE_REQUIREMENTS_FAIL' , data . requirements ? data . requirements . minimum _php : '?' , 'PHP %s or newer is required before continuing.' ) ) ;
}
if ( data . symlinked ) {
blockers . push ( t ( 'GRAV_SYMBOLICALLY_LINKED' , 'Grav is symbolically linked. Upgrade will not be available.' ) ) ;
}
if ( data . safe _upgrade && data . safe _upgrade . enabled === false ) {
blockers . push ( t ( 'SAFE_UPGRADE_DISABLED' , 'Safe upgrade is disabled. Enable it in Configuration ▶ System ▶ Updates.' ) ) ;
}
if ( ! data . safe _upgrade || ! data . safe _upgrade . staging _ready ) {
const err = data . safe _upgrade && data . safe _upgrade . error ? data . safe _upgrade . error : t ( 'SAFE_UPGRADE_STAGING_ERROR' , 'Safe upgrade staging directory is not writable.' ) ;
blockers . push ( err ) ;
}
if ( ! data . upgrade _available ) {
blockers . push ( t ( 'SAFE_UPGRADE_NOT_AVAILABLE' , 'No Grav update is available.' ) ) ;
}
if ( Object . keys ( pending ) . length ) {
blockers . push ( t ( 'SAFE_UPGRADE_PENDING_HINT' , 'Update all plugins and themes before proceeding.' ) ) ;
}
const warningsList = warnings . length ? `
< div class = "safe-upgrade-alert" >
< strong > $ { t ( 'SAFE_UPGRADE_WARNINGS' , 'Warnings' ) } < / s t r o n g >
< ul > $ { warnings . map ( ( warning ) => ` <li> ${ warning } </li> ` ) . join ( '' ) } < / u l >
< / d i v >
` : '';
const pendingList = Object . keys ( pending ) . length ? `
< div class = "safe-upgrade-pending" >
< strong > $ { t ( 'SAFE_UPGRADE_PENDING_UPDATES' , 'Pending plugin or theme updates' ) } < / s t r o n g >
< ul >
$ { Object . keys ( pending ) . map ( ( slug ) => {
const item = pending [ slug ] || { } ;
const type = item . type || 'plugin' ;
const current = item . current || t ( 'SAFE_UPGRADE_UNKNOWN_VERSION' , 'unknown' ) ;
const next = item . available || t ( 'SAFE_UPGRADE_UNKNOWN_VERSION' , 'unknown' ) ;
return ` <li><code> ${ slug } </code> ( ${ type } ) ${ current } → ${ next } </li> ` ;
} ) . join ( '' ) }
< / u l >
< / d i v >
` : '';
const psrList = Object . keys ( psrConflicts ) . length ? `
< div class = "safe-upgrade-conflict" >
< div class = "safe-upgrade-conflict-header" >
< strong > $ { t ( 'SAFE_UPGRADE_CONFLICTS_PSR' , 'Potential psr/log compatibility issues' ) } < / s t r o n g >
$ { this . renderDecisionSelect ( 'psr_log' ) }
< / d i v >
< ul >
$ { Object . keys ( psrConflicts ) . map ( ( slug ) => {
const info = psrConflicts [ slug ] || { } ;
const requires = info . requires || '*' ;
return ` <li><code> ${ slug } </code> — ${ r ( 'SAFE_UPGRADE_CONFLICTS_REQUIRES' , requires , 'Requires psr/log %s' ) } </li> ` ;
} ) . join ( '' ) }
< / u l >
< / d i v >
` : '';
const monologList = Object . keys ( monologConflicts ) . length ? `
< div class = "safe-upgrade-conflict" >
< div class = "safe-upgrade-conflict-header" >
< strong > $ { t ( 'SAFE_UPGRADE_CONFLICTS_MONOLOG' , 'Potential Monolog API compatibility issues' ) } < / s t r o n g >
$ { this . renderDecisionSelect ( 'monolog' ) }
< / d i v >
< ul >
$ { Object . keys ( monologConflicts ) . map ( ( slug ) => {
const entries = Array . isArray ( monologConflicts [ slug ] ) ? monologConflicts [ slug ] : [ ] ;
const details = entries . map ( ( entry ) => {
const method = entry . method || '' ;
const file = entry . file ? basename ( entry . file ) : '' ;
return ` <span> ${ method } ${ file ? ` <code> ${ file } </code> ` : '' } </span> ` ;
} ) . join ( ', ' ) ;
return ` <li><code> ${ slug } </code> — ${ details } </li> ` ;
} ) . join ( '' ) }
< / u l >
< / d i v >
` : '';
const blockersList = blockers . length ? `
< div class = "safe-upgrade-blockers" >
< ul > $ { blockers . map ( ( item ) => ` <li> ${ item } </li> ` ) . join ( '' ) } < / u l >
< / d i v >
` : '';
const summary = `
< div class = "safe-upgrade-summary" >
< p > $ { r ( 'SAFE_UPGRADE_SUMMARY_CURRENT' , version . local || '?' , 'Current Grav version: <strong>v%s</strong>' ) } < / p >
< p > $ { r ( 'SAFE_UPGRADE_SUMMARY_REMOTE' , version . remote || '?' , 'Available Grav version: <strong>v%s</strong>' ) } < / p >
< p > $ { releaseDate ? r ( 'SAFE_UPGRADE_RELEASED_ON' , releaseDate , 'Released on %s' ) : '' } < / p >
< p > $ { r ( 'SAFE_UPGRADE_PACKAGE_SIZE' , packageSize , 'Package size: %s' ) } < / p >
< / d i v >
` ;
this . steps . preflight . html ( `
< div class = "safe-upgrade-preflight" >
$ { summary }
$ { warningsList }
$ { pendingList }
$ { psrList }
$ { monologList }
$ { blockersList }
< div class = "safe-upgrade-actions inline-actions" >
< button data - safe - upgrade - action = "recheck" class = "button secondary" > $ { t ( 'SAFE_UPGRADE_RECHECK' , 'Re-run Checks' ) } < / b u t t o n >
< / d i v >
< / d i v >
` );
this . switchStep ( 'preflight' ) ;
const hasBlockingConflicts = ( Object . keys ( psrConflicts ) . length && ! this . decisions . psr _log ) || ( Object . keys ( monologConflicts ) . length && ! this . decisions . monolog ) ;
const canStart = ! blockers . length && ! hasBlockingConflicts ;
this . buttons . start
. removeClass ( 'hidden' )
. prop ( 'disabled' , ! canStart )
. text ( t ( 'SAFE_UPGRADE_START' , 'Start Safe Upgrade' ) ) ;
if ( Object . keys ( psrConflicts ) . length && ! this . decisions . psr _log ) {
this . decisions . psr _log = 'disable' ;
}
if ( Object . keys ( monologConflicts ) . length && ! this . decisions . monolog ) {
this . decisions . monolog = 'disable' ;
}
this . updateStartButtonState ( ) ;
}
renderDecisionSelect ( type ) {
return `
< label class = "safe-upgrade-decision" >
< span > $ { t ( 'SAFE_UPGRADE_DECISION_PROMPT' , 'When conflicts are detected:' ) } < / s p a n >
< select data - safe - upgrade - decision = "${type}" >
< option value = "disable" > $ { t ( 'SAFE_UPGRADE_DECISION_DISABLE' , 'Disable conflicting plugins' ) } < / o p t i o n >
< option value = "continue" > $ { t ( 'SAFE_UPGRADE_DECISION_CONTINUE' , 'Continue with plugins enabled' ) } < / o p t i o n >
< / s e l e c t >
< / l a b e l >
` ;
}
updateStartButtonState ( ) {
const decisionInputs = this . modalElement . find ( '[data-safe-upgrade-decision]' ) ;
const unresolved = [ ] ;
decisionInputs . each ( ( index , element ) => {
const input = $ ( element ) ;
const key = input . data ( 'safe-upgrade-decision' ) ;
if ( ! this . decisions [ key ] ) {
unresolved . push ( key ) ;
}
} ) ;
const hasUnresolvedConflicts = unresolved . length > 0 ;
const blockers = this . steps . preflight . find ( '.safe-upgrade-blockers li' ) ;
const disabled = hasUnresolvedConflicts || blockers . length > 0 ;
this . buttons . start . prop ( 'disabled' , disabled ) ;
}
startUpgrade ( ) {
this . switchStep ( 'progress' ) ;
this . renderProgress ( {
stage : 'initializing' ,
message : t ( 'SAFE_UPGRADE_STAGE_INITIALIZING' , 'Preparing upgrade' ) ,
percent : 0
} ) ;
this . buttons . start . prop ( 'disabled' , true ) ;
2025-10-16 17:31:57 -06:00
this . stopPolling ( ) ;
this . jobId = null ;
2025-10-16 10:59:50 -06:00
2025-10-16 15:13:34 -06:00
const body = { decisions : this . decisions } ;
2025-10-16 10:59:50 -06:00
request ( this . urls . start , { method : 'post' , body } , ( response ) => {
2025-10-16 15:13:34 -06:00
if ( ! this . active ) {
return ;
}
2025-10-16 10:59:50 -06:00
if ( response . status === 'error' ) {
this . stopPolling ( ) ;
this . renderProgress ( {
stage : 'error' ,
message : response . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) ,
percent : null
} ) ;
this . renderResult ( {
status : 'error' ,
message : response . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' )
} ) ;
return ;
}
const data = response . data || { } ;
if ( data . status === 'error' ) {
this . stopPolling ( ) ;
this . renderProgress ( {
stage : 'error' ,
message : data . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) ,
percent : null
} ) ;
this . renderResult ( data ) ;
return ;
}
2025-10-16 17:31:57 -06:00
if ( data . fallback ) {
this . renderResult ( data ) ;
this . stopPolling ( ) ;
this . renderProgress ( {
stage : data . status === 'success' ? 'complete' : 'error' ,
message : data . message || t ( 'SAFE_UPGRADE_STAGE_COMPLETE' , 'Upgrade complete' ) ,
percent : data . status === 'success' ? 100 : null ,
target _version : data . version || ( data . manifest && data . manifest . target _version ) || null ,
manifest : data . manifest || null
} ) ;
2025-10-16 18:12:09 -06:00
if ( data . status === 'success' ) {
setTimeout ( ( ) => window . location . reload ( ) , 2500 ) ;
}
2025-10-16 17:31:57 -06:00
return ;
}
2025-10-16 10:59:50 -06:00
2025-10-16 17:31:57 -06:00
if ( data . status === 'queued' && data . job _id ) {
this . jobId = data . job _id ;
if ( data . progress ) {
this . renderProgress ( data . progress ) ;
}
this . beginPolling ( 1200 ) ;
} else {
this . renderResult ( data ) ;
this . stopPolling ( ) ;
}
2025-10-16 10:59:50 -06:00
} ) ;
}
2025-10-16 13:49:53 -06:00
beginPolling ( delay = 1200 ) {
if ( this . isPolling ) {
return ;
}
this . isPolling = true ;
this . schedulePoll ( delay ) ;
}
schedulePoll ( delay = 1200 ) {
this . clearPollTimer ( ) ;
if ( ! this . isPolling ) {
return ;
}
2025-10-16 15:13:34 -06:00
this . pollTimer = setTimeout ( ( ) => this . fetchStatus ( true ) , delay ) ;
2025-10-16 13:49:53 -06:00
}
clearPollTimer ( ) {
if ( this . pollTimer ) {
clearTimeout ( this . pollTimer ) ;
this . pollTimer = null ;
}
}
2025-10-16 10:59:50 -06:00
fetchStatus ( silent = false ) {
2025-10-16 13:49:53 -06:00
if ( this . statusRequest ) {
return ;
}
this . pollTimer = null ;
let nextStage = null ;
2025-10-16 15:04:40 -06:00
let shouldContinue = true ;
2025-10-16 15:47:33 -06:00
console . debug ( '[SafeUpgrade] poll status' ) ;
2025-10-16 15:13:34 -06:00
2025-10-16 17:31:57 -06:00
const statusUrl = this . jobId ? ` ${ this . urls . status } ?job= ${ encodeURIComponent ( this . jobId ) } ` : this . urls . status ;
this . statusRequest = request ( statusUrl , ( response ) => {
2025-10-16 15:47:33 -06:00
console . debug ( '[SafeUpgrade] status response' , response ) ;
2025-10-16 15:13:34 -06:00
2025-10-16 15:47:33 -06:00
if ( response . status === 'error' ) {
2025-10-16 10:59:50 -06:00
if ( ! silent ) {
this . renderProgress ( {
stage : 'error' ,
2025-10-16 15:47:33 -06:00
message : response . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) ,
2025-10-16 10:59:50 -06:00
percent : null
} ) ;
}
2025-10-16 15:47:33 -06:00
nextStage = 'error' ;
return ;
}
2025-10-16 13:49:53 -06:00
2025-10-16 17:31:57 -06:00
const payload = response . data || { } ;
2025-10-16 18:12:09 -06:00
const job = payload . job || { } ;
2025-10-16 17:31:57 -06:00
const data = payload . progress || payload ;
2025-10-16 15:47:33 -06:00
nextStage = data . stage || null ;
2025-10-16 13:49:53 -06:00
2025-10-16 18:12:09 -06:00
this . renderProgress ( data , job ) ;
2025-10-16 17:31:57 -06:00
2025-10-16 18:12:09 -06:00
if ( job . status === 'error' ) {
shouldContinue = false ;
nextStage = 'error' ;
const message = job . error || data . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) ;
this . renderResult ( { status : 'error' , message } ) ;
} else if ( job . status === 'success' && data . stage === 'complete' ) {
shouldContinue = false ;
nextStage = 'complete' ;
if ( job . result ) {
this . renderResult ( job . result ) ;
}
} else if ( nextStage === 'installing' || nextStage === 'finalizing' || nextStage === 'complete' ) {
2025-10-16 15:47:33 -06:00
shouldContinue = false ;
}
} ) ;
const finalize = ( ) => {
this . statusRequest = null ;
if ( ! this . isPolling ) {
return ;
}
if ( nextStage === 'complete' || nextStage === 'error' ) {
this . stopPolling ( ) ;
2025-10-16 17:31:57 -06:00
this . jobId = null ;
2025-10-16 18:12:09 -06:00
if ( nextStage === 'complete' ) {
setTimeout ( ( ) => window . location . reload ( ) , 2500 ) ;
}
2025-10-16 15:47:33 -06:00
} else if ( shouldContinue ) {
this . schedulePoll ( ) ;
} else {
this . stopPolling ( ) ;
2025-10-16 17:31:57 -06:00
this . jobId = null ;
2025-10-16 15:47:33 -06:00
}
} ;
this . statusRequest . then ( finalize , finalize ) ;
2025-10-16 10:59:50 -06:00
}
2025-10-16 18:12:09 -06:00
renderProgress ( data , job = { } ) {
2025-10-16 10:59:50 -06:00
if ( ! data ) {
return ;
}
const stage = data . stage || 'initializing' ;
const titleResolver = STAGE _TITLES [ stage ] || STAGE _TITLES . initializing ;
const title = titleResolver ( ) ;
2025-10-16 18:12:09 -06:00
let percent = typeof data . percent === 'number' ? data . percent : null ;
const scaledPercent = ( ) => {
if ( stage === 'queued' ) { return 0 ; }
if ( stage === 'initializing' ) { return percent !== null ? Math . min ( percent , 5 ) : 5 ; }
if ( stage === 'downloading' ) {
if ( percent !== null ) {
return Math . min ( 60 , Math . round ( 10 + ( percent * 0.5 ) ) ) ;
}
return 25 ;
}
if ( stage === 'installing' ) { return percent !== null ? Math . max ( percent , 80 ) : 80 ; }
if ( stage === 'finalizing' ) { return percent !== null ? Math . max ( percent , 95 ) : 95 ; }
if ( stage === 'complete' ) { return 100 ; }
if ( stage === 'error' ) { return null ; }
return percent ;
} ;
percent = scaledPercent ( ) ;
2025-10-16 10:59:50 -06:00
const percentLabel = percent !== null ? ` ${ percent } % ` : '' ;
2025-10-16 18:12:09 -06:00
const statusLine = job && job . status ? ` <p class="safe-upgrade-status"> ${ t ( 'SAFE_UPGRADE_JOB_STATUS' , 'Status' ) } : <strong> ${ job . status . toUpperCase ( ) } </strong> ${ job . error ? ` — ${ job . error } ` : '' } </p> ` : '' ;
2025-10-16 10:59:50 -06:00
this . steps . progress . html ( `
< div class = "safe-upgrade-progress" >
< h3 > $ { title } < / h 3 >
< p > $ { data . message || '' } < / p >
2025-10-16 18:12:09 -06:00
$ { statusLine }
2025-10-16 10:59:50 -06:00
$ { percentLabel ? ` <div class="safe-upgrade-progress-bar"><span style="width: ${ percent } %"></span></div><div class="progress-value"> ${ percentLabel } </div> ` : '' }
< / d i v >
` );
this . switchStep ( 'progress' ) ;
if ( stage === 'complete' ) {
this . renderResult ( {
status : 'success' ,
manifest : data . manifest || null ,
version : data . target _version || null
} ) ;
} else if ( stage === 'error' ) {
this . renderResult ( {
status : 'error' ,
message : data . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' )
} ) ;
}
}
renderResult ( result ) {
const status = result . status || 'success' ;
if ( status === 'success' || status === 'finalized' ) {
const manifest = result . manifest || { } ;
const target = result . version || manifest . target _version || '' ;
const backup = manifest . backup _path || '' ;
const identifier = manifest . id || '' ;
this . steps . result . html ( `
< div class = "safe-upgrade-result success" >
< h3 > $ { r ( 'SAFE_UPGRADE_RESULT_SUCCESS' , target , 'Grav upgraded to v%s' ) } < / h 3 >
$ { identifier ? ` <p> ${ r ( 'SAFE_UPGRADE_RESULT_MANIFEST' , identifier , 'Snapshot reference: %s' ) } </p> ` : '' }
$ { backup ? ` <p> ${ r ( 'SAFE_UPGRADE_RESULT_ROLLBACK' , backup , 'Rollback snapshot stored at: %s' ) } </p> ` : '' }
< / d i v >
` );
this . switchStep ( 'result' ) ;
$ ( '[data-gpm-grav]' ) . remove ( ) ;
if ( target ) {
$ ( '#footer .grav-version' ) . html ( ` v ${ target } ` ) ;
}
if ( this . updates ) {
this . updates . fetch ( true ) ;
}
} else if ( status === 'noop' ) {
this . steps . result . html ( `
< div class = "safe-upgrade-result neutral" >
< h3 > $ { t ( 'SAFE_UPGRADE_RESULT_NOOP' , 'Grav is already up to date.' ) } < / h 3 >
< / d i v >
` );
this . switchStep ( 'result' ) ;
} else {
this . steps . result . html ( `
< div class = "safe-upgrade-result error" >
< h3 > $ { t ( 'SAFE_UPGRADE_RESULT_FAILURE' , 'Safe upgrade failed' ) } < / h 3 >
< p > $ { result . message || t ( 'SAFE_UPGRADE_GENERIC_ERROR' , 'Safe upgrade could not complete. See Grav logs for details.' ) } < / p >
< / d i v >
` );
this . switchStep ( 'result' ) ;
}
}
switchStep ( step ) {
Object . keys ( this . steps ) . forEach ( ( handle ) => {
this . steps [ handle ] . toggle ( handle === step ) ;
} ) ;
}
stopPolling ( ) {
2025-10-16 13:49:53 -06:00
this . isPolling = false ;
this . clearPollTimer ( ) ;
2025-10-16 10:59:50 -06:00
}
}
function basename ( path ) {
if ( ! path ) { return '' ; }
return path . split ( /[\\/]/ ) . pop ( ) ;
}