9e68e526e9
gates / gates (push) Failing after 48s
Biztonsagi mentes was still a card — it sits directly in the tab, not inside a section, so the
.app-section scoping rule did not reach it. It is a section now.
The 2nd-copy heading still read '2. mentes beallitasa — <app>', because the locale key carries the
app name: it was a PAGE title, and a tab already says which app it is. New key
app_page.second_backup ('2. mentes' / 'Second backup'). This is the leftover the task named.
Found by looking at the rendered page, not the markup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
504 lines
24 KiB
HTML
504 lines
24 KiB
HTML
{{define "app_data"}}
|
|
{{template "layout_start" .}}
|
|
<div class="app-page">
|
|
{{template "app_shell_start" .}}
|
|
|
|
{{/* „ADATOK ES MENTES" — everything about this app's FILES, in the order the household asks it:
|
|
|
|
where are they -> „Hova tegyem a fajlokat?" and the move-to-another-drive offer
|
|
are they safe -> the nightly copy, and the 2nd-copy setting that used to be reachable only
|
|
from the backup page's „Beallitas" button
|
|
how do I get them out -> the export, which was a page of its own
|
|
|
|
Both forms keep the URLs they always POSTed to (`/stacks/{name}/backup` and the export API), so
|
|
nothing about saving changed — only where the household finds the controls. */}}
|
|
|
|
<div class="app-data-tab">
|
|
{{if .Flash}}<div class="monitoring-banner monitoring-banner-green">{{.Flash}}</div>{{end}}
|
|
{{if .FlashError}}<div class="monitoring-banner monitoring-banner-red">{{.FlashError}}</div>{{end}}
|
|
|
|
{{if .DataPathCards}}
|
|
<div class="app-section" style="margin-top:1rem">
|
|
<h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3>
|
|
<p class="form-hint">{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}</p>
|
|
<div class="datapath-list">
|
|
{{range .DataPathCards}}
|
|
<div class="datapath-row">
|
|
<div class="datapath-head">
|
|
<a href="{{.Link}}" target="_blank" rel="noopener" class="datapath-link">{{.Label}} ↗</a>
|
|
{{if .FreeSpace}}<span class="datapath-space">{{.FreeSpace}}</span>{{end}}
|
|
</div>
|
|
{{if .Consequence}}<p class="datapath-note">{{.Consequence}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if and .Stack.Deployed .MigrateTargets}}
|
|
<div class="app-section" style="margin-top:1rem">
|
|
<h3>{{T "app_info.athelyezes_masik_tarhelyre"}}</h3>
|
|
<p class="form-hint">{{T "app_info.ennek_az_alkalmazasnak_az_adatait"}}</p>
|
|
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem">
|
|
<select id="app-migrate-target" class="btn btn-sm btn-outline">
|
|
<option value="">{{T "app_info.valassz_celtarhelyet"}}</option>
|
|
{{range .MigrateTargets}}<option value="{{.Path}}">{{.Label}} ({{.Path}})</option>{{end}}
|
|
</select>
|
|
<button class="btn btn-sm btn-outline" onclick="appMigrate(this,'{{.Stack.Name}}','{{.Meta.DisplayName}}')">{{T "app_info.athelyezes"}}</button>
|
|
</div>
|
|
<div id="app-migrate-progress" style="display:none;margin-top:.75rem;padding:.75rem;border:1px solid var(--blue);border-radius:6px;background:rgba(0,136,204,0.06)"></div>
|
|
</div>
|
|
<script>
|
|
function appMigFmtGB(b){ return fhNum(Number(b||0)/1e9, 1)+' GB'; }
|
|
function appMigRender(job){
|
|
var names={stop:'{{T "app_info.leallitas"}}',copy:'{{T "app_info.adatok_masolasa"}}',verify:'{{T "app_info.ellenorzes"}}',flip:'{{T "app_info.ujratelepites"}}',redeploy:'{{T "app_info.ujratelepites"}}',cleanup:'{{T "app_info.regi_adatok_torlese"}}'};
|
|
var s=names[job.phase]||job.phase;
|
|
if(job.phase==='copy'&&job.bytes_total>0){ s+=' ('+Math.floor(100*job.bytes_done/job.bytes_total)+'% — '+appMigFmtGB(job.bytes_done)+'/'+appMigFmtGB(job.bytes_total)+')'; }
|
|
return s+'…';
|
|
}
|
|
function appMigWatch(){
|
|
var panel=document.getElementById('app-migrate-progress');
|
|
if(panel) panel.style.display='block';
|
|
function tick(){
|
|
fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){
|
|
var job=d.data&&d.data.job;
|
|
if(!job){ if(panel) panel.textContent='{{T "app_info.nincs_folyamatban_athelyezes"}}'; return; }
|
|
if(panel) panel.innerHTML=appMigRender(job);
|
|
if(job.phase==='done'){ if(panel) panel.innerHTML+='<br><strong>{{T "app_info.kesz"}}</strong>'; setTimeout(function(){location.reload();},1500); return; }
|
|
if(job.phase==='aborted'){ if(panel) panel.innerHTML+='<br><strong style="color:var(--danger,#c0392b)">{{T "app_info.megszakadt"}} '+(job.error||'')+'</strong><br>{{T "app_info.a_regi_adatok_erintetlenek"}}'; return; }
|
|
setTimeout(tick,1500);
|
|
}).catch(function(){ setTimeout(tick,2000); });
|
|
}
|
|
tick();
|
|
}
|
|
function appMigrate(btn,app,label){
|
|
var sel=document.getElementById('app-migrate-target');
|
|
var target=sel?sel.value:'';
|
|
if(!target){ showAlert('{{T "app_info.valassz_celtarhelyet_2"}}'); return; }
|
|
felhomConfirm(btn,'{{T "app_info.athelyezed_a_z"}} '+label+' {{T "app_info.adatait_ide"}} '+target+'{{T "app_info.az_alkalmazas_rovid_idore_leall"}}',function(){
|
|
fetch('/api/storage/migrate-app',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({app:app,target:target})})
|
|
.then(function(r){return r.json();}).then(function(d){ if(d.ok){ appMigWatch(); } else { showAlert('{{T "common.hiba_kettospont"}} '+(d.error||'{{T "app_info.ismeretlen"}}')); } })
|
|
.catch(function(e){ showAlert('{{T "common.hiba_kettospont"}} '+e); });
|
|
});
|
|
}
|
|
(function(){ fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){ if(d.data&&d.data.job){ appMigWatch(); } }).catch(function(){}); })();
|
|
</script>
|
|
{{end}}
|
|
|
|
{{/* The nightly copy, stated as a fact. It was a card on the settings page; the files it is about
|
|
live here. */}}
|
|
<div class="app-section deploy-cross-drive">
|
|
<h3>{{T "deploy.biztonsagi_mentes"}}</h3>
|
|
|
|
<div class="cross-drive-nightly">
|
|
<span class="form-hint" style="display:block;margin-top:.25rem">
|
|
{{T "deploy.az_alkalmazas_adatbazisa_es_docker"}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{if .Tier2}}
|
|
{{/* v0.316.0: a SECTION, not a page title over a card. „2. mentés beállítása — <app>" was the
|
|
heading of a page that no longer exists; the tab already says which app this is. */}}
|
|
<div class="app-section">
|
|
<h3>{{T "app_page.second_backup"}}</h3>
|
|
{{with .Tier2}}
|
|
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
|
|
{{T "tier2_config.a_2_mentes_egy_masik"}}
|
|
</p>
|
|
|
|
{{if not .IsHDDApp}}
|
|
{{if eq $.SystemBackup "protected"}}
|
|
<div class="app-callout" style="margin-top:1rem" data-system-backup="protected">
|
|
{{T "tier2_config.rendszerlemez.vedett"}}
|
|
</div>
|
|
{{else if eq $.SystemBackup "same_disk"}}
|
|
<div class="app-callout app-callout-warn" style="margin-top:1rem" data-system-backup="same_disk">
|
|
{{T "tier2_config.rendszerlemez.ugyanaz"}}
|
|
</div>
|
|
{{else if eq $.SystemBackup "absent"}}
|
|
<div class="app-callout app-callout-warn" style="margin-top:1rem" data-system-backup="absent">
|
|
{{T "tier2_config.rendszerlemez.hianyzik"}}
|
|
</div>
|
|
{{else}}
|
|
<div class="app-callout app-callout-warn" style="margin-top:1rem" data-system-backup="unknown">
|
|
{{T "tier2_config.rendszerlemez.ismeretlen"}}
|
|
</div>
|
|
{{end}}
|
|
{{else}}
|
|
<h3 style="margin-top:1.5rem">{{T "tier2_config.jelenlegi_allapot"}}</h3>
|
|
<div class="sysinfo-grid">
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">{{T "tier2_config.2_mentes"}}</span>
|
|
<span class="sysinfo-value">{{if .Disabled}}{{T "tier2_config.kikapcsolva"}}{{else}}{{T "tier2_config.bekapcsolva"}}{{end}}</span>
|
|
</div>
|
|
{{if .NoTarget}}
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">{{T "tier2_config.cel"}}</span>
|
|
<span class="sysinfo-value text-error">{{T "tier2_config.nincs_elerheto_off_drive_cel"}}</span>
|
|
</div>
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">{{T "tier2_config.megjegyzes"}}</span>
|
|
<span class="sysinfo-value" style="color:var(--text-3)">{{.NoTargetReason}}</span>
|
|
</div>
|
|
{{else}}
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">{{T "tier2_config.cel_meghajto"}}</span>
|
|
<span class="sysinfo-value">{{.EffectiveLabel}}{{if .EffectiveIsSSD}} {{T "tier2_config.csak_db_konfiguracio"}}{{end}}</span>
|
|
</div>
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">{{T "tier2_config.kivalasztas_modja"}}</span>
|
|
<span class="sysinfo-value" style="color:var(--text-3)">{{if .Preferred}}{{T "tier2_config.kezi_valasztas"}}{{else}}{{T "tier2_config.automatikus_kisbetu"}}{{end}} — {{.EffectiveDesc}}</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{if .EffectiveIsSSD}}
|
|
<div class="app-callout app-callout-warn" style="margin-top:1rem">
|
|
{{T "tier2_config.jelenleg_csak_a_belso_ssd"}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="/stacks/{{$.StackName}}/backup" style="margin-top:1.5rem">
|
|
{{$.CSRFField}}
|
|
<div class="form-group">
|
|
<label style="display:flex;align-items:center;gap:.5rem">
|
|
<input type="checkbox" name="enabled" value="on" {{if not .Disabled}}checked{{end}}>
|
|
{{T "tier2_config.2_mentes_bekapcsolva"}}
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="target">{{T "tier2_config.cel_meghajto"}}</label>
|
|
<select id="target" name="target" class="form-control">
|
|
<option value="">{{T "tier2_config.automatikus"}}{{if not .NoTarget}} {{T "tier2_config.jelenleg"}}{{end}}</option>
|
|
{{range .Alternatives}}
|
|
<option value="{{.Path}}" {{if eq .Path $.Tier2.Preferred}}selected{{end}}>{{.Label}}</option>
|
|
{{end}}
|
|
</select>
|
|
{{if not .Alternatives}}
|
|
<div style="font-size:.8rem;color:var(--text-3);margin-top:.4rem">
|
|
{{T "tier2_config.nincs_masik_adatmeghajto_automatikus_cel"}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">{{T "tier2_config.mentes"}}</button>
|
|
</form>
|
|
{{end}}
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .ExportOffered}}
|
|
<div class="app-section">
|
|
<h3>{{T "app_export.exportalas"}}</h3>
|
|
<h4 style="font-size:14px;font-weight:600;margin:12px 0 6px">{{T "app_export.mentes_helye"}}</h4>
|
|
<select id="destDrive" onchange="loadEstimate()" style="width:100%;padding:.5rem;margin-bottom:1rem">
|
|
<option value="">{{T "app_export.valassz_tarolot"}}</option>
|
|
{{range .Drives}}
|
|
<option value="{{.Path}}">{{if .Label}}{{.Label}} ({{.Path}}){{else}}{{.Path}}{{end}}</option>
|
|
{{end}}
|
|
</select>
|
|
|
|
<div id="estimateBox" style="display:none;background:var(--bg-2);border-radius:8px;padding:1rem;margin-bottom:1.5rem">
|
|
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
|
|
<span>{{T "app_export.konfiguracio"}}</span>
|
|
<span id="estConfig">-</span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
|
|
<span>{{T "app_export.felhasznaloi_adatok"}}</span>
|
|
<span id="estData">-</span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;font-weight:600;margin-bottom:.25rem;border-top:1px solid var(--line);padding-top:.5rem">
|
|
<span>{{T "app_export.osszesen"}}</span>
|
|
<span><span id="estTotal">-</span> <span id="estFree" style="color:var(--text-3)">{{T "app_export.szabad"}}</span></span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between">
|
|
<span>{{T "app_export.becsult_ido"}}</span>
|
|
<span id="estTime">-</span>
|
|
</div>
|
|
<div id="estWarning" style="display:none;color:var(--crit);margin-top:.5rem;font-weight:600"></div>
|
|
</div>
|
|
|
|
<div id="classSelect" style="display:none;margin-bottom:1.5rem">
|
|
<div id="mandBox" style="display:none;margin-bottom:1rem">
|
|
<div style="font-weight:600;margin-bottom:.25rem">{{T "app_export.mindig_resze_a_mentesnek"}}</div>
|
|
<div id="mandList" style="color:var(--text-3);font-size:.9rem"></div>
|
|
</div>
|
|
<div id="optBox" style="display:none;margin-bottom:1rem">
|
|
<div style="font-weight:600;margin-bottom:.25rem">{{T "app_export.valaszthato_tartalom"}}</div>
|
|
<div id="optList"></div>
|
|
</div>
|
|
<div id="exclBox" style="display:none">
|
|
<div id="exclSummary" style="margin-bottom:.25rem"></div>
|
|
<div style="color:var(--text-3);font-size:.85rem;margin-bottom:.5rem">{{T "app_export.a_kihagyott_mappak_tartalma_a"}}</div>
|
|
<button type="button" class="btn btn-xs btn-outline" onclick="toggleExcl()" id="exclToggle">{{T "app_export.kihagyott_tartalom_megjelenitese"}}</button>
|
|
<div id="exclList" style="display:none;margin-top:.5rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>{{T "app_export.jelszo_opcionalis"}}</h3>
|
|
<div style="display:flex;gap:.5rem;margin-bottom:1rem">
|
|
<input type="password" id="exportPassword" placeholder="{{T "app_export.titkositasi_jelszo"}}" style="flex:1;padding:.5rem">
|
|
<button class="btn btn-sm btn-outline" onclick="togglePw()" type="button" title="{{T "app_export.jelszo_mutatasa"}}">👁</button>
|
|
</div>
|
|
|
|
<label style="display:flex;align-items:flex-start;gap:.5rem;margin-bottom:.5rem;cursor:pointer">
|
|
<input type="checkbox" id="stopApp" checked style="margin-top:3px">
|
|
<div>
|
|
<strong>{{T "app_export.alkalmazas_leallitasa_mentes_elott_ajanl"}}</strong>
|
|
<div style="color:var(--text-3);font-size:.85rem">{{T "app_export.az_adatok_konzisztenciaja_erdekeben_java"}}</div>
|
|
</div>
|
|
</label>
|
|
|
|
<button id="startBtn" class="btn btn-primary" onclick="startExport()" style="margin-top:1rem;width:100%" disabled>
|
|
{{T "app_export.exportalas_inditasa"}}
|
|
</button>
|
|
</div>
|
|
|
|
<div id="progressCard" class="card" style="max-width:700px;display:none">
|
|
<h3>{{T "app_export.folyamat"}}</h3>
|
|
<div id="progressSteps"></div>
|
|
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
|
|
</div>
|
|
|
|
<div id="doneCard" class="card" style="max-width:700px;display:none">
|
|
<h3 style="color:var(--blue)">{{T "app_export.kesz"}}</h3>
|
|
<div style="margin-bottom:1rem">
|
|
<span id="doneFile" style="font-weight:600"></span>
|
|
<span id="doneSize" style="color:var(--text-3)"></span>
|
|
</div>
|
|
<a id="doneFBLink" href="#" target="_blank" class="btn btn-outline">{{T "app_export.megnyitas_filebrowser_ben"}}</a>
|
|
</div>
|
|
|
|
<script>
|
|
var stackName = '{{.Stack.Name}}';
|
|
var domain = '{{.Stack.Meta.Subdomain}}' ? '{{.Stack.Meta.Subdomain}}.{{$.Domain}}' : '';
|
|
var pollTimer = null;
|
|
|
|
function csrfH() {
|
|
var el = document.querySelector('meta[name="csrf-token"]');
|
|
return el ? {'X-CSRF-Token': el.content, 'Content-Type': 'application/json'} : {'Content-Type': 'application/json'};
|
|
}
|
|
|
|
function togglePw() {
|
|
var inp = document.getElementById('exportPassword');
|
|
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
}
|
|
|
|
async function loadEstimate() {
|
|
var drive = document.getElementById('destDrive').value;
|
|
var box = document.getElementById('estimateBox');
|
|
var btn = document.getElementById('startBtn');
|
|
|
|
if (!drive) {
|
|
box.style.display = 'none';
|
|
btn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var resp = await fetch('/api/export/estimate?stack=' + encodeURIComponent(stackName) + '&drive=' + encodeURIComponent(drive));
|
|
var data = await resp.json();
|
|
if (!data.ok) {
|
|
box.style.display = 'none';
|
|
btn.disabled = true;
|
|
return;
|
|
}
|
|
var est = data.data;
|
|
document.getElementById('estConfig').textContent = est.config_size_human;
|
|
document.getElementById('estData').textContent = est.data_size_human;
|
|
document.getElementById('estTotal').textContent = '~' + est.total_size_human;
|
|
document.getElementById('estFree').textContent = '{{T "app_export.szabad_2"}} ' + est.dest_free_human + ')';
|
|
document.getElementById('estTime').textContent = '~' + est.estimated_minutes + ' {{T "app_export.perc"}}';
|
|
|
|
var warn = document.getElementById('estWarning');
|
|
if (!est.fits_on_dest) {
|
|
warn.textContent = '{{T "app_export.nincs_eleg_szabad_hely_a"}}';
|
|
warn.style.display = 'block';
|
|
btn.disabled = true;
|
|
} else {
|
|
warn.style.display = 'none';
|
|
btn.disabled = false;
|
|
}
|
|
box.style.display = 'block';
|
|
renderClassSelect(est);
|
|
} catch(e) {
|
|
console.error('Estimate error:', e);
|
|
}
|
|
}
|
|
|
|
// Task 4: the class-scoped selection UI (classified apps only; legacy apps see the plain estimate).
|
|
var lastEst = null;
|
|
function humanBytes(b) {
|
|
if (b >= 1073741824) return fhNum(b/1073741824, 1) + ' GB';
|
|
if (b >= 1048576) return fhNum(b/1048576, 1) + ' MB';
|
|
if (b >= 1024) return fhNum(b/1024, 1) + ' KB';
|
|
return b + ' B';
|
|
}
|
|
function renderClassSelect(est) {
|
|
lastEst = est;
|
|
var wrap = document.getElementById('classSelect');
|
|
if (!est.has_classification) { wrap.style.display = 'none'; return; }
|
|
wrap.style.display = 'block';
|
|
|
|
var mand = est.mandatory_items || [], opt = est.optional_items || [], excl = est.excluded_items || [];
|
|
// Mandatory (locked).
|
|
var mb = document.getElementById('mandBox');
|
|
if (mand.length) {
|
|
mb.style.display = 'block';
|
|
document.getElementById('mandList').innerHTML = mand.map(function(i){
|
|
return '<div>' + esc(i.rel_path) + ' <span style="opacity:.7">(' + i.human + ')</span></div>';
|
|
}).join('');
|
|
} else { mb.style.display = 'none'; }
|
|
// Optional (pre-selected checkboxes).
|
|
var ob = document.getElementById('optBox');
|
|
if (opt.length) {
|
|
ob.style.display = 'block';
|
|
document.getElementById('optList').innerHTML = opt.map(function(i){
|
|
return '<label style="display:flex;gap:.5rem;align-items:center;cursor:pointer;padding:.15rem 0">' +
|
|
'<input type="checkbox" class="fab-opt" checked data-key="' + esc(i.key) + '" data-bytes="' + i.bytes + '" onchange="recomputeTotal()">' +
|
|
'<span>' + esc(i.rel_path) + ' <span style="opacity:.7">(' + i.human + ')</span></span></label>';
|
|
}).join('');
|
|
} else { ob.style.display = 'none'; }
|
|
// Excluded (opt-in, collapsed, behind the two-number warning).
|
|
var eb = document.getElementById('exclBox');
|
|
if (excl.length) {
|
|
eb.style.display = 'block';
|
|
document.getElementById('exclList').innerHTML = excl.map(function(i){
|
|
return '<label style="display:flex;gap:.5rem;align-items:center;cursor:pointer;padding:.15rem 0">' +
|
|
'<input type="checkbox" class="fab-excl" data-key="' + esc(i.key) + '" data-bytes="' + i.bytes + '" onchange="recomputeTotal()">' +
|
|
'<span>' + esc(i.rel_path) + ' <span style="opacity:.7">(' + i.human + ')</span></span></label>';
|
|
}).join('');
|
|
} else { eb.style.display = 'none'; }
|
|
recomputeTotal();
|
|
}
|
|
function recomputeTotal() {
|
|
if (!lastEst) return;
|
|
var base = lastEst.base_bytes || 0;
|
|
var optSum = 0, exclSum = 0, exclAll = 0;
|
|
document.querySelectorAll('.fab-opt').forEach(function(cb){ if (cb.checked) optSum += parseInt(cb.dataset.bytes,10)||0; });
|
|
document.querySelectorAll('.fab-excl').forEach(function(cb){ var b = parseInt(cb.dataset.bytes,10)||0; exclAll += b; if (cb.checked) exclSum += b; });
|
|
var alap = base + optSum;
|
|
var full = alap + exclAll;
|
|
var s = document.getElementById('exclSummary');
|
|
if ((lastEst.excluded_items||[]).length) {
|
|
s.innerHTML = '{{T "app_export.alap_mentes"}}' + humanBytes(alap) + '{{T "app_export.a_kihagyott_nagy_meretu_tartalommal"}}' + humanBytes(full) + '.';
|
|
}
|
|
}
|
|
function toggleExcl() {
|
|
var l = document.getElementById('exclList');
|
|
var shown = l.style.display !== 'none';
|
|
l.style.display = shown ? 'none' : 'block';
|
|
document.getElementById('exclToggle').textContent = shown ? '{{T "app_export.kihagyott_tartalom_megjelenitese"}}' : '{{T "app_export.kihagyott_tartalom_elrejtese"}}';
|
|
}
|
|
function gatherSelections() {
|
|
var deselect = [], optIn = [];
|
|
document.querySelectorAll('.fab-opt').forEach(function(cb){ if (!cb.checked) deselect.push(cb.dataset.key); });
|
|
document.querySelectorAll('.fab-excl').forEach(function(cb){ if (cb.checked) optIn.push(cb.dataset.key); });
|
|
return { deselect_optional: deselect, opt_in_excluded: optIn };
|
|
}
|
|
function esc(s) { return String(s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
|
|
|
|
async function startExport() {
|
|
var drive = document.getElementById('destDrive').value;
|
|
var password = document.getElementById('exportPassword').value;
|
|
var stopApp = document.getElementById('stopApp').checked;
|
|
|
|
document.getElementById('startBtn').disabled = true;
|
|
document.getElementById('progressCard').style.display = 'block';
|
|
document.getElementById('doneCard').style.display = 'none';
|
|
|
|
try {
|
|
var sel = gatherSelections();
|
|
var resp = await fetch('/api/export/start', {
|
|
method: 'POST',
|
|
headers: csrfH(),
|
|
body: JSON.stringify({
|
|
stack_name: stackName,
|
|
dest_drive: drive,
|
|
password: password,
|
|
stop_app: stopApp,
|
|
deselect_optional: sel.deselect_optional,
|
|
opt_in_excluded: sel.opt_in_excluded
|
|
})
|
|
});
|
|
var data = await resp.json();
|
|
if (!data.ok) {
|
|
showError(data.error || '{{T "common.hiba_tortent"}}');
|
|
return;
|
|
}
|
|
pollStatus();
|
|
} catch(e) {
|
|
showError(e.message);
|
|
}
|
|
}
|
|
|
|
function pollStatus() {
|
|
if (pollTimer) clearInterval(pollTimer);
|
|
pollTimer = setInterval(async function() {
|
|
try {
|
|
var resp = await fetch('/api/export/status');
|
|
var data = await resp.json();
|
|
renderSteps(data.steps || []);
|
|
|
|
if (data.error) {
|
|
showError(data.error);
|
|
clearInterval(pollTimer);
|
|
return;
|
|
}
|
|
|
|
if (data.done && !data.error) {
|
|
clearInterval(pollTimer);
|
|
showDone(data);
|
|
}
|
|
} catch(e) {
|
|
console.error('Poll error:', e);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function renderSteps(steps) {
|
|
var html = '';
|
|
for (var i = 0; i < steps.length; i++) {
|
|
var s = steps[i];
|
|
var icon = '○'; // pending
|
|
if (s.status === 'running') icon = '⟳';
|
|
if (s.status === 'done') icon = '✓';
|
|
if (s.status === 'failed') icon = '✗';
|
|
var cls = s.status === 'failed' ? 'color:var(--crit)' : s.status === 'done' ? 'color:var(--blue)' : s.status === 'running' ? 'color:var(--blue)' : '';
|
|
html += '<div style="padding:.25rem 0;' + cls + '">' + icon + ' ' + s.label;
|
|
if (s.error) html += ' <span style="font-size:.85rem">(' + s.error + ')</span>';
|
|
html += '</div>';
|
|
}
|
|
document.getElementById('progressSteps').innerHTML = html;
|
|
}
|
|
|
|
function showError(msg) {
|
|
var el = document.getElementById('progressError');
|
|
el.textContent = msg;
|
|
el.style.display = 'block';
|
|
document.getElementById('startBtn').disabled = false;
|
|
}
|
|
|
|
function showDone(data) {
|
|
document.getElementById('progressCard').style.display = 'none';
|
|
document.getElementById('doneCard').style.display = 'block';
|
|
var fileName = (data.output_path || '').split('/').pop();
|
|
document.getElementById('doneFile').textContent = fileName;
|
|
document.getElementById('doneSize').textContent = data.output_size ? '(' + data.output_size + ')' : '';
|
|
|
|
// Build FileBrowser link to the exports directory
|
|
var drive = document.getElementById('destDrive').value;
|
|
var fbPath = drive + '/felhom-data/exports/';
|
|
document.getElementById('doneFBLink').href = 'https://files.' + location.hostname.split('.').slice(-2).join('.') + '/files' + fbPath;
|
|
}
|
|
</script>
|
|
|
|
{{end}}
|
|
</div>
|
|
|
|
{{template "app_press_script" .}}
|
|
</div>{{/* .app-page */}}
|
|
{{template "layout_end" .}}
|
|
{{end}}
|