2025-05-06 11:45:49 +02:00
< script setup >
import { ref , onMounted , useTemplateRef } from 'vue' ;
2025-10-06 20:25:46 +02:00
import { Notification , Button , SingleSelect , FormGroup , PasswordInput , TextInput , Checkbox } from '@cloudron/pankow' ;
2025-07-22 16:48:51 +02:00
import { copyToClipboard } from '@cloudron/pankow/utils' ;
2025-05-06 14:43:53 +02:00
import { REGIONS _CONTABO , REGIONS _VULTR , REGIONS _IONOS , REGIONS _OVH , REGIONS _LINODE , REGIONS _SCALEWAY , REGIONS _WASABI } from '../constants.js' ;
2025-11-26 11:38:06 +01:00
import { redirectIfNeeded , mountlike , s3like , parseFullBackupPath } from '../utils.js' ;
2025-05-06 11:45:49 +02:00
import ProvisionModel from '../models/ProvisionModel.js' ;
import BackupProviderForm from '../components/BackupProviderForm.vue' ;
2025-10-06 20:25:46 +02:00
import Whirlpool from '../components/Whirlpool.vue' ;
2025-10-07 20:57:03 +02:00
import AnimatedDots from '../components/AnimatedDots.vue' ;
2025-05-06 11:45:49 +02:00
const provisionModel = ProvisionModel . create ( ) ;
const ipProviders = [
{ name : 'Disabled' , value : 'noop' } ,
{ name : 'Public IP' , value : 'generic' } ,
{ name : 'Static IP Address' , value : 'fixed' } ,
{ name : 'Network Interface' , value : 'network-interface' }
] ;
const formError = ref ( { } ) ;
const busy = ref ( false ) ;
const ready = ref ( false ) ;
const waitingForRestore = ref ( false ) ;
const progressMessage = ref ( '' ) ;
const taskMinutesActive = ref ( 0 ) ;
const provider = ref ( '' ) ;
const providerConfig = ref ( { } ) ;
2025-11-26 11:38:06 +01:00
const fullPath = ref ( '' ) ;
2025-08-05 14:13:39 +02:00
const format = ref ( '' ) ;
2025-08-16 19:26:19 +02:00
const encrypted = ref ( false ) ;
const encryptionPasswordHint = ref ( '' ) ;
2025-08-05 14:13:39 +02:00
const encryptionPassword = ref ( '' ) ;
const encryptedFilenames = ref ( false ) ;
2025-05-06 11:45:49 +02:00
const showAdvanced = ref ( false ) ;
const ipv4Provider = ref ( 'generic' ) ;
const ipv4Address = ref ( '' ) ;
const ipv4Interface = ref ( '' ) ;
const ipv6Provider = ref ( 'generic' ) ;
const ipv6Address = ref ( '' ) ;
const ipv6Interface = ref ( '' ) ;
const skipDnsSetup = ref ( false ) ;
2025-10-06 20:03:38 +02:00
const siteId = ref ( '' ) ;
2025-05-06 11:45:49 +02:00
const form = useTemplateRef ( 'form' ) ;
const isFormValid = ref ( false ) ;
function checkValidity ( ) {
if ( ! provider . value ) return false ;
isFormValid . value = form . value . checkValidity ( ) ;
}
async function waitForRestore ( ) {
waitingForRestore . value = true ;
formError . value = { } ;
const [ error , result ] = await provisionModel . status ( ) ;
if ( error ) {
setTimeout ( waitForRestore , 5000 ) ;
return console . error ( error ) ;
}
if ( ! result . restore . active ) {
2025-10-07 20:42:14 +02:00
if ( result . adminFqdn && ! result . restore . errorMessage ) { // proceed to dashboard
2025-05-06 11:45:49 +02:00
window . location . href = 'https://' + result . adminFqdn ;
2025-10-07 20:42:14 +02:00
return ;
2025-05-06 11:45:49 +02:00
}
2025-10-07 20:42:14 +02:00
// restore reset or errored. start over
formError . value . dnsWait = true ;
formError . value . generic = result . restore . errorMessage ;
waitingForRestore . value = false ;
busy . value = false ;
2025-05-06 11:45:49 +02:00
return ;
}
progressMessage . value = result . restore . message ;
// TODO do we have the time here?
taskMinutesActive . value = ( new Date ( ) - new Date ( result . restore . startTime ) ) / 60000 ;
setTimeout ( waitForRestore , 5000 ) ;
}
async function onSubmit ( ) {
if ( ! isFormValid . value ) return ;
busy . value = true ;
formError . value = { } ;
2025-11-26 11:38:06 +01:00
if ( fullPath . value . indexOf ( '/' ) === - 1 ) {
2025-12-03 10:27:20 +01:00
formError . value . generic = 'Backup id must include the directory path' ;
formError . value . remotePath = true ;
2025-05-06 14:43:53 +02:00
busy . value = false ;
return ;
2025-05-06 11:45:49 +02:00
}
2025-11-26 11:38:06 +01:00
if ( fullPath . value . indexOf ( 'box' ) === - 1 ) {
2025-12-03 10:27:20 +01:00
formError . value . generic = 'Backup id must contain "box"' ;
formError . value . remotePath = true ;
2025-05-06 14:43:53 +02:00
busy . value = false ;
return ;
2025-05-06 11:45:49 +02:00
}
2025-11-26 11:38:06 +01:00
const version = fullPath . value . match ( /_v(\d+.\d+.\d+)/ ) ;
2025-05-06 11:45:49 +02:00
if ( ! version ) {
2025-05-06 14:43:53 +02:00
formError . value . generic = 'Backup id is missing version information' ;
formError . value . remotePath = true ;
busy . value = false ;
return ;
2025-05-06 11:45:49 +02:00
}
2025-11-26 11:38:06 +01:00
const config = { } ;
const { prefix , remotePath } = parseFullBackupPath ( fullPath . value ) ;
2025-08-05 14:13:39 +02:00
2025-05-06 14:43:53 +02:00
if ( s3like ( provider . value ) ) {
2025-08-05 14:13:39 +02:00
config . endpoint = providerConfig . value . endpoint ;
2025-11-26 11:38:06 +01:00
config . prefix = prefix ;
2025-08-05 14:13:39 +02:00
config . bucket = providerConfig . value . bucket ;
config . accessKeyId = providerConfig . value . accessKeyId ;
config . secretAccessKey = providerConfig . value . secretAccessKey ;
2025-05-06 14:43:53 +02:00
if ( provider . value === 's3' ) {
2025-08-05 14:13:39 +02:00
config . region = providerConfig . value . region || undefined ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'minio' || provider . value === 's3-v4-compat' ) {
2025-08-05 14:13:39 +02:00
config . region = providerConfig . value . region || 'us-east-1' ;
config . acceptSelfSignedCerts = providerConfig . value . acceptSelfSignedCerts ;
config . s3ForcePathStyle = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'exoscale-sos' ) {
2025-08-05 14:13:39 +02:00
config . region = 'us-east-1' ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'wasabi' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _WASABI . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'scaleway-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _SCALEWAY . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'linode-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _LINODE . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'ovh-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _OVH . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'ionos-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _IONOS . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'vultr-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _VULTR . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'contabo-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = REGIONS _CONTABO . find ( function ( x ) { return x . value === config . endpoint ; } ) . region ;
config . signatureVersion = 'v4' ;
config . s3ForcePathStyle = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'upcloud-objectstorage' ) { // the UI sets region and endpoint
2025-08-05 14:13:39 +02:00
const m = /^.*\.(.*)\.upcloudobjects.com$/ . exec ( config . endpoint ) ;
config . region = m ? m [ 1 ] : 'us-east-1' ; // let it fail in validation phase if m is not valid
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'digitalocean-spaces' ) {
2025-08-05 14:13:39 +02:00
config . region = 'us-east-1' ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'hetzner-objectstorage' ) {
2025-08-05 14:13:39 +02:00
config . region = 'us-east-1' ;
config . signatureVersion = 'v4' ;
2025-05-06 14:43:53 +02:00
}
} else if ( mountlike ( provider . value ) ) {
2025-11-26 11:38:06 +01:00
config . prefix = prefix ;
2025-08-05 14:13:39 +02:00
config . noHardlinks = ! providerConfig . value . useHardlinks ;
config . mountOptions = { } ;
2025-05-06 14:43:53 +02:00
2025-11-06 14:35:12 +01:00
if ( provider . value === 'sshfs' || provider . value === 'cifs' || provider . value === 'nfs' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . host = providerConfig . value . mountOptionHost ;
config . mountOptions . remoteDir = providerConfig . value . mountOptionRemoteDir ;
2025-05-06 14:43:53 +02:00
if ( provider . value === 'cifs' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . username = providerConfig . value . mountOptionUsername ;
config . mountOptions . password = providerConfig . value . mountOptionPassword ;
config . mountOptions . seal = ! ! providerConfig . value . mountOptionSeal ;
config . preserveAttributes = ! ! providerConfig . value . preserveAttributes ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'sshfs' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . user = providerConfig . value . mountOptionUser ;
config . mountOptions . port = parseInt ( providerConfig . value . mountOptionPort ) ;
config . mountOptions . privateKey = providerConfig . value . mountOptionPrivateKey ;
config . preserveAttributes = true ;
2025-05-06 14:43:53 +02:00
}
} else if ( provider . value === 'ext4' || provider . value === 'xfs' || provider . value === 'disk' ) {
2025-08-05 14:13:39 +02:00
config . mountOptions . diskPath = providerConfig . value . mountOptionDiskPath ;
config . preserveAttributes = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'mountpoint' ) {
2025-08-05 14:13:39 +02:00
config . mountPoint = providerConfig . value . mountPoint ;
config . chown = ! ! providerConfig . value . chown ;
config . preserveAttributes = ! ! providerConfig . value . preserveAttributes ;
2025-05-06 14:43:53 +02:00
}
} else if ( provider . value === 'filesystem' ) {
2025-11-26 11:38:06 +01:00
config . backupDir = prefix ;
2025-08-05 14:13:39 +02:00
config . noHardlinks = ! providerConfig . value . useHardlinks ;
config . preserveAttributes = true ;
2025-05-06 14:43:53 +02:00
} else if ( provider . value === 'gcs' ) {
2025-08-05 14:13:39 +02:00
config . bucket = providerConfig . value . bucket ;
2025-11-26 11:38:06 +01:00
config . prefix = prefix ;
2025-08-05 14:13:39 +02:00
config . projectId = providerConfig . value . projectId ;
config . credentials = providerConfig . value . credentials ;
2025-05-06 14:43:53 +02:00
}
2025-11-26 11:37:16 +01:00
const data = {
backupConfig : {
provider : provider . value ,
config ,
format : format . value ,
} ,
2025-11-26 11:38:06 +01:00
remotePath ,
2025-11-26 11:37:16 +01:00
version : version ? version [ 1 ] : '' ,
ipv4Config : {
provider : ipv4Provider . value ,
ip : ipv4Address . value ,
ifname : ipv4Interface . value ,
} ,
ipv6Config : {
provider : ipv6Provider . value ,
ip : ipv6Address . value ,
ifname : ipv6Interface . value ,
} ,
skipDnsSetup : skipDnsSetup . value ,
siteId : siteId . value
} ;
if ( encrypted . value ) {
data . backupConfig . encryptionPassword = encryptionPassword . value ;
data . backupConfig . encryptedFilenames = encryptedFilenames . value ;
}
2025-05-06 11:45:49 +02:00
const [ error ] = await provisionModel . restore ( data ) ;
if ( error ) {
if ( error . status === 424 ) {
formError . value . generic = error . body . message ;
if ( error . body . message . indexOf ( 'AWS Access Key Id' ) !== - 1 ) {
formError . value . accessKeyId = true ;
} else if ( error . body . message . indexOf ( 'not match the signature' ) !== - 1 ) {
formError . value . secretAccessKey = true ;
} else if ( error . body . message . toLowerCase ( ) === 'access denied' ) {
formError . value . bucket = true ;
} else if ( error . body . message . indexOf ( 'ECONNREFUSED' ) !== - 1 ) {
formError . value . generic = 'Unknown region' ;
formError . value . region = true ;
} else if ( error . body . message . toLowerCase ( ) === 'wrong region' ) {
formError . value . generic = 'Wrong S3 Region' ;
formError . value . region = true ;
} else {
formError . value . generic = error . body . message ;
}
} else {
formError . value . generic = error . body ? error . body . message : 'Internal error' ;
console . error ( error ) ;
}
busy . value = false ;
return ;
}
waitForRestore ( ) ;
}
function onBackupConfigChanged ( event ) {
const reader = new FileReader ( ) ;
reader . onload = function ( result ) {
if ( ! result . target || ! result . target . result ) return console . error ( 'Unable to read backup config' ) ;
2025-08-05 14:13:39 +02:00
let data ;
2025-05-06 11:45:49 +02:00
try {
2025-08-05 14:13:39 +02:00
data = JSON . parse ( result . target . result ) ; // 'provider', 'config', 'limits', 'format', 'remotePath', 'encrypted', 'encryptedFilenames'
if ( data . provider === 'filesystem' ) { // this allows a user to upload a backup to server and import easily with an absolute path
data . remotePath = ` ${ data . config . backupDir } / ${ data . remotePath } ` ;
2025-05-06 11:45:49 +02:00
}
} catch ( e ) {
console . error ( 'Unable to parse backup config' , e ) ;
return ;
}
2025-08-05 14:13:39 +02:00
provider . value = data . provider ;
2025-11-26 11:38:06 +01:00
fullPath . value = data . config . prefix ? ` ${ data . config . prefix } / ${ data . remotePath } ` : data . remotePath ;
2025-08-05 14:13:39 +02:00
format . value = data . format ;
2025-08-16 19:26:19 +02:00
encrypted . value = ! ! data . encrypted ;
encryptionPasswordHint . value = data . encryptionPasswordHint || '' ;
encryptionPassword . value = '' ;
2025-08-05 14:13:39 +02:00
encryptedFilenames . value = data . encryptedFilenames ;
2025-10-06 20:03:38 +02:00
siteId . value = data . siteId || '' ;
2025-11-26 14:10:54 +01:00
providerConfig . value = { } ;
for ( const [ key , value ] of Object . entries ( data . config ) ) {
if ( key === 'noHardlinks' || key === 'chown' || key === 'preserveAttributes' ) {
// not really used for restoring
} else if ( key === 'mountOptions' ) { // providerConfig uses a flattened format of config.mountOptions
providerConfig . value . mountOptionHost = data . config . mountOptions . host ;
providerConfig . value . mountOptionPort = data . config . mountOptions . port ;
providerConfig . value . mountOptionRemoteDir = data . config . mountOptions . remoteDir ;
providerConfig . value . mountOptionSeal = ! ! data . config . mountOptions . seal ;
providerConfig . value . mountOptionDiskPath = data . config . mountOptions . diskPath ;
providerConfig . value . mountOptionUser = data . config . mountOptions . user ;
providerConfig . value . mountOptionUsername = data . config . mountOptions . username ;
providerConfig . value . mountOptionPassword = data . config . mountOptions . password ;
providerConfig . value . mountOptionPrivateKey = '' ;
} else {
// s3: 'accessKeyId', 'secretAccessKey', 'bucket', 'prefix', 'signatureVersion', 'endpoint', 'region', 'acceptSelfSignedCerts', 's3ForcePathStyle'
// gcs: 'bucket', 'prefix'
providerConfig . value [ key ] = value ;
}
}
2025-11-26 16:19:34 +01:00
setTimeout ( checkValidity , 100 ) ; // update state of the confirm button
2025-05-06 11:45:49 +02:00
} ;
reader . readAsText ( event . target . files [ 0 ] ) ;
}
const backupConfigFileInput = useTemplateRef ( 'backupConfigFileInput' ) ;
function onUploadBackupConfig ( ) {
backupConfigFileInput . value . click ( ) ;
}
2025-07-22 18:03:40 +02:00
function onCopyToClipboard ( value ) {
copyToClipboard ( value ) ;
2025-10-14 09:12:13 +02:00
window . pankow . notify ( 'Copied' ) ;
2025-07-22 16:48:51 +02:00
}
2025-05-06 11:45:49 +02:00
onMounted ( async ( ) => {
2025-10-06 21:23:44 +02:00
const [ error , status ] = await provisionModel . status ( ) ;
2025-05-06 11:45:49 +02:00
if ( error ) return console . error ( error ) ;
2025-10-06 21:23:44 +02:00
if ( redirectIfNeeded ( status , 'restore' ) ) return ; // redirected to some other view...
2025-05-06 11:45:49 +02:00
2025-10-06 21:23:44 +02:00
const [ error2 , result ] = await provisionModel . detectIp ( ) ;
if ( error2 ) return console . error ( error2 ) ;
2025-05-06 14:43:53 +02:00
ipv4Provider . value = result . ipv4 ? 'generic' : 'noop' ;
ipv6Provider . value = result . ipv6 ? 'generic' : 'noop' ;
2025-05-06 11:45:49 +02:00
ready . value = true ;
2025-10-06 21:23:44 +02:00
if ( status . restore . active ) return waitForRestore ( ) ;
formError . value . generic = status . restore . errorMessage ; // show any previous error
2025-05-06 11:45:49 +02:00
} ) ;
< / script >
< template >
< div class = "container" v-if = "ready" >
2025-07-22 16:48:51 +02:00
< Notification / >
2025-10-06 20:25:46 +02:00
< Transition name = "fade-scale" mode = "out-in" >
< div class = "view" v-if = "waitingForRestore" style="max-width: unset; height: 100%;" >
< div style = "display: flex; flex-direction: column; height: 100%; align-items: center;" >
2025-10-06 21:23:44 +02:00
< Whirlpool / >
2025-10-07 20:57:03 +02:00
< h1 > Please wait while Cloudron is restoring < AnimatedDots / > < / h1 >
2025-12-01 20:49:59 +01:00
< h4 class = "progress-message" > { { progressMessage } } < / h4 >
2025-10-06 20:25:46 +02:00
< small > You can follow the logs on the server at < code @click ="onCopyToClipboard('/home/yellowtent/platformdata/logs/box.log')" > / home / yellowtent / platformdata / logs / box.log < / code > < / small >
< p v-show = "taskMinutesActive >= 4">If restore appears stuck, it can be restarted by running <code @click="onCopyToClipboard('systemctl restart box')" > systemctl restart box < / code > and reloading this page. < / p >
2025-05-06 11:45:49 +02:00
< / div >
< / div >
2025-07-22 16:48:51 +02:00
< div class = "view" v-else style = "max-width: 500px;" >
2025-09-09 15:55:01 +02:00
< h1 style = "text-align: center" > Cloudron Restore < / h1 >
2025-09-09 16:14:20 +02:00
< p > Provide the backup information to restore from , or
2025-07-22 18:03:40 +02:00
< input type = "file" ref = "backupConfigFileInput" @change ="onBackupConfigChanged" accept = "application/json, text/json" style = "display:none" / >
2025-09-09 16:14:20 +02:00
< button type = "button" style = "background: none; border: none; color: #007bff; cursor: pointer; text-decoration: underline; padding: 0;" @click ="onUploadBackupConfig()" >
upload a Backup Config
< / button >
< / p >
2025-05-06 11:45:49 +02:00
< form ref = "form" @submit.prevent ="onSubmit()" @input ="checkValidity()" >
< fieldset :disabled = "busy" >
< input type = "submit" style = "display: none;" / >
2025-10-07 20:44:05 +02:00
<!-- when provisioning = true BackupProviderForm won 't display the error since we want to show the error above remotePath -->
<div class="error-label" v-show="formError.generic">{{ formError.generic }}</div>
2025-05-06 11:45:49 +02:00
<!-- remotePath contains the prefix as well -->
<FormGroup>
<label for="inputRemotePath">{{ $t(' app . importBackupDialog . remotePath ') }} <sup><a href="https://docs.cloudron.io/backups/#import-app-backup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
2025-11-26 11:38:06 +01:00
<TextInput id="inputRemotePath" v-model="fullPath" required />
2025-05-06 11:45:49 +02:00
</FormGroup>
2025-08-05 14:13:39 +02:00
<BackupProviderForm ref="form"
v-model:provider="provider"
v-model:provider-config="providerConfig"
v-model:format="format"
:form-error="formError"
:provisioning="true"
:import-only="true" />
2025-05-06 11:45:49 +02:00
2025-10-07 19:26:17 +02:00
<Checkbox v-model="encrypted" :label="$t(' backups . configureBackupStorage . usesEncryption ')"/>
<FormGroup v-if="encrypted">
<label for="encryptionPassswordInput">{{ $t(' backups . configureBackupStorage . encryptionPassword ') }}</label>
<PasswordInput id="encryptionPassswordInput" v-model="encryptionPassword" :placeholder="$t(' backups . configureBackupStorage . encryptionPasswordPlaceholder ')" required/>
<div class="warning-label" v-if="encryptionPasswordHint">{{ $t(' backups . configureBackupStorage . encryptionHint ') }}: {{ encryptionPasswordHint }}</div>
</FormGroup>
<Checkbox v-if="encrypted && format === ' rsync '" v-model="encryptedFilenames" :label="$t(' backups . configureBackupStorage . encryptFilenames ')"/>
2025-08-16 19:26:19 +02:00
2025-10-07 19:26:17 +02:00
<div v-show="showAdvanced">
2025-05-06 11:45:49 +02:00
<!-- IPv4 provider -->
<FormGroup>
<label class="control-label">IPv4 Configuration <sup><a href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<SingleSelect v-model="ipv4Provider" :options="ipProviders" option-key="value" option-label="name" />
</FormGroup>
<!-- IPv4 Fixed -->
<FormGroup v-if="ipv4Provider === ' fixed '">
<label for="ipv4AddressInput">IPv4 Address</label>
<TextInput id="ipv4AddressInput" v-model="ipv4Address" required />
</FormGroup>
<!-- IPv4 Network Interface -->
<FormGroup v-if="ipv4Provider === ' network - interface '">
<label for="ipv4InterfaceInput">IPv4 Interface Name</label>
<TextInput id="ipv4InterfaceInput" v-model="ipv4Interface" required />
</FormGroup>
<!-- IPv6 provider -->
<FormGroup>
<label class="control-label">IPv6 Configuration <sup><a href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<SingleSelect v-model="ipv6Provider" :options="ipProviders" option-key="value" option-label="name" />
</FormGroup>
<!-- IPv6 Fixed -->
<FormGroup v-if="ipv6Provider === ' fixed '">
<label for="ipv6AddressInput">IPv6 Address</label>
<TextInput id="ipv6AddressInput" v-model="ipv6Address" required />
</FormGroup>
<!-- IPv6 Network Interface -->
<FormGroup v-if="ipv6Provider === ' network - interface '">
<label for="ipv6InterfaceInpt">IPv6 Interface Name</label>
<TextInput id="ipv6InterfaceInpt" v-model="ipv6Interface" required />
</FormGroup>
</div>
2025-10-07 19:26:17 +02:00
<div class="actionable advanced" @click="showAdvanced = false" v-if="showAdvanced">Hide Advanced settings</div>
<div class="actionable advanced" @click="showAdvanced = true" v-else>Advanced settings...</div>
2025-05-06 11:45:49 +02:00
2025-10-07 19:26:17 +02:00
<div class="dry-run">
2025-05-06 11:45:49 +02:00
<Checkbox v-model="skipDnsSetup" label="Dry run"/>
2025-09-09 16:14:20 +02:00
<small class="helper-text">
2025-05-06 11:45:49 +02:00
When enabled, apps are restored but the DNS records are not updated to point to this server.
To access the dashboard, this browser' s host must have an entry in < code > / etc / hosts < / code > for the dashboard domain to this server ' s IP .
See the < a href = "https://docs.cloudron.io/backups/#dry-run" target = "_blank" > docs < / a > for more information .
< / small >
< / div >
< / fieldset >
< / form >
2025-09-09 15:55:01 +02:00
< div class = "actions" >
2025-07-22 18:03:40 +02:00
< Button @click ="onSubmit()" : disabled = "busy || !isFormValid" :loading = "busy" > Restore < / Button >
2025-09-09 15:55:01 +02:00
< a class = "setup" href = "/setup.html" > Looking to setup ? < / a >
2025-07-22 18:03:40 +02:00
< / div >
2025-05-06 11:45:49 +02:00
< / div >
< / Transition >
< / div >
< / template >
2025-09-09 15:55:01 +02:00
< style scoped >
. actions {
margin - top : 1.5 em ;
display : flex ;
flex - direction : column ;
align - items : center ;
}
. actions . setup {
margin - top : 1 em ;
font - size : 0.9 em ;
}
2025-10-07 19:26:17 +02:00
. advanced {
margin - top : 20 px ;
}
. dry - run {
margin - top : 20 px ;
}
2025-12-01 20:49:59 +01:00
. progress - message {
margin - top : 0 ;
max - width : 90 vw ;
overflow : hidden ;
text - overflow : ellipsis ;
}
2025-09-09 15:55:01 +02:00
< / style >