@@ -0,0 +1,908 @@
<!DOCTYPE html>
< html lang = "hu" class = "no-js" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Visszaállítás a mentésből — Felhom.eu< / title >
< link rel = "icon" type = "image/svg+xml" href = "/static/favicon.svg?v=0.247.0" >
< link rel = "stylesheet" href = "/static/style.css?v=ASSET" >
< meta name = "csrf-token" content = "tok" >
< script >
document . documentElement . className = document . documentElement . className . replace ( 'no-js' , 'js' ) ;
function csrfHeaders ( ) { var el = document . querySelector ( 'meta[name="csrf-token"]' ) ; return el ? { 'X-CSRF-Token' : el . content } : { } ; }
var fhEN = document . documentElement . lang === 'en' ;
function fhNum ( v , prec ) { var s = Number ( v ) . toFixed ( prec ) ; return fhEN ? s : s . replace ( '.' , ',' ) ; }
function fhIsoDate ( iso ) { return fhEN ? iso : iso . replace ( /^(\d{4})-(\d{2})-(\d{2})/ , '$1. $2. $3.' ) ; }
function fhDateTime ( d ) { return fhIsoDate ( d . toLocaleString ( 'sv-SE' , { timeZone : 'Europe/Budapest' , year : 'numeric' , month : '2-digit' , day : '2-digit' , hour : '2-digit' , minute : '2-digit' } ) ) ; }
function fhMonthDay ( d ) { var s = d . toLocaleDateString ( 'sv-SE' , { timeZone : 'Europe/Budapest' , month : '2-digit' , day : '2-digit' } ) ; return fhEN ? s : s . replace ( /^(\d{2})-(\d{2})$/ , '$1. $2.' ) ; }
function felhomConfirm ( el , question , onYes ) {
if ( ! el || el . dataset . fcOpen ) return ;
el . dataset . fcOpen = '1' ;
var wrap = document . createElement ( 'span' ) ; wrap . className = 'inline-confirm' ;
var q = document . createElement ( 'span' ) ; q . className = 'inline-confirm-q' ; q . textContent = question ;
var yes = document . createElement ( 'button' ) ; yes . type = 'button' ; yes . className = 'btn btn-xs btn-danger' ; yes . textContent = 'Igen' ;
var no = document . createElement ( 'button' ) ; no . type = 'button' ; no . className = 'btn btn-xs btn-outline' ; no . textContent = 'Mégse' ;
wrap . appendChild ( q ) ; wrap . appendChild ( yes ) ; wrap . appendChild ( no ) ;
el . style . display = 'none' ; el . parentNode . insertBefore ( wrap , el . nextSibling ) ;
function close ( ) { wrap . remove ( ) ; el . style . display = '' ; delete el . dataset . fcOpen ; }
no . addEventListener ( 'click' , close ) ;
yes . addEventListener ( 'click' , function ( ) { close ( ) ; onYes ( ) ; } ) ;
}
document . addEventListener ( 'click' , function ( e ) {
var btn = e . target . closest ? e . target . closest ( '[data-confirm]' ) : null ;
if ( ! btn ) return ;
e . preventDefault ( ) ;
felhomConfirm ( btn , btn . getAttribute ( 'data-confirm' ) , function ( ) {
var form = btn . closest ( 'form' ) ;
if ( form ) { if ( form . requestSubmit ) form . requestSubmit ( btn ) ; else form . submit ( ) ; }
} ) ;
} ) ;
document . addEventListener ( 'click' , function ( e ) {
var btn = e . target . closest ? e . target . closest ( '.nav-group-toggle' ) : null ;
if ( ! btn ) return ;
var group = btn . closest ( '.nav-group' ) ;
if ( ! group ) return ;
var willOpen = ! group . classList . contains ( 'is-open' ) ;
document . querySelectorAll ( '.nav-group.is-open' ) . forEach ( function ( g ) {
g . classList . remove ( 'is-open' ) ;
var t = g . querySelector ( '.nav-group-toggle' ) ;
if ( t ) t . setAttribute ( 'aria-expanded' , 'false' ) ;
} ) ;
if ( willOpen ) { group . classList . add ( 'is-open' ) ; btn . setAttribute ( 'aria-expanded' , 'true' ) ; }
} ) ;
function showAlert ( msg ) { var o = document . createElement ( 'div' ) ; o . className = 'modal-overlay' ; o . id = 'alert-modal' ; o . addEventListener ( 'click' , function ( e ) { if ( e . target === o ) o . remove ( ) ; } ) ; var c = document . createElement ( 'div' ) ; c . className = 'modal-card' ; c . innerHTML = '<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">' + msg . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) + '</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>' ; o . appendChild ( c ) ; document . body . appendChild ( o ) ; }
< / script >
< / head >
< body >
< svg xmlns = "http://www.w3.org/2000/svg" style = "display:none" aria-hidden = "true" >
< symbol id = "i-hard-drive" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M10 16h.01" / > < path d = "M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" / > < path d = "M21.946 12.013H2.054" / > < path d = "M6 16h.01" / > < / symbol >
< symbol id = "i-cpu" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 20v2" / > < path d = "M12 2v2" / > < path d = "M17 20v2" / > < path d = "M17 2v2" / > < path d = "M2 12h2" / > < path d = "M2 17h2" / > < path d = "M2 7h2" / > < path d = "M20 12h2" / > < path d = "M20 17h2" / > < path d = "M20 7h2" / > < path d = "M7 20v2" / > < path d = "M7 2v2" / > < rect x = "4" y = "4" width = "16" height = "16" rx = "2" / > < rect x = "8" y = "8" width = "8" height = "8" rx = "1" / > < / symbol >
< symbol id = "i-memory-stick" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 12v-2" / > < path d = "M12 18v-2" / > < path d = "M16 12v-2" / > < path d = "M16 18v-2" / > < path d = "M2 11h1.5" / > < path d = "M20 18v-2" / > < path d = "M20.5 11H22" / > < path d = "M4 18v-2" / > < path d = "M8 12v-2" / > < path d = "M8 18v-2" / > < rect x = "2" y = "6" width = "20" height = "10" rx = "2" / > < / symbol >
< symbol id = "i-thermometer" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" / > < / symbol >
< symbol id = "i-triangle-alert" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" / > < path d = "M12 9v4" / > < path d = "M12 17h.01" / > < / symbol >
< symbol id = "i-lock" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect width = "18" height = "11" x = "3" y = "11" rx = "2" ry = "2" / > < path d = "M7 11V7a5 5 0 0 1 10 0v4" / > < / symbol >
< symbol id = "i-cloud" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" / > < / symbol >
< symbol id = "i-square" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect width = "18" height = "18" x = "3" y = "3" rx = "2" / > < / symbol >
< symbol id = "i-rotate-cw" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" / > < path d = "M21 3v5h-5" / > < / symbol >
< symbol id = "i-file-text" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" / > < path d = "M14 2v5a1 1 0 0 0 1 1h5" / > < path d = "M10 9H8" / > < path d = "M16 13H8" / > < path d = "M16 17H8" / > < / symbol >
< symbol id = "i-external-link" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M15 3h6v6" / > < path d = "M10 14 21 3" / > < path d = "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" / > < / symbol >
< symbol id = "i-shield" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" / > < / symbol >
< symbol id = "i-server" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect width = "20" height = "8" x = "2" y = "2" rx = "2" ry = "2" / > < rect width = "20" height = "8" x = "2" y = "14" rx = "2" ry = "2" / > < line x1 = "6" x2 = "6.01" y1 = "6" y2 = "6" / > < line x1 = "6" x2 = "6.01" y1 = "18" y2 = "18" / > < / symbol >
< symbol id = "i-share" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "18" cy = "5" r = "3" / > < circle cx = "6" cy = "12" r = "3" / > < circle cx = "18" cy = "19" r = "3" / > < line x1 = "8.59" x2 = "15.42" y1 = "13.51" y2 = "17.49" / > < line x1 = "15.41" x2 = "8.59" y1 = "6.51" y2 = "10.49" / > < / symbol >
< symbol id = "i-layout-grid" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect width = "7" height = "7" x = "3" y = "3" rx = "1" / > < rect width = "7" height = "7" x = "14" y = "3" rx = "1" / > < rect width = "7" height = "7" x = "14" y = "14" rx = "1" / > < rect width = "7" height = "7" x = "3" y = "14" rx = "1" / > < / symbol >
< symbol id = "i-rocket" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" / > < path d = "m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" / > < path d = "M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" / > < path d = "M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" / > < / symbol >
< symbol id = "i-settings" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" / > < circle cx = "12" cy = "12" r = "3" / > < / symbol >
< symbol id = "i-bell" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M10.268 21a2 2 0 0 0 3.464 0" / > < path d = "M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" / > < / symbol >
< symbol id = "i-x" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M18 6 6 18" / > < path d = "m6 6 12 12" / > < / symbol >
< symbol id = "i-check" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M20 6 9 17l-5-5" / > < / symbol >
< symbol id = "i-pencil" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" / > < path d = "m15 5 4 4" / > < / symbol >
< symbol id = "i-info" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 16v-4" / > < path d = "M12 8h.01" / > < / symbol >
< symbol id = "i-download" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 15V3" / > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < path d = "m7 10 5 5 5-5" / > < / symbol >
< symbol id = "i-upload" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M12 3v12" / > < path d = "m17 8-5-5-5 5" / > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < / symbol >
< symbol id = "i-trash-2" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M10 11v6" / > < path d = "M14 11v6" / > < path d = "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" / > < path d = "M3 6h18" / > < path d = "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" / > < / symbol >
< symbol id = "i-wrench" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" / > < / symbol >
< symbol id = "i-link" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" / > < path d = "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" / > < / symbol >
< symbol id = "i-search" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "m21 21-4.34-4.34" / > < circle cx = "11" cy = "11" r = "8" / > < / symbol >
< symbol id = "i-plus" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M5 12h14" / > < path d = "M12 5v14" / > < / symbol >
< symbol id = "i-chevron-down" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "m6 9 6 6 6-6" / > < / symbol >
< symbol id = "i-play" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" / > < / symbol >
< symbol id = "i-pause" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < rect x = "14" y = "3" width = "5" height = "18" rx = "1" / > < rect x = "5" y = "3" width = "5" height = "18" rx = "1" / > < / symbol >
< symbol id = "i-menu" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M4 12h16" / > < path d = "M4 6h16" / > < path d = "M4 18h16" / > < / symbol >
< / svg >
< header class = "mobile-topbar" >
< a href = "/" class = "mobile-topbar-logo" > < img src = "/static/felhom-logo.svg?v=0.247.0" alt = "Felhom.eu" > < / a >
< button type = "button" class = "nav-burger" aria-expanded = "false" aria-controls = "sidebar" aria-label = "Menü" >
< svg class = "ico" > < use href = "#i-menu" / > < / svg >
< / button >
< / header >
< div class = "nav-backdrop" hidden > < / div >
< nav class = "sidebar" id = "sidebar" >
< div class = "sidebar-header" >
< img src = "/static/felhom-logo.svg?v=0.247.0" alt = "Felhom.eu" class = "sidebar-logo" >
< / div >
< ul class = "nav-links" >
< li > < a href = "/launcher" class = "" > < svg class = "ico" > < use href = "#i-rocket" / > < / svg > Indítópult< / a > < / li >
< li > < a href = "/dashboard" class = "" > < svg class = "ico" > < use href = "#i-layout-grid" / > < / svg > Vezérlőpult< / a > < / li >
< li > < a href = "/stacks" class = "" > < svg class = "ico" > < use href = "#i-cloud" / > < / svg > Alkalmazások< / a > < / li >
< li class = "nav-group" >
< button type = "button" class = "nav-group-toggle" aria-expanded = "false" aria-controls = "nav-group-storage" > < svg class = "ico" > < use href = "#i-hard-drive" / > < / svg > Tárhely< svg class = "ico nav-chevron" > < use href = "#i-chevron-down" / > < / svg > < / button >
< ul id = "nav-group-storage" class = "nav-links nav-links-sub nav-links-nested" >
< li > < a href = "/storage" class = "" > Meghajtók< / a > < / li >
< li > < a href = "/storage/network" class = "" > Hálózati tárhely< / a > < / li >
< / ul >
< / li >
< li class = "nav-group is-open" >
< button type = "button" class = "nav-group-toggle active" aria-expanded = "true" aria-controls = "nav-group-backups" > < svg class = "ico" > < use href = "#i-shield" / > < / svg > Biztonsági mentés< svg class = "ico nav-chevron" > < use href = "#i-chevron-down" / > < / svg > < / button >
< ul id = "nav-group-backups" class = "nav-links nav-links-sub nav-links-nested" >
< li > < a href = "/backups" class = "" > Áttekintés< / a > < / li >
< li > < a href = "/backups/remote" class = "" > Távoli mentés< / a > < / li >
< li > < a href = "/backups/apps" class = "" > Alkalmazások< / a > < / li >
< li > < a href = "/backups/restore" class = "active" > Visszaállítás< / a > < / li >
< / ul >
< / li >
< li class = "nav-group" >
< button type = "button" class = "nav-group-toggle" aria-expanded = "false" aria-controls = "nav-group-sharing" > < svg class = "ico" > < use href = "#i-share" / > < / svg > Megosztás< svg class = "ico nav-chevron" > < use href = "#i-chevron-down" / > < / svg > < / button >
< ul id = "nav-group-sharing" class = "nav-links nav-links-sub nav-links-nested" >
< li > < a href = "/sharing" class = "" > Hálózati megosztás< / a > < / li >
< / ul >
< / li >
< li > < a href = "/monitoring" class = "" > < svg class = "ico" > < use href = "#i-cpu" / > < / svg > Rendszermonitor< / a > < / li >
< li > < a href = "/debug" class = "" > < svg class = "ico" > < use href = "#i-wrench" / > < / svg > Hibakeresés< / a > < / li >
< / ul >
< div class = "sidebar-bottom" >
< div class = "nav-group-label" > Beállítások< / div >
< ul class = "nav-links nav-links-sub" >
< li > < a href = "/settings" class = "" > < svg class = "ico" > < use href = "#i-settings" / > < / svg > Rendszer< / a > < / li >
< li > < a href = "/settings/notifications" class = "" > < svg class = "ico" > < use href = "#i-bell" / > < / svg > Értesítések< / a > < / li >
< li > < a href = "/settings/security" class = "" > < svg class = "ico" > < use href = "#i-lock" / > < / svg > Biztonság és hozzáférés< / a > < / li >
< / ul >
< div class = "sidebar-footer" >
< span class = "version" > 0.247.0< / span > < details class = "lang-globe" >
< summary class = "lang-globe-btn" aria-label = "Nyelv" title = "Nyelv" > < svg class = "ico" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" aria-hidden = "true" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M2 12h20" / > < path d = "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" / > < / svg > < / summary >
< ul class = "lang-globe-menu" >
< li > < form method = "POST" action = "/settings/language" > < input type = "hidden" name = "_csrf" value = "CSRF" > < input type = "hidden" name = "back" value = "/i18n-fixture" > < button type = "submit" name = "lang" value = "hu" lang = "hu" class = "lang-globe-item current" aria-current = "true" > Magyar< / button > < / form > < / li >
< li > < form method = "POST" action = "/settings/language" > < input type = "hidden" name = "_csrf" value = "CSRF" > < input type = "hidden" name = "back" value = "/i18n-fixture" > < button type = "submit" name = "lang" value = "en" lang = "en" class = "lang-globe-item" > English< / button > < / form > < / li >
< / ul >
< / details >
< form method = "post" action = "/logout" class = "logout-form" > < button type = "submit" class = "logout-link" > Kijelentkezés ↗< / button > < / form >
< / div >
< / div >
< / nav >
< main class = "content" >
< div class = "page-header" >
< h2 > Biztonsági mentés — Visszaállítás< / h2 >
< span class = "domain-badge" > example.hu< / span >
< / div >
< div id = "restore-banner" class = "flash" style = "display:none" > < / div >
< div class = "backup-section-card" >
< h3 > Visszaállítás< / h3 >
< div class = "restore-section" >
< div class = "restore-form-row" >
< label class = "restore-label" > Alkalmazás:< / label >
< select id = "restore-app" class = "restore-select" onchange = "onRestoreAppChange()" >
< option value = "" > — Válassz —< / option >
< option value = "privatebin" data-has-hdd = "" data-has-db = "" data-has-volumes = "" > < / option >
< / select >
< / div >
< div class = "restore-form-row" >
< label class = "restore-label" > Pillanatkép:< / label >
< select id = "restore-snapshot" class = "restore-select" onchange = "onRestoreConfirmChange()" >
< option value = "" > — Válassz alkalmazást —< / option >
< / select >
< / div >
< div id = "restore-type-info" class = "restore-info" style = "display:none;margin-bottom:0.5rem" >
< / div >
< div id = "restore-no-snapshots" class = "restore-warning" style = "display:none;" >
Még nincs mentés felhasználói adattal.
< / div >
< div class = "restore-warning" >
< svg class = "ico ico-sm" > < use href = "#i-triangle-alert" / > < / svg > A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
Az alkalmazás a folyamat során automatikusan leáll és újraindul.
< / div >
< div class = "restore-confirm" >
< label >
< input type = "checkbox" id = "restore-confirm-cb" onchange = "onRestoreConfirmChange()" >
Megértettem, visszaállítás indítása.
< / label >
< / div >
< div class = "restore-actions" >
< button type = "button" class = "btn btn-sm btn-danger" id = "restore-btn" disabled onclick = "submitRestore()" > Visszaállítás indítása< / button >
< / div >
< div style = "margin-top: 1rem; text-align: center; border-top: 1px solid var(--line); padding-top: 1rem;" >
< a href = "/import" class = "btn btn-sm btn-outline" > Importálás mentett csomagból (.fab)< / a >
< / div >
< / div >
< / div >
< div class = "backup-section-card" >
< h3 > Visszaállítás a távoli tárolóból< / h3 >
< p class = "form-hint" style = "margin:-0.25rem 0 1rem" > Válaszd ki az alkalmazást, és a következő oldalon döntsd el, mit szeretnél: ellenőrzést külön mappába, csak a hiányzó fájlok visszahozását, vagy teljes visszaállítást. Egyik sem indul el kérdés nélkül.< / p >
< p class = "form-hint" > A távoli tároló üres — nincs mit visszaállítani.< / p >
< div class = "backup-tier-divider" style = "margin-top:1.5rem" > < / div >
< h3 style = "margin-bottom:.5rem" > Megőrzött másolatok< / h3 >
< p class = "form-hint" style = "margin-bottom:.75rem" > Ezek a második meghajtón lévő másolatok olyan mappákhoz tartoznak, amelyek most nem érhetők el: a meghajtó nincs csatlakoztatva, már nincs a gépen, vagy a mappa hiányzik. Megőrizzük őket, amíg te el nem távolítod őket.< / p >
< div class = "app-row-list" >
< div class = "app-row" >
< img class = "app-row-icon" src = "/static/assets/-logo.svg" alt = "" data-fallback = "/static/app-placeholder.svg"
onerror = "if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/-logo.png';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}" >
< div class = "app-row-text" >
< span class = "app-row-name" > csaladi_fotok< / span >
< span class = "app-row-secondary" > hdd_3 · 1,2 GB · 2026. 10. 21. 14:00< / span >
< / div >
< div class = "app-row-actions" >
< form method = "POST" action = "/backup/kept/remove" style = "display:inline" >
< input type = "hidden" name = "id" value = "0123456789abcdef" >
< input type = "hidden" name = "confirm" value = "1" >
< button type = "button" class = "btn btn-xs btn-danger-outline"
onclick = "confirmRemoveKeptCopy(this)" > Eltávolítás< / button >
< / form >
< / div >
< / div >
< / div >
< script >
function confirmRemoveKeptCopy ( btn ) {
felhomConfirm ( btn , 'Biztosan eltávolítod ezt a megőrzött másolatot? A második meghajtóról véglegesen törlődik.' , function ( ) {
var f = btn . closest ( 'form' ) ;
if ( f ) { if ( f . requestSubmit ) f . requestSubmit ( ) ; else f . submit ( ) ; }
} ) ;
}
< / script >
< div class = "backup-tier-divider" style = "margin-top:1.5rem" > < / div >
< h3 style = "margin-bottom:.5rem" > Meglévő ellenőrző másolatok< / h3 >
< p class = "form-hint" > Nincs ellenőrző másolat a meghajtón.< / p >
< / div >
< div class = "backup-section-card" >
< h3 > Hordozható mentéscsomag (.fab)< / h3 >
< p class = "form-hint" style = "margin:-0.25rem 0 1rem" > Hordozható pillanatfelvétel — bárhol tárolhatod, és bármikor visszatöltheted egy meghajtóról. A folyamatos védelmet az 1– 3. szintű mentés adja.< / p >
< p class = "form-hint" > Nincs telepített alkalmazás.< / p >
< / div >
< script >
( function ( ) {
var banner = document . getElementById ( 'restore-banner' ) ;
if ( ! banner ) return ;
var sawRunning = false ;
function opLabel ( op ) { return op === 'tier2-restore' ? 'Fájl-visszaállítás'
: op === 'offbox-restore' ? 'Távoli visszaállítás' : 'Visszaállítás' ; }
function render ( st ) {
if ( st . running ) {
sawRunning = true ;
banner . className = 'flash' ;
banner . style . display = 'block' ;
banner . textContent = opLabel ( st . op ) + ' folyamatban' + ( st . stack ? ': ' + st . stack : '' ) + '…' ;
return ;
}
if ( st . last && ( sawRunning || st . last _recent ) ) {
banner . style . display = 'block' ;
if ( st . last . ok ) {
banner . className = 'flash flash-success' ;
banner . textContent = st . last . message || ( opLabel ( st . last . op ) + ' kész.' ) ;
} else {
banner . className = 'flash flash-error' ;
banner . textContent = st . last . message || ( opLabel ( st . last . op ) + ' sikertelen.' ) ;
}
}
}
function poll ( ) {
fetch ( '/api/backup/restore-status' , { headers : { 'Accept' : 'application/json' } } )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( j ) { if ( j && j . data ) render ( j . data ) ; } )
. catch ( function ( ) { } ) ;
}
poll ( ) ;
setInterval ( poll , 3000 ) ;
} ) ( ) ;
var fabQueue = [ ] ;
function fabSetStatus ( msg ) { document . getElementById ( 'fab-dl-status' ) . textContent = msg ; }
function fabPassword ( ) { var el = document . getElementById ( 'fab-dl-password' ) ; return el ? el . value : '' ; }
function fabDownload ( btn ) {
var stack = btn . getAttribute ( 'data-stack' ) ;
fetch ( '/api/export/download/estimate?stack=' + encodeURIComponent ( stack ) )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( j ) {
if ( ! j . ok ) { fabSetStatus ( 'Hiba: ' + ( j . error || 'a becslés sikertelen' ) ) ; return ; }
var size = ( j . data && j . data . total _size _human ) || '?' ;
felhomConfirm ( btn , 'A csomag becsült mérete: ' + size + '. Elindítod a letöltést? (Pillanatfelvétel a mostani állapotról.)' , function ( ) {
fabStart ( stack , null ) ;
} ) ;
} )
. catch ( function ( ) { fabSetStatus ( 'Hiba: a becslés nem érhető el.' ) ; } ) ;
}
function confirmDeleteVerifyCopy ( btn ) {
var path = btn . getAttribute ( 'data-copy-path' ) || '' ;
felhomConfirm ( btn , 'Biztosan törlöd ezt az ellenőrző másolatot? ' + path + ' — a tényleges adataid változatlanok maradnak.' , function ( ) {
felhomConfirm ( btn , 'UTOLSÓ MEGERŐSÍTÉS: a másolat véglegesen törlődik.' , function ( ) {
var f = btn . closest ( 'form' ) ;
if ( f ) { if ( f . requestSubmit ) f . requestSubmit ( ) ; else f . submit ( ) ; }
} ) ;
} ) ;
}
function fabStart ( stack , next ) {
fetch ( '/api/export/download/start' , { method : 'POST' , headers : Object . assign ( { 'Content-Type' : 'application/json' } , csrfHeaders ( ) ) , body : JSON . stringify ( { stack _name : stack , password : fabPassword ( ) } ) } )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( j ) {
if ( ! j . ok ) { fabSetStatus ( 'Hiba (' + stack + '): ' + ( j . error || 'az export nem indult el' ) ) ; if ( next ) next ( ) ; return ; }
fabSetStatus ( 'Csomag készítése: ' + stack + '…' ) ;
fabPoll ( stack , next ) ;
} )
. catch ( function ( ) { fabSetStatus ( 'Hiba: az export nem indult el.' ) ; if ( next ) next ( ) ; } ) ;
}
function fabPoll ( stack , next ) {
var iv = setInterval ( function ( ) {
fetch ( '/api/export/status' ) . then ( function ( r ) { return r . json ( ) ; } ) . then ( function ( j ) {
if ( ! j || j . running || ! j . done ) return ;
clearInterval ( iv ) ;
if ( j . error ) { fabSetStatus ( 'Hiba (' + stack + '): ' + j . error ) ; if ( next ) next ( ) ; return ; }
var base = ( j . output _path || '' ) . split ( '/' ) . pop ( ) ;
if ( ! base ) { fabSetStatus ( 'Hiba: a csomag útvonala hiányzik.' ) ; if ( next ) next ( ) ; return ; }
fabSetStatus ( 'Letöltés: ' + base + ( j . output _size ? ' (' + j . output _size + ')' : '' ) ) ;
window . location . href = '/api/export/download?file=' + encodeURIComponent ( base ) ;
if ( next ) setTimeout ( next , 3000 ) ;
} ) . catch ( function ( ) { } ) ;
} , 2000 ) ;
}
function fabDownloadAll ( ) {
var btns = document . querySelectorAll ( '.fab-dl-btn' ) ;
fabQueue = Array . prototype . map . call ( btns , function ( b ) { return b . getAttribute ( 'data-stack' ) ; } ) ;
fabRunQueue ( ) ;
}
function fabRunQueue ( ) {
var stack = fabQueue . shift ( ) ;
if ( ! stack ) { fabSetStatus ( 'Minden csomag elkészült.' ) ; return ; }
fabStart ( stack , fabRunQueue ) ;
}
var huDays = [ 'vasárnap' , 'hétfő' , 'kedd' , 'szerda' , 'csütörtök' , 'péntek' , 'szombat' ] ;
function formatSnapshot ( s ) {
var t = new Date ( s . time ) ;
var pad = function ( n ) { return n < 10 ? '0' + n : '' + n ; } ;
var label = fhIsoDate ( t . getFullYear ( ) + '-' + pad ( t . getMonth ( ) + 1 ) + '-' + pad ( t . getDate ( ) ) ) +
' ' + huDays [ t . getDay ( ) ] + ' ' + pad ( t . getHours ( ) ) + ':' + pad ( t . getMinutes ( ) ) +
' (' + s . short _id + ')' ;
var tierLabel = s . tier === 2 ? '2. szint' : '1. szint' ;
if ( s . drive _label ) {
label += ' — ' + tierLabel + ', ' + s . drive _label ;
} else {
label += ' — ' + tierLabel ;
}
return label ;
}
function onRestoreAppChange ( ) {
var sel = document . getElementById ( 'restore-app' ) ;
var appName = sel . value ;
var snapSel = document . getElementById ( 'restore-snapshot' ) ;
var noSnaps = document . getElementById ( 'restore-no-snapshots' ) ;
var typeInfo = document . getElementById ( 'restore-type-info' ) ;
document . getElementById ( 'restore-confirm-cb' ) . checked = false ;
document . getElementById ( 'restore-btn' ) . disabled = true ;
noSnaps . style . display = 'none' ;
typeInfo . style . display = 'none' ;
if ( ! appName ) {
snapSel . innerHTML = '<option value="">— Válassz alkalmazást —</option>' ;
return ;
}
var opt = sel . options [ sel . selectedIndex ] ;
var hasHDD = opt . getAttribute ( 'data-has-hdd' ) === 'true' ;
var hasDB = opt . getAttribute ( 'data-has-db' ) === 'true' ;
var hasVolumes = opt . getAttribute ( 'data-has-volumes' ) === 'true' ;
if ( hasHDD || hasVolumes ) {
typeInfo . innerHTML = 'Teljes visszaállítás: adatbázis + konfiguráció + felhasználói adatok a kiválasztott pillanatképből.' ;
typeInfo . className = 'restore-info' ;
} else if ( hasDB ) {
typeInfo . innerHTML = 'Adatbázis és konfiguráció visszaállítása — az alkalmazásnak nincs külön felhasználói adata.' ;
typeInfo . className = 'restore-info restore-info-partial' ;
} else {
typeInfo . innerHTML = 'Csak konfiguráció visszaállítása (compose fájlok, beállítások).' ;
typeInfo . className = 'restore-info restore-info-partial' ;
}
typeInfo . style . display = 'block' ;
snapSel . innerHTML = '<option value="">— Betöltés... —</option>' ;
fetch ( '/api/backup/snapshots?stack=' + encodeURIComponent ( appName ) )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( data ) {
snapSel . innerHTML = '<option value="">— Válassz —</option>' ;
if ( data . ok && data . data && data . data . length > 0 ) {
var tier1 = data . data . filter ( function ( s ) { return s . tier !== 2 ; } ) ;
var tier2 = data . data . filter ( function ( s ) { return s . tier === 2 ; } ) ;
if ( tier1 . length > 0 ) {
var grp1 = document . createElement ( 'optgroup' ) ;
grp1 . label = '1. szint — Helyi mentés (ajánlott)' ;
tier1 . forEach ( function ( s ) {
var o = document . createElement ( 'option' ) ;
o . value = s . short _id ;
o . textContent = formatSnapshot ( s ) ;
grp1 . appendChild ( o ) ;
} ) ;
snapSel . appendChild ( grp1 ) ;
}
if ( tier2 . length > 0 ) {
var grp2 = document . createElement ( 'optgroup' ) ;
grp2 . label = '2. szint — Másodlagos másolat' ;
tier2 . forEach ( function ( s ) {
var o = document . createElement ( 'option' ) ;
o . value = s . short _id ;
o . textContent = formatSnapshot ( s ) ;
grp2 . appendChild ( o ) ;
} ) ;
snapSel . appendChild ( grp2 ) ;
}
} else {
snapSel . innerHTML = '<option value="">— Nincs elérhető mentés —</option>' ;
noSnaps . style . display = 'block' ;
}
} ) ;
}
function onRestoreConfirmChange ( ) {
var cb = document . getElementById ( 'restore-confirm-cb' ) ;
var app = document . getElementById ( 'restore-app' ) . value ;
var snap = document . getElementById ( 'restore-snapshot' ) . value ;
document . getElementById ( 'restore-btn' ) . disabled = ! ( cb . checked && app && snap ) ;
}
function submitRestore ( ) {
var app = document . getElementById ( 'restore-app' ) . value ;
var snap = document . getElementById ( 'restore-snapshot' ) . value ;
if ( ! app || ! snap ) return ;
var btn = document . getElementById ( 'restore-btn' ) ;
btn . disabled = true ;
btn . textContent = 'Visszaállítás folyamatban...' ;
var form = document . createElement ( 'form' ) ;
form . method = 'POST' ;
form . action = '/backup/restore' ;
var fc = document . createElement ( 'input' ) ;
fc . type = 'hidden' ; fc . name = '_csrf' ;
fc . value = ( document . querySelector ( 'meta[name="csrf-token"]' ) || { } ) . content || '' ;
form . appendChild ( fc ) ;
var f1 = document . createElement ( 'input' ) ;
f1 . type = 'hidden' ; f1 . name = 'stack_name' ; f1 . value = app ;
form . appendChild ( f1 ) ;
var f2 = document . createElement ( 'input' ) ;
f2 . type = 'hidden' ; f2 . name = 'snapshot_id' ; f2 . value = snap ;
form . appendChild ( f2 ) ;
document . body . appendChild ( form ) ;
form . submit ( ) ;
}
< / script >
< / main >
< script >
( function ( ) {
var burger = document . querySelector ( '.nav-burger' ) ;
var sidebar = document . getElementById ( 'sidebar' ) ;
var backdrop = document . querySelector ( '.nav-backdrop' ) ;
if ( ! burger || ! sidebar || ! backdrop ) return ;
function setOpen ( open ) {
sidebar . classList . toggle ( 'is-open' , open ) ;
document . body . classList . toggle ( 'nav-open' , open ) ;
backdrop . hidden = ! open ;
burger . setAttribute ( 'aria-expanded' , open ? 'true' : 'false' ) ;
}
burger . addEventListener ( 'click' , function ( ) { setOpen ( ! sidebar . classList . contains ( 'is-open' ) ) ; } ) ;
backdrop . addEventListener ( 'click' , function ( ) { setOpen ( false ) ; } ) ;
document . addEventListener ( 'keydown' , function ( e ) {
if ( e . key === 'Escape' && sidebar . classList . contains ( 'is-open' ) ) setOpen ( false ) ;
} ) ;
} ) ( ) ;
document . addEventListener ( 'click' , function ( e ) {
if ( e . target . closest ( 'a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions' ) ) return ;
var card = e . target . closest ( '[data-href]' ) ;
if ( card ) window . location . href = card . dataset . href ;
} ) ;
async function checkBeforeDeploy ( e , name ) {
try {
var resp = await fetch ( '/api/stacks/' + name ) ;
var data = await resp . json ( ) ;
if ( data . ok && data . data && data . data . deployed ) {
e . preventDefault ( ) ;
showAlert ( 'Ez az alkalmazás már telepítve van.' ) ;
window . location . reload ( ) ;
return false ;
}
} catch ( err ) { }
return true ;
}
async function syncTemplates ( ) {
const btn = document . getElementById ( 'sync-btn' ) ;
const toast = document . getElementById ( 'sync-toast' ) ;
if ( ! btn ) return ;
const origText = btn . innerHTML ;
btn . disabled = true ;
btn . innerHTML = '↻ Frissítés...' ;
btn . classList . add ( 'loading' ) ;
try {
const resp = await fetch ( '/api/sync' , {
method : 'POST' ,
headers : Object . assign ( { 'Content-Type' : 'application/json' } , csrfHeaders ( ) )
} ) ;
const data = await resp . json ( ) ;
if ( toast ) {
toast . textContent = data . ok ? ( data . message || 'Sablonok frissítve' ) : ( 'Hiba: ' + ( data . error || 'Ismeretlen hiba' ) ) ;
toast . className = 'sync-toast ' + ( data . ok ? 'sync-toast-ok' : 'sync-toast-err' ) ;
toast . style . display = 'block' ;
setTimeout ( function ( ) { toast . style . display = 'none' ; } , 5000 ) ;
}
if ( data . ok && data . data && ( data . data . new _apps && data . data . new _apps . length > 0 || data . data . updated && data . data . updated . length > 0 ) ) {
setTimeout ( function ( ) { window . location . reload ( ) ; } , 1500 ) ;
}
} catch ( err ) {
if ( toast ) {
toast . textContent = 'Hálózati hiba: ' + err . message ;
toast . className = 'sync-toast sync-toast-err' ;
toast . style . display = 'block' ;
setTimeout ( function ( ) { toast . style . display = 'none' ; } , 5000 ) ;
}
}
btn . innerHTML = origText ;
btn . disabled = false ;
btn . classList . remove ( 'loading' ) ;
}
async function stackAction ( event , name , action ) {
const btn = event . currentTarget ;
const origText = btn . textContent ;
btn . disabled = true ;
btn . textContent = 'Folyamatban...' ;
btn . classList . add ( 'loading' ) ;
try {
const resp = await fetch ( '/api/stacks/' + name + '/' + action , {
method : 'POST' ,
headers : Object . assign ( { 'Content-Type' : 'application/json' } , csrfHeaders ( ) )
} ) ;
const data = await resp . json ( ) ;
if ( ! data . ok ) {
showAlert ( 'Hiba: ' + ( data . error || 'Ismeretlen hiba' ) ) ;
btn . textContent = origText ;
btn . disabled = false ;
btn . classList . remove ( 'loading' ) ;
return ;
}
if ( action === 'update' ) {
followUpdate ( name , btn ) ;
return ;
}
window . location . reload ( ) ;
} catch ( err ) {
showAlert ( 'Hálózati hiba: ' + err . message ) ;
btn . textContent = origText ;
btn . disabled = false ;
btn . classList . remove ( 'loading' ) ;
}
}
function followUpdate ( name , btn ) {
var started = Date . now ( ) ;
var timer = setInterval ( async function ( ) {
if ( Date . now ( ) - started > 30 * 60 * 1000 ) { clearInterval ( timer ) ; window . location . reload ( ) ; return ; }
try {
var r = await fetch ( '/api/stacks/' + name ) ;
var d = await r . json ( ) ;
if ( ! d . ok || ! d . data ) return ;
if ( d . data . update _phase _label ) btn . textContent = d . data . update _phase _label ;
if ( ! d . data . updating ) { clearInterval ( timer ) ; window . location . reload ( ) ; }
} catch ( e ) { }
} , 3000 ) ;
}
async function deleteOrphanStack ( name ) {
var modal = document . createElement ( 'div' ) ;
modal . className = 'modal-overlay' ;
modal . id = 'delete-modal' ;
modal . innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>' ;
modal . addEventListener ( 'click' , function ( e ) { if ( e . target === modal ) closeDeleteModal ( ) ; } ) ;
document . body . appendChild ( modal ) ;
try {
var resp = await fetch ( '/api/stacks/' + name + '/hdd-data' ) ;
var data = await resp . json ( ) ;
var hddInfo = '' ;
var checkboxHTML = '' ;
if ( data . ok && data . data && data . data . has _hdd _data ) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>' ;
data . data . hdd _paths . forEach ( function ( p ) {
hddInfo += '<div class="modal-hdd-path">' + p . path + ' (' + ( p . exists ? p . size _human : 'nem létezik' ) + ')</div>' ;
} ) ;
hddInfo += '</div>' ;
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>' ;
}
if ( data . ok && data . data && data . data . userdata _kept && data . data . userdata _kept . length > 0 ) {
var udPaths = '' ;
data . data . userdata _kept . forEach ( function ( p ) { udPaths += '<div class="modal-hdd-path">' + p . path + ' (' + p . size _human + ')</div>' ; } ) ;
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>' ;
}
modal . querySelector ( '.modal-card' ) . innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>' ;
} catch ( err ) {
modal . querySelector ( '.modal-card' ) . innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err . message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>' ;
}
}
function closeDeleteModal ( ) {
var modal = document . getElementById ( 'delete-modal' ) ;
if ( modal ) modal . remove ( ) ;
}
async function confirmDelete ( name ) {
var btn = document . getElementById ( 'confirm-delete-btn' ) ;
var checkbox = document . getElementById ( 'delete-hdd-check' ) ;
var removeHDD = checkbox ? checkbox . checked : false ;
btn . disabled = true ;
btn . textContent = 'Törlés folyamatban...' ;
try {
var resp = await fetch ( '/api/stacks/' + name , {
method : 'DELETE' ,
headers : Object . assign ( { 'Content-Type' : 'application/json' } , csrfHeaders ( ) ) ,
body : JSON . stringify ( { remove _hdd _data : removeHDD } )
} ) ;
var data = await resp . json ( ) ;
if ( data . ok ) {
var modal = document . getElementById ( 'delete-modal' ) ;
var removedInfo = '' ;
if ( data . data && data . data . hdd _paths _removed && data . data . hdd _paths _removed . length > 0 ) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data . data . hdd _paths _removed . join ( ', ' ) + '</p>' ;
}
var preservedInfo = '' ;
if ( data . data && data . data . hdd _paths _preserved && data . data . hdd _paths _preserved . length > 0 ) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data . data . hdd _paths _preserved . join ( ', ' ) + '</p>' ;
}
if ( data . data && data . data . hdd _note ) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data . data . hdd _note + '</p>' ;
}
if ( data . data && data . data . userdata _kept && data . data . userdata _kept . length > 0 ) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data . data . userdata _kept . join ( ', ' ) + '</p>' ;
}
modal . querySelector ( '.modal-card' ) . innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>' ;
} else {
showAlert ( 'Hiba: ' + ( data . error || 'Ismeretlen hiba' ) ) ;
btn . disabled = false ;
btn . textContent = 'Törlés' ;
}
} catch ( err ) {
showAlert ( 'Hálózati hiba: ' + err . message ) ;
btn . disabled = false ;
btn . textContent = 'Törlés' ;
}
}
async function removeStack ( name ) {
var modal = document . createElement ( 'div' ) ;
modal . className = 'modal-overlay' ;
modal . id = 'remove-modal' ;
modal . innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>' ;
modal . addEventListener ( 'click' , function ( e ) { if ( e . target === modal ) closeRemoveModal ( ) ; } ) ;
document . body . appendChild ( modal ) ;
try {
var [ hddResp , backupResp ] = await Promise . all ( [
fetch ( '/api/stacks/' + name + '/hdd-data' ) . then ( function ( r ) { return r . json ( ) ; } ) ,
fetch ( '/api/stacks/' + name + '/backup-data' ) . then ( function ( r ) { return r . json ( ) ; } )
] ) ;
var sections = '' ;
var keepOnly = hddResp . ok && hddResp . data && hddResp . data . keep _data _only ;
if ( keepOnly ) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>' ;
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>' ;
var hddCheckbox = '' ;
if ( ! keepOnly && hddResp . ok && hddResp . data && hddResp . data . has _hdd _data ) {
var hddPaths = '' ;
hddResp . data . hdd _paths . forEach ( function ( p ) {
hddPaths += '<div class="modal-hdd-path">' + p . path + ' (' + ( p . exists ? p . size _human : 'nem létezik' ) + ')</div>' ;
} ) ;
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>' ;
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>' ;
}
var backupCheckbox = '' ;
if ( ! keepOnly && backupResp . ok && backupResp . data && backupResp . data . has _backups ) {
var bkPaths = '' ;
backupResp . data . backup _paths . forEach ( function ( p ) {
if ( p . exists ) bkPaths += '<div class="modal-hdd-path">' + p . path + ' (' + p . size _human + ')</div>' ;
} ) ;
if ( bkPaths ) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>' ;
}
}
if ( hddResp . ok && hddResp . data && hddResp . data . userdata _kept && hddResp . data . userdata _kept . length > 0 ) {
var udPaths = '' ;
hddResp . data . userdata _kept . forEach ( function ( p ) { udPaths += '<div class="modal-hdd-path">' + p . path + ' (' + p . size _human + ')</div>' ; } ) ;
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>' ;
}
modal . querySelector ( '.modal-card' ) . innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox ;
} catch ( err ) {
modal . querySelector ( '.modal-card' ) . innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err . message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>' ;
}
}
function closeRemoveModal ( ) {
var modal = document . getElementById ( 'remove-modal' ) ;
if ( modal ) modal . remove ( ) ;
}
async function confirmRemoveStack ( name ) {
var btn = document . getElementById ( 'confirm-remove-btn' ) ;
var hddCheck = document . getElementById ( 'remove-hdd-check' ) ;
var backupCheck = document . getElementById ( 'remove-backup-check' ) ;
var removeHDD = hddCheck ? hddCheck . checked : false ;
var removeBackups = backupCheck ? backupCheck . checked : false ;
btn . disabled = true ;
btn . textContent = 'Eltávolítás folyamatban...' ;
try {
var resp = await fetch ( '/api/stacks/' + name + '/remove' , {
method : 'POST' ,
headers : Object . assign ( { 'Content-Type' : 'application/json' } , csrfHeaders ( ) ) ,
body : JSON . stringify ( { remove _hdd _data : removeHDD , remove _backups : removeBackups } )
} ) ;
var data = await resp . json ( ) ;
if ( data . ok ) {
var modal = document . getElementById ( 'remove-modal' ) ;
var removedInfo = '' ;
if ( data . data && data . data . hdd _paths _removed && data . data . hdd _paths _removed . length > 0 ) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data . data . hdd _paths _removed . join ( ', ' ) + '</p>' ;
}
if ( data . data && data . data . backup _paths _removed && data . data . backup _paths _removed . length > 0 ) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data . data . backup _paths _removed . join ( ', ' ) + '</p>' ;
}
var preservedInfo = '' ;
if ( data . data && data . data . hdd _paths _preserved && data . data . hdd _paths _preserved . length > 0 ) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data . data . hdd _paths _preserved . join ( ', ' ) + '</p>' ;
}
if ( data . data && data . data . hdd _note ) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data . data . hdd _note + '</p>' ;
}
if ( data . data && data . data . userdata _kept && data . data . userdata _kept . length > 0 ) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data . data . userdata _kept . join ( ', ' ) + '</p>' ;
}
if ( data . data && data . data . backup _paths _refused && data . data . backup _paths _refused . length > 0 ) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data . data . backup _paths _refused . join ( '; ' ) + '</p>' ;
}
modal . querySelector ( '.modal-card' ) . innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>' ;
} else {
showAlert ( 'Hiba: ' + ( data . error || 'Ismeretlen hiba' ) ) ;
btn . disabled = false ;
btn . textContent = 'Eltávolítás' ;
}
} catch ( err ) {
showAlert ( 'Hálózati hiba: ' + err . message ) ;
btn . disabled = false ;
btn . textContent = 'Eltávolítás' ;
}
}
< / script >
< / body >
< / html >