@@ -2320,6 +2320,31 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
23202320 </ span >
23212321 < hr >
23222322 </ div >
2323+ < div class ="container " id ="backup ">
2324+ < legend data-i18n ="tools.backup.title "> </ legend >
2325+ < p data-i18n ="tools.backup.desc "> </ p >
2326+ < span class ="container " style ="padding: 0 ">
2327+ < p style ="font-weight: bold " data-i18n ="tools.backup.export.title "> </ p >
2328+ < div class ="d-flex gap-2 ">
2329+ < input type ="checkbox " id ="backupIncludeWifi " value ="false ">
2330+ < label for ="backupIncludeWifi " data-i18n ="tools.backup.export.includeWifi "> </ label >
2331+ </ div > < br >
2332+ < button type ="button " class ="btn btn-primary " onclick ="exportBackup() "
2333+ data-i18n ="tools.backup.export.button "> </ button >
2334+ < br > < br >
2335+ </ span >
2336+ < span class ="container " style ="padding: 0 ">
2337+ < p style ="font-weight: bold " data-i18n ="tools.backup.import.title "> </ p >
2338+ < p data-i18n ="tools.backup.import.desc "> </ p >
2339+ < input type ="file " class ="form-control-file btn btn-primary " id ="backupUpload " accept =".json ">
2340+ < br > < br >
2341+ < div class ="text-center ">
2342+ < button type ="button " class ="btn btn-primary " onclick ="importBackup() "
2343+ data-i18n ="tools.backup.import.button "> </ button >
2344+ </ div >
2345+ </ span >
2346+ < hr >
2347+ </ div >
23232348 < div class ="container " id ="httpUpdate ">
23242349 < legend id ="legendfwupdate " data-i18n ="tools.fwupdate.title "> </ legend >
23252350 < form id ="firmwareUploadForm " method ="POST " enctype ="multipart/form-data " action ="/update ">
@@ -4354,6 +4379,123 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
43544379 }
43554380 return dDisplay + hDisplay + mDisplay ;
43564381 }
4382+ async function exportBackup ( ) {
4383+ const backup = {
4384+ espuinoBackup : 1 ,
4385+ created : new Date ( ) . toISOString ( ) ,
4386+ settings : await ( await fetch ( "http://" + host + "/settings" ) ) . json ( ) ,
4387+ rfid : await ( await fetch ( "http://" + host + "/rfid" ) ) . json ( )
4388+ } ;
4389+ delete backup . settings . ssids ; // names only - not usable for a restore
4390+ if ( document . getElementById ( 'backupIncludeWifi' ) . checked ) {
4391+ backup . wifiConfig = await ( await fetch ( "http://" + host + "/wificonfig" ) ) . json ( ) ;
4392+ backup . wifiNetworks = await ( await fetch ( "http://" + host + "/savedSSIDs?credentials=1" ) ) . json ( ) ;
4393+ }
4394+ const blob = new Blob ( [ JSON . stringify ( backup , null , "\t" ) ] , {
4395+ type : "application/json"
4396+ } ) ;
4397+ const anchor = document . createElement ( 'a' ) ;
4398+ anchor . href = URL . createObjectURL ( blob ) ;
4399+ anchor . download = 'espuino-backup_' + new Date ( ) . toISOString ( ) . substring ( 0 , 10 ) + '.json' ;
4400+ anchor . click ( ) ;
4401+ URL . revokeObjectURL ( anchor . href ) ;
4402+ }
4403+
4404+ async function importBackup ( ) {
4405+ const fileInput = document . getElementById ( 'backupUpload' ) ;
4406+ if ( ! fileInput . files . length ) {
4407+ alert ( i18next . t ( "files.upload.selectFile" ) ) ;
4408+ return ;
4409+ }
4410+ let backup ;
4411+ try {
4412+ backup = JSON . parse ( await fileInput . files [ 0 ] . text ( ) ) ;
4413+ } catch ( e ) {
4414+ toaster . error ( i18next . t ( "tools.backup.import.invalid" ) ) ;
4415+ return ;
4416+ }
4417+ if ( backup . espuinoBackup !== 1 ) {
4418+ toaster . error ( i18next . t ( "tools.backup.import.invalid" ) ) ;
4419+ return ;
4420+ }
4421+ let errors = 0 ;
4422+ // the firmware applies one settings section per request
4423+ const sections = [ 'general' , 'equalizer' , 'led' , 'battery' , 'playlist' , 'buttons' , 'rotary' , 'ftp' , 'mqtt' , 'bluetooth' ] ;
4424+ for ( const section of sections ) {
4425+ if ( backup . settings && backup . settings [ section ] ) {
4426+ const response = await fetch ( "http://" + host + "/settings" , {
4427+ method : "POST" ,
4428+ headers : {
4429+ "Content-Type" : "application/json"
4430+ } ,
4431+ body : JSON . stringify ( {
4432+ [ section ] : backup . settings [ section ]
4433+ } )
4434+ } ) ;
4435+ if ( ! response . ok ) {
4436+ errors ++ ;
4437+ }
4438+ }
4439+ }
4440+ if ( Array . isArray ( backup . rfid ) ) {
4441+ for ( const entry of backup . rfid ) {
4442+ if ( ! entry . id ) {
4443+ continue ;
4444+ }
4445+ const response = await fetch ( "http://" + host + "/rfid" , {
4446+ method : "POST" ,
4447+ headers : {
4448+ "Content-Type" : "application/json"
4449+ } ,
4450+ body : JSON . stringify ( entry )
4451+ } ) ;
4452+ if ( ! response . ok ) {
4453+ errors ++ ;
4454+ }
4455+ }
4456+ }
4457+ if ( Array . isArray ( backup . wifiNetworks ) ) {
4458+ for ( const network of backup . wifiNetworks ) {
4459+ if ( ! network . ssid ) {
4460+ continue ;
4461+ }
4462+ const response = await fetch ( "http://" + host + "/savedSSIDs" , {
4463+ method : "POST" ,
4464+ headers : {
4465+ "Content-Type" : "application/json"
4466+ } ,
4467+ body : JSON . stringify ( network )
4468+ } ) ;
4469+ if ( ! response . ok ) {
4470+ errors ++ ;
4471+ }
4472+ }
4473+ await rebuildSSIDList ( ) ;
4474+ }
4475+ if ( backup . wifiConfig ) {
4476+ const response = await fetch ( "http://" + host + "/wificonfig" , {
4477+ method : "POST" ,
4478+ headers : {
4479+ "Content-Type" : "application/json"
4480+ } ,
4481+ body : JSON . stringify ( backup . wifiConfig )
4482+ } ) ;
4483+ if ( ! response . ok ) {
4484+ errors ++ ;
4485+ }
4486+ }
4487+ if ( errors ) {
4488+ toaster . warning ( i18next . t ( "tools.backup.import.partial" , {
4489+ count : errors
4490+ } ) ) ;
4491+ } else {
4492+ toaster . success ( i18next . t ( "tools.backup.import.done" ) ) ;
4493+ }
4494+ // reload the imported settings into the UI
4495+ socket . send ( '{"settings":{"settings":"settings"}}' ) ;
4496+ rebuildRFIDList ( ) ;
4497+ }
4498+
43574499 async function cleanSd ( ) {
43584500 $ ( '#cleanSdModal' ) . modal ( 'hide' ) ;
43594501 toaster . info ( i18next . t ( "files.clean.running" ) ) ;
0 commit comments