Files
felhom-controller/controller/internal/web/templates/app_data.html
T
admin f037be7a6a
gates / gates (push) Successful in 48s
R-970: the box's hub key opens no route; Data tab leftovers; R-1103: the install form's dead branches
R-970: /api/config, /api/config/hash, /api/config/apply removed (they read and replaced controller.yaml with
every secret, reachable with the box's own hub API key, unsigned; nothing called them). The hub-key Bearer
door on /api/selfupdate/ and /api/geo/ and the matching CSRF exemption are gone; the dashboard keeps using
those with a session. The route table is buildMux, so a test drives the real one.

Data tab: the export password field is autocomplete=new-password with a non-login name; the 2nd copy's
status rows that repeated the form are gone (the no-target reason moves into the form).

R-1103: app_fields.html loses its installed-only branches (no page serves them since R-1095); the tests
that rendered that dead path are retired, their rules asserted on the settings tab (R-694 moved there).

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-16 18:24:00 +02:00

480 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 datapath-fb-note" data-fb-login-note="1">{{T "app_page.files_login_note"}} <a href="/apps/filebrowser" class="app-info-link">{{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}}
{{/* „BIZTONSAGI MENTES" — ONE section (v0.321.0). The nightly backup and the 2nd copy were two sections with
two titles for one subject, and the 2nd copy's state sat in a two-column grid of monospace values. Now: the
nightly backup is a row, then what the 2nd copy means and the form that shows and changes it (v0.322.0: the
rows that repeated the form are gone). The words
are plain: no „off-drive", no „PBS", no „DB/konfiguráció". Pinned by TestAppData_OneBackupSection. */}}
<div class="app-section" id="app-backup-section" data-backup-section="1">
<h3>{{T "deploy.biztonsagi_mentes"}}</h3>
<div class="app-field-row" data-backup-row="nightly">
<div class="app-field-label">{{T "app_page.backup_nightly"}}</div>
<div class="app-field-value">{{T "app_page.backup_nightly_text"}} <a href="/backups" class="app-info-link">{{T "app_page.backup_status_link"}}</a></div>
</div>
{{- if .Tier2}}{{with .Tier2}}
{{- /* v0.322.0: the 2nd copy's state is said ONCE — by the form below (checkbox, target, hint). Its rows here
repeated the same three facts. */}}
<p class="app-field-hint" style="margin-top:12px">{{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}}
{{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" data-second-backup-form="1">
{{$.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 .NoTarget}}
<div class="form-hint text-error" style="margin-top:.4rem" data-second-backup-notarget="1">{{T "tier2_config.nincs_elerheto_off_drive_cel"}} — {{.NoTargetReason}}</div>
{{end}}
{{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}}{{end}}
</div>
{{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">
{{/* v0.322.0: autocomplete="new-password" and a name that is not a login field — the browser filled the household's
saved dashboard password in here, and an export could be locked with it unnoticed. */}}
<input type="password" id="fabEncryptionKey" name="fab_encryption_key" autocomplete="new-password" data-export-key="1" class="form-control" placeholder="{{T "app_export.titkositasi_jelszo"}}" style="flex:1">
<button class="btn btn-sm btn-outline" onclick="togglePw(this)" type="button" id="exportPwToggle" aria-label="{{T "app_export.jelszo_mutatasa"}}">{{T "deploy.megjelenites"}}</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(btn) {
var inp = document.getElementById('fabEncryptionKey');
var show = inp.type === 'password';
inp.type = show ? 'text' : 'password';
if (btn) { btn.textContent = show ? '{{T "app_info.elrejtes"}}' : '{{T "deploy.megjelenites"}}'; }
}
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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
async function startExport() {
var drive = document.getElementById('destDrive').value;
var password = document.getElementById('fabEncryptionKey').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 = '&#9675;'; // pending
if (s.status === 'running') icon = '&#10227;';
if (s.status === 'done') icon = '&#10003;';
if (s.status === 'failed') icon = '&#10007;';
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}}