f30a80841e
gates / gates (push) Successful in 50s
FileBrowser (and traefik, cloudflared, samba) has no app.yaml, so Deployed is false. Since v0.315.0 the app page read Deployed alone and showed FileBrowser "not installed", with Install and without its login. A protected infra stack is now installed by definition on the app page (Deployed || Protected, as every other surface already counted): settings/guide/log tabs, Restart only, no install page, no data tab. The Install press would have deployed the infra stack as an app: DeployStack and the deploy API now refuse a protected stack. In the operator state (the box does not know FileBrowser's password - both demo boxes) the login panel offers "Új jelszó beállítása": the file browser's own CLI with the server stopped (spiked on demo-hp: new 200, old 401, users/shares/settings unchanged), verified, stored encrypted, state generated. Every installed app's settings tab has the login panel with exactly one answer: the box's fields, the login the app wrote (InitialCreds), or a sentence chosen by the catalog's new app_info.login_kind (first_visit default, email, none; none behind the family gate says the family sign-in is the login). "Fájlok ↗" opens the library folder, else the first declared folder, else a media app's first present drive's media folder. The data tab lists each drive's media folders; a folder whose drive is not bound into FileBrowser is text, never a link. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
507 lines
24 KiB
HTML
507 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">
|
|
<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">
|
|
{{- /* v0.320.0: a folder whose FileBrowser source is not on the box now is text, never a link. */}}
|
|
{{if .Link}}<a href="{{.Link}}" target="_blank" rel="noopener" class="datapath-link"{{if .IsMedia}} data-media-folder="1"{{end}}>{{.Label}} ↗</a>{{else}}<span class="datapath-link" data-folder-unavailable="1">{{.Label}}</span> <span class="datapath-space">{{T "datapath.unavailable"}}</span>{{end}}
|
|
{{if .FreeSpace}}<span class="datapath-space">{{.FreeSpace}}</span>{{end}}
|
|
</div>
|
|
{{if .Consequence}}<p class="datapath-note">{{.Consequence}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{/* v0.320.0: the file manager asks for its own login the first time — say so, and where it is. */}}
|
|
<p class="form-hint" data-fb-login-note="1">{{T "app_page.files_login_note"}} <a href="/apps/filebrowser">{{T "app_page.files_login_link"}}</a></p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if and .Stack.Deployed .MigrateTargets}}
|
|
<div class="app-section">
|
|
<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">
|
|
<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" class="form-control" onchange="loadEstimate()" style="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" class="form-control" placeholder="{{T "app_export.titkositasi_jelszo"}}" style="flex:1">
|
|
<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="app-section" style="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="app-section" style="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}}
|